Browse Source

test(session): cover legacy migration paths

_Kerman 1 month ago
parent
commit
384c87899c

File diff suppressed because it is too large
+ 0 - 0
examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl


+ 31 - 29
examples/acp-agent/tests/snapshots/fs-glob-sampling/session.jsonl

@@ -1,31 +1,33 @@
 {"type":"session","version":0,"id":"4428b809-66d5-4ea2-9a03-89de742fcda1","createdAt":1785591986068,"cwd":"{{cwd}}","delegationDepth":0}
 {"type":"session","version":0,"id":"4428b809-66d5-4ea2-9a03-89de742fcda1","createdAt":1785591986068,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"turn/start","seq":0,"time":1785591986072,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
-{"type":"user/message","seq":1,"time":1785591986073,"data":{"content":[{"type":"text","text":"Call glob exactly once with pattern * and path tree. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"},"role":"user","id":"3d05fb76-4185-460b-9c6a-8c1b2495bc9f"},"surfaceOp":"append"}
-{"type":"session/title","seq":2,"time":1785591986074,"data":{"title":"Call glob exactly once with","messageSeqs":[1],"source":{"kind":"fallback"}}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785815911816,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call glob exactly once with pattern * and path tree. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"},"role":"user","id":"b7f14c10-5c33-42b5-994e-60eed15a990e"}]}}
+{"type":"agent/inbox/spliced","seq":1,"time":1785815911817,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"turn/start","seq":2,"time":1785815911838,"data":{"turn":1}}
 {"type":"step/start","seq":3,"time":1785591986092,"data":{"turn":1,"step":1}}
 {"type":"step/start","seq":3,"time":1785591986092,"data":{"turn":1,"step":1}}
-{"type":"request/header","seq":4,"time":1785591986093,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":5,"time":1785591986094,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
-{"type":"assistant/chunk","seq":6,"time":1785591987500,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":7,"time0":1785591987500,"data":{"turn":1,"step":1,"index":0,"dt":[29,58,1,0,0,0,51,0,0,46,0,191,1,0,0,0,0,0,0,0,1,0,0,0,0,0,99],"texts":["The"," user"," wants"," me"," to"," call"," glob"," exactly"," once"," with"," pattern"," *"," and"," path"," tree",","," then"," reply"," with"," exactly"," \"","G","LOB","_S","AM","PL","ED","\"."]}}
-{"type":"assistant/chunk","seq":35,"time":1785591988034,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
-{"type":"tool-call-chunks","seq0":36,"time0":1785591988035,"data":{"turn":1,"step":1,"index":1,"dt":[55,0,0,1,45,0,0,57,14,0,0,0,0,77,0,0,54],"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","args":["","{","\"","pattern","\"",": ","\"","*","\"",", ","\"","path","\"",": ","\"","tree","\"","}"]}}
-{"type":"assistant/chunk","seq":54,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."}}}}
-{"type":"assistant/chunk","seq":55,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}}}
-{"type":"assistant/chunk","seq":56,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}}}}
-{"type":"assistant/chunk","seq":57,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":58,"time":1785591988430,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."},{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"b74cbab2-c017-4e44-8c09-a7745d8b274a"},"usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57],"surfaceOp":"append"}
-{"type":"tool/call","seq":59,"time":1785591988431,"data":{"turn":1,"step":1,"callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}
-{"type":"tool/result","seq":60,"time":1785591988476,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430"},"content":[{"type":"tool-result","toolCallId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","content":[{"type":"text","text":"tree/archive/a.ts\ntree/docs/guide.md\ntree/src/index.ts\ntree/test/spec.ts\n\n(Showing 4 of 8 paths, sampled across 4 of the 6 top-level entries this pattern matched instead of taken in modification-time order. Narrow path to inspect a specific subtree. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"10284f88-4890-49ed-9a17-56edbd6bfaa7"},"meta":{"shape":"paths","paths":["tree/archive/a.ts","tree/docs/guide.md","tree/src/index.ts","tree/test/spec.ts"],"truncated":true,"total":8}},"sourceEventSeqs":[59],"surfaceOp":"append"}
-{"type":"step/end","seq":61,"time":1785591988476,"data":{"turn":1,"step":1}}
-{"type":"step/start","seq":62,"time":1785591988482,"data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","seq":63,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":64,"time0":1785591989939,"data":{"turn":1,"step":2,"index":0,"dt":[0,0,0,49,36,103,1,0,0,326,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,14],"texts":["The"," glob"," result"," shows"," it"," was"," sampled"," -"," ","4"," of"," ","8"," paths"," across"," ","4"," of"," ","6"," top","-level"," entries","."," I"," need"," to"," reply"," with"," exactly"," \"","G","LOB","_S","AM","PL","ED","\""," as"," instructed","."]}}
-{"type":"assistant/chunk","seq":105,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":106,"time0":1785591990470,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,0,48,0],"texts":["G","LOB","_S","AM","PL","ED"]}}
-{"type":"assistant/chunk","seq":112,"time":1785591990526,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."}}}}
-{"type":"assistant/chunk","seq":113,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"GLOB_SAMPLED"}}}}
-{"type":"assistant/chunk","seq":114,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}}}}
-{"type":"assistant/chunk","seq":115,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":116,"time":1785591990527,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."},{"type":"text","text":"GLOB_SAMPLED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"dd3a9c28-43b2-4fdc-8089-1547309a71c0"},"usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}},"sourceEventSeqs":[63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115],"surfaceOp":"append"}
-{"type":"step/end","seq":117,"time":1785591990527,"data":{"turn":1,"step":2}}
-{"type":"turn/end","seq":118,"time":1785591990528,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"user/message","seq":4,"time":1785815911838,"data":{"content":[{"type":"text","text":"Call glob exactly once with pattern * and path tree. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"},"role":"user","id":"b7f14c10-5c33-42b5-994e-60eed15a990e"},"surfaceOp":"append"}
+{"type":"session/title","seq":5,"time":1785815911838,"data":{"title":"Call glob exactly once with","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":6,"time":1785815911840,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":7,"time":1785815911840,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
+{"type":"assistant/chunk","seq":8,"time":1785591987500,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":9,"time0":1785591987529,"data":{"turn":1,"step":1,"index":0,"dt":[58,1,0,0,0,51,0,0,46,0,191,1,0,0,0,0,0,0,0,1,0,0,0,0,0,99,57],"texts":["The"," user"," wants"," me"," to"," call"," glob"," exactly"," once"," with"," pattern"," *"," and"," path"," tree",","," then"," reply"," with"," exactly"," \"","G","LOB","_S","AM","PL","ED","\"."]}}
+{"type":"assistant/chunk","seq":37,"time":1785591988035,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
+{"type":"tool-call-chunks","seq0":38,"time0":1785591988090,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,1,45,0,0,57,14,0,0,0,0,77,0,0,54,89],"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","args":["","{","\"","pattern","\"",": ","\"","*","\"",", ","\"","path","\"",": ","\"","tree","\"","}"]}}
+{"type":"assistant/chunk","seq":56,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."}}}}
+{"type":"assistant/chunk","seq":57,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}}}
+{"type":"assistant/chunk","seq":58,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}}}}
+{"type":"assistant/chunk","seq":59,"time":1785815911849,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":60,"time":1785815911849,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."},{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"6aa63a52-8daf-41e1-84ff-6a991c1c65c2"},"usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59],"surfaceOp":"append"}
+{"type":"tool/call","seq":61,"time":1785815911849,"data":{"turn":1,"step":1,"callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}
+{"type":"tool/result","seq":62,"time":1785815911873,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430"},"content":[{"type":"tool-result","toolCallId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","content":[{"type":"text","text":"tree/archive/a.ts\ntree/docs/guide.md\ntree/src/index.ts\ntree/test/spec.ts\n\n(Showing 4 of 8 paths, sampled across 4 of the 6 top-level entries this pattern matched instead of taken in modification-time order. Narrow path to inspect a specific subtree. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"1aa9a147-cf62-448c-a3a3-607afc166c8b"},"meta":{"shape":"paths","paths":["tree/archive/a.ts","tree/docs/guide.md","tree/src/index.ts","tree/test/spec.ts"],"truncated":true,"total":8}},"sourceEventSeqs":[61],"surfaceOp":"append"}
+{"type":"step/end","seq":63,"time":1785815911874,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":64,"time":1785815911886,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":65,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":66,"time0":1785591989939,"data":{"turn":1,"step":2,"index":0,"dt":[0,0,49,36,103,1,0,0,326,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,14,0],"texts":["The"," glob"," result"," shows"," it"," was"," sampled"," -"," ","4"," of"," ","8"," paths"," across"," ","4"," of"," ","6"," top","-level"," entries","."," I"," need"," to"," reply"," with"," exactly"," \"","G","LOB","_S","AM","PL","ED","\""," as"," instructed","."]}}
+{"type":"assistant/chunk","seq":107,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":108,"time0":1785591990470,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,48,0,8],"texts":["G","LOB","_S","AM","PL","ED"]}}
+{"type":"assistant/chunk","seq":114,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."}}}}
+{"type":"assistant/chunk","seq":115,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"GLOB_SAMPLED"}}}}
+{"type":"assistant/chunk","seq":116,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}}}}
+{"type":"assistant/chunk","seq":117,"time":1785815911893,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":118,"time":1785815911894,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."},{"type":"text","text":"GLOB_SAMPLED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"2ceaed84-ac69-436e-82a2-f1bdacb8ca04"},"usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}},"sourceEventSeqs":[65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117],"surfaceOp":"append"}
+{"type":"step/end","seq":119,"time":1785815911894,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":120,"time":1785815911894,"data":{"turn":1,"step":2,"reason":{"kind":"completed"}}}

+ 167 - 2
packages/session-persistence/session-persistence/tests/coordinator-contract.ts

@@ -166,6 +166,35 @@ function preReactLoopLog(): SessionEvent[] {
       type: 'turn/end', seq: 17, time: 18,
       type: 'turn/end', seq: 17, time: 18,
       data: { turn: 5, reason: { kind: 'error', step: 1, message: 'old thrown value' } },
       data: { turn: 5, reason: { kind: 'error', step: 1, message: 'old thrown value' } },
     },
     },
