Merge remote-tracking branch 'origin/master' into codex/agent-session-jsonl-location
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/core-data-structures/bash.md # docs/module-graph.md # docs/rfc/INDEX.md # docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md # docs/rfc/implemented/feature/2026-06-30-hook-bridges.md # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.golden.md # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl # packages/bash/bash-local/README.md # packages/bash/bash-local/src/run.ts # packages/bash/bash-local/tests/run.spec.ts # packages/bash/bash/README.md # packages/bash/tool-bash/README.md # packages/bash/tool-bash/src/index.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-core/src/index.ts # packages/session-persistence/session-persistence-jsonl/src/index.ts # packages/session-persistence/session-persistence-sqlite/src/index.ts # packages/session-persistence/session-persistence/README.md # packages/session-persistence/session-persistence/src/index.ts # packages/ui/acp-agent/README.md # packages/ui/stdio-agent/README.md
This commit is contained in:
@@ -54,12 +54,8 @@ runPersistenceContract('jsonl', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Run the shared coordinator orchestration suite against the real JSONL backend.
|
||||
// One temp root is the shared storage scope (two mounted instances over the same
|
||||
// root = HMR/reload). `corruptTail` appends a partial, newline-less fragment to
|
||||
// the session's .jsonl past the committed region — a never-committed torn tail
|
||||
// that drives the coordinator's commitRepair-with-tornMarker branch over real
|
||||
// file bytes.
|
||||
// Two mounts share this temp root to exercise reload. `corruptTail` appends a partial,
|
||||
// newline-less fragment past the committed region so coordinator repair runs on real file bytes.
|
||||
runCoordinatorContract('jsonl', async (): Promise<CoordinatorFixture> => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-jsonl-coord-'))
|
||||
return {
|
||||
@@ -385,10 +381,9 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
||||
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
|
||||
JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1
|
||||
].join('\n') + '\n'
|
||||
// No committed turn/end, so the gap is a tolerated crash boundary: scanLog
|
||||
// PRESERVES the contiguous prefix (turn/start seq 0) — real interrupted-turn
|
||||
// work, not discarded — and stops at the gap. The orphaned open turn is
|
||||
// closed by loadCore's synthetic turn/end, not here.
|
||||
// No committed turn/end, so the gap is a tolerated crash boundary: scanLog PRESERVES the
|
||||
// contiguous prefix (turn/start seq 0) — real interrupted-turn work, not discarded — and
|
||||
// stops at the gap. `loadCore`, not this scanner, later closes the orphaned turn.
|
||||
expect(scanLog(Buffer.from(log)).events.map(e => e.seq)).toEqual([0])
|
||||
})
|
||||
|
||||
@@ -508,9 +503,8 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
})
|
||||
|
||||
it('list reads a header line longer than the 8KB read chunk', async () => {
|
||||
// readFirstLine accumulates across reads when the first line exceeds its
|
||||
// buffer. Plant a valid header whose line is > 8192 bytes (a long extra
|
||||
// field is tolerated by the header type guard) and confirm list() reads it.
|
||||
// A tolerated extra field makes this valid header exceed the 8192-byte read buffer, proving
|
||||
// `readFirstLine` accumulates chunks before `list()` parses it.
|
||||
const bucket = join(root, '_no-cwd')
|
||||
await mkdir(bucket, { recursive: true })
|
||||
const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, pad: 'x'.repeat(9000) })
|
||||
@@ -530,10 +524,8 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
for (const s of ctx.sessions.list()) await ctx.parallel('session/flush', s)
|
||||
await sessFiberA.dispose()
|
||||
|
||||
// A NEW live Session object reuses id "reuse". The init cache is keyed by
|
||||
// the Session OBJECT, so this gets its OWN onCreated (not A's stale promise)
|
||||
// — which detects the on-disk collision and rejects, rather than silently
|
||||
// appending the new session's events onto A's log under a stale cursor.
|
||||
// A new Session object reuses the id. Object-keyed initialization must run independently,
|
||||
// detect the disk collision, and reject instead of appending through session A's stale cursor.
|
||||
const backend = ctx.sessionPersistence as unknown as { inits: Map<Session, Promise<void>> }
|
||||
let b!: Session
|
||||
await ctx.plugin(Object.assign((inner: Context) => {
|
||||
@@ -551,13 +543,9 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
await ctx.sessionPersistence.append(SessionId('x'), oneTurnLog())
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
// Backend 2 over the SAME root. A live no-cwd session reuses id "x". Because
|
||||
// loadLive(id, undefined) is the DEFINITE no-cwd bucket (NOT an all-buckets
|
||||
// scan), case-2 adoption does NOT match the "/w" log — so it would NOT
|
||||
// silently graft the no-cwd events onto the "/w" log with a mismatched cwd
|
||||
// (the bug a non-scope-exact loadLive caused). It falls through to the
|
||||
// new-session path, where createCore's any-cwd collision probe (loadStored)
|
||||
// catches the duplicate id and REJECTS — the id is taken in another bucket.
|
||||
// Backend 2 creates a no-cwd session whose id exists only in `/w`. Exact `loadLive(id,
|
||||
// undefined)` must not adopt across buckets; the any-cwd collision check then rejects instead
|
||||
// of grafting no-cwd events onto a log with mismatched cwd.
|
||||
const ctx2 = new Context()
|
||||
await ctx2.plugin(SessionStore)
|
||||
await ctx2.plugin(SessionPersistenceJsonl, { root })
|
||||
@@ -625,9 +613,8 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
})
|
||||
|
||||
it('list surfaces a non-ENOENT root error (ENOTDIR) instead of reporting no sessions', async () => {
|
||||
// A durable backend must NOT collapse a storage fault to "no sessions". Point
|
||||
// the root at a regular FILE: readdir then fails with ENOTDIR, which must
|
||||
// propagate rather than be swallowed as an empty listing.
|
||||
// A durable backend must not collapse a storage fault to "no sessions". Making the root a
|
||||
// regular file forces ENOTDIR from `readdir`, which must propagate.
|
||||
const filePath = join(root, 'not-a-dir')
|
||||
await writeFile(filePath, 'x')
|
||||
const ctx2 = new Context()
|
||||
@@ -638,11 +625,8 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
})
|
||||
|
||||
it('loadLive surfaces a non-ENOENT lookup error (ENOTDIR) instead of reporting absent', async () => {
|
||||
// A non-ENOENT error from the per-id open() must surface, not be collapsed to
|
||||
// "not found" (which would let live-adoption proceed under a false absence
|
||||
// assumption). A live session's onCreated reaches loadLive(id, cwd) →
|
||||
// exists(logPath). Make that cwd's bucket DIRECTORY a regular file: open()ing
|
||||
// `bucket/<id>.jsonl` under it then fails ENOTDIR.
|
||||
// A non-ENOENT per-id open error must surface rather than become "not found" and permit false
|
||||
// live adoption. Making the cwd bucket a regular file forces ENOTDIR for its child log path.
|
||||
const cwd = '/x'
|
||||
const ctx2 = new Context()
|
||||
await ctx2.plugin(SessionStore)
|
||||
@@ -761,10 +745,9 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
||||
|
||||
it('Session.append rejects a non-serializable event at the source (never enters the log)', () => {
|
||||
const session = ctx.sessions.create(SessionId('reject-bad'))
|
||||
// Serializability is enforced at the source: Session.append throws on a
|
||||
// BigInt-bearing event BEFORE it enters session.events, so the durable log
|
||||
// can never diverge from the live log. The throw surfaces at the caller's
|
||||
// append site, not asynchronously in a backend flush.
|
||||
// Serializability is enforced at the source: Session.append throws on a BigInt-bearing
|
||||
// event before it enters session.events, so the durable log can never diverge from the live
|
||||
// log. The error therefore surfaces synchronously at append, not later during backend flush.
|
||||
expect(() => {
|
||||
session.append('user/message', { content: [{ type: 'text', text: 'bad' }], source: { kind: 'user' }, bad: 1n } as never, { surfaceOp: 'append' })
|
||||
}).toThrow(/non-JSON-serializable/)
|
||||
|
||||
Reference in New Issue
Block a user