+    {
+      type: 'turn/start', seq: 18, time: 19,
+      data: { turn: 6, trigger: { kind: 'message', source: { kind: 'user' } } },
+    },
+    {
+      type: 'turn/end', seq: 19, time: 20,
+      data: {
+        turn: 6,
+        reason: {
+          kind: 'error',
+          step: 0,
+          failure: {
+            message: 'old detailed provider failure',
+            code: 'RATE_LIMIT',
+            status: 429,
+            providerRetryAfterMs: 1000,
+            requestId: 'request-1',
+          },
+        },
+      },
+    },
+    {
+      type: 'turn/start', seq: 20, time: 21,
+      data: { turn: 7, trigger: { kind: 'message', source: { kind: 'user' } } },
+    },
+    {
+      type: 'turn/end', seq: 21, time: 22,
+      data: { turn: 7, reason: { kind: 'error', step: 0, message: 'old coded error', code: 'CODED' } },
+    },
   ] as unknown as SessionEvent[]
   ] as unknown as SessionEvent[]
 }
 }
 
 
@@ -459,7 +488,9 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
         for (const snapshot of snapshots) {
         for (const snapshot of snapshots) {
           expect(snapshot.events.some(event => (event.type as string) === 'steering/message')).toBe(false)
           expect(snapshot.events.some(event => (event.type as string) === 'steering/message')).toBe(false)
           expect(snapshot.events.filter(event => event.type === 'turn/start').map(event => event.data))
           expect(snapshot.events.filter(event => event.type === 'turn/start').map(event => event.data))
-            .toEqual([{ turn: 1 }, { turn: 2 }, { turn: 3 }, { turn: 4 }, { turn: 5 }])
+            .toEqual([
+              { turn: 1 }, { turn: 2 }, { turn: 3 }, { turn: 4 }, { turn: 5 }, { turn: 6 }, { turn: 7 },
+            ])
           expect(snapshot.events.filter(event => event.type === 'turn/end').map(event => event.data)).toEqual([
           expect(snapshot.events.filter(event => event.type === 'turn/end').map(event => event.data)).toEqual([
             { turn: 1, step: 1, reason: { kind: 'completed' } },
             { turn: 1, step: 1, reason: { kind: 'completed' } },
             {
             {
@@ -474,6 +505,25 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
               step: 1,
               step: 1,
               reason: { kind: 'error', error: { message: 'old thrown value', code: 'UNKNOWN' } },
               reason: { kind: 'error', error: { message: 'old thrown value', code: 'UNKNOWN' } },
             },
             },
+            {
+              turn: 6,
+              step: 0,
+              reason: {
+                kind: 'error',
+                error: {
+                  message: 'old detailed provider failure',
+                  code: 'RATE_LIMIT',
+                  status: 429,
+                  providerRetryAfterMs: 1000,
+                  requestId: 'request-1',
+                },
+              },
+            },
+            {
+              turn: 7,
+              step: 0,
+              reason: { kind: 'error', error: { message: 'old coded error', code: 'CODED' } },
+            },
           ])
           ])
 
 
           const resumed = new Session(id, snapshot.events, snapshot.meta)
           const resumed = new Session(id, snapshot.events, snapshot.meta)
@@ -490,7 +540,29 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
           data: { id: legacySteering.data.message.id },
           data: { id: legacySteering.data.message.id },
         })
         })
         expect(suffix.events.filter(event => event.type === 'turn/end').map(event => event.data.step))
         expect(suffix.events.filter(event => event.type === 'turn/end').map(event => event.data.step))
-          .toEqual([1, 1, 0, 0, 1])
+          .toEqual([1, 1, 0, 0, 1, 0, 0])
+
+        const flatId = SessionId('pre-react-loop-flat-steering')
+        await ctx.sessionPersistence.create(meta(flatId, WORK))
+        await ctx.sessionPersistence.append(flatId, [{
+          type: 'steering/message',
+          seq: 0,
+          time: 1,
+          data: {
+            turn: 1,
+            content: [{ type: 'text', text: 'flat steering' }],
+            source: { kind: 'user' },
+          },
+          surfaceOp: 'append',
+        } as unknown as SessionEvent])
+        expect((await ctx.sessionPersistence.inspect(flatId)).events[0]).toMatchObject({
+          type: 'user/message',
+          data: {
+            id: `legacy-message:${flatId}:0`,
+            role: 'user',
+            content: [{ type: 'text', text: 'flat steering' }],
+          },
+        })
       } finally {
       } finally {
         await fiber.dispose()
         await fiber.dispose()
         await fix.cleanup()
         await fix.cleanup()
@@ -538,6 +610,13 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
             } as unknown as SessionEvent,
             } as unknown as SessionEvent,
             message: 'malformed pre-react-loop steering/message',
             message: 'malformed pre-react-loop steering/message',
           },
           },
+          {
+            id: 'invalid-old-steering-data',
+            event: {
+              type: 'steering/message', seq: 0, time: 1, surfaceOp: 'append', data: null,
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop steering/message',
+          },
           {
           {
             id: 'invalid-old-turn-end',
             id: 'invalid-old-turn-end',
             event: {
             event: {
@@ -546,6 +625,54 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
             } as unknown as SessionEvent,
             } as unknown as SessionEvent,
             message: 'malformed pre-react-loop turn/end',
             message: 'malformed pre-react-loop turn/end',
           },
           },
+          {
+            id: 'invalid-old-turn-end-reason',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: null },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
+          {
+            id: 'invalid-old-turn-end-aborted',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: { kind: 'aborted', extra: true } },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
+          {
+            id: 'invalid-old-turn-end-disposed',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: { kind: 'disposed', extra: true } },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
+          {
+            id: 'invalid-old-turn-end-error-step',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: { kind: 'error', step: -1, message: 'bad step' } },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
+          {
+            id: 'invalid-old-turn-end-error-code',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: { kind: 'error', step: 0, message: 'bad code', code: 1 } },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
+          {
+            id: 'invalid-old-turn-end-kind',
+            event: {
+              type: 'turn/end', seq: 0, time: 1,
+              data: { turn: 1, reason: { kind: 'unknown' } },
+            } as unknown as SessionEvent,
+            message: 'malformed pre-react-loop turn/end',
+          },
         ]
         ]
         for (const malformed of malformedLegacy) {
         for (const malformed of malformedLegacy) {
           const malformedId = SessionId(malformed.id)
           const malformedId = SessionId(malformed.id)
@@ -579,6 +706,22 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
         } as unknown as SessionEvent])
         } as unknown as SessionEvent])
         await expect(ctx.sessionPersistence.inspect(pluginId))
         await expect(ctx.sessionPersistence.inspect(pluginId))
           .resolves.toMatchObject({ events: [{ type: 'plugin/test', data: null }] })
           .resolves.toMatchObject({ events: [{ type: 'plugin/test', data: null }] })
+        await expect(ctx.sessionPersistence.readFrom(pluginId, 0))
+          .resolves.toMatchObject({ events: [{ type: 'plugin/test', data: null }] })
+
+        for (const type of ['user/message', 'assistant/message'] as const) {
+          const missingContentId = SessionId(`invalid-${type}-without-content`)
+          await ctx.sessionPersistence.create(meta(missingContentId, WORK))
+          await ctx.sessionPersistence.append(missingContentId, [{
+            type,
+            seq: 0,
+            time: 1,
+            surfaceOp: 'append',
+            data: {},
+          } as unknown as SessionEvent])
+          await expect(ctx.sessionPersistence.readFrom(missingContentId, 0))
+            .rejects.toThrow('lacks an identified message')
+        }
       } finally {
       } finally {
         await fiber.dispose()
         await fiber.dispose()
         await fix.cleanup()
         await fix.cleanup()
@@ -975,6 +1118,28 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
       }
       }
     })
     })
 
 
+    it('a live session whose complete seed matches loaded ownerless state claims it without appending', async () => {
+      const fix = await makeFixture()
+      const { ctx, fiber } = await freshCtx(fix)
+      try {
+        const id = SessionId('claim-exact')
+        const completeSeed = [
+          ...oneTurnLog(),
+          { type: 'session/end-seed', seq: 6, time: 7, data: {} },
+        ] as SessionEvent[]
+        await ctx.sessionPersistence.create(meta(id, WORK))
+        await ctx.sessionPersistence.append(id, completeSeed)
+        const { events } = await ctx.sessionPersistence.load(id)
+
+        const live = ctx.sessions.create(id, { seed: events, meta: { cwd: WORK } })
+        await expect(ctx.sessions.flush(live)).resolves.toBe(true)
+        expect((await ctx.sessionPersistence.load(id)).events).toEqual(events)
+      } finally {
+        await fiber.dispose()
+        await fix.cleanup()
+      }
+    })
+
     it('a live session whose seed matches the loaded prefix claims ownerless state and persists the suffix', async () => {
     it('a live session whose seed matches the loaded prefix claims ownerless state and persists the suffix', async () => {
       const fix = await makeFixture()
       const fix = await makeFixture()
       const { ctx, fiber } = await freshCtx(fix)
       const { ctx, fiber } = await freshCtx(fix)

Some files were not shown because too many files changed in this diff