mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id
# Conflicts: # docs/cordis-catalog/services.md
This commit is contained in:
@@ -246,7 +246,7 @@ list(): Session[]
|
||||
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:609`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:612`](../../packages/core/session/src/index.ts)
|
||||
|
||||
## `ctx.skills` — `SkillService`
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ The session log maintains two representations that cost more machinery than thei
|
||||
|
||||
The request-header subsystem implements a custom system/tool delta codec and transmission-decision layer even though its contract says deltas are an encoding optimization, not a reconstructability requirement. Retaining the initial/resume full snapshot at each loop-instance boundary, then writing a canonical full `request/header` whenever that instance's assembled header changes, preserves replay while deleting `SystemDelta`, `ToolsDelta`, round-trip fallback, and the durable `request/header-delta` variant. Codec-only vocabulary disappears with the codec, not because its individual arms were invalid.
|
||||
|
||||
This proposal deliberately retains append and replacement `sourceEventSeqs`, crash-repair provenance, and all `SessionStartSource` variants: implemented RFCs give those fields an audit/interception role that zero current readers does not overturn.
|
||||
The implementation retains append and replacement `sourceEventSeqs`, crash-repair provenance, and all `SessionStartSource` variants because those fields have an audit/interception role that zero current readers does not overturn.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type AgentUnderTest,
|
||||
type LaunchedAcpTestAgent,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
/**
|
||||
* End-to-end: boot examples/acp-agent as a real subprocess speaking ACP over
|
||||
@@ -31,16 +32,11 @@ let spawned: LaunchedAcpTestAgent | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await spawned?.close('SIGKILL')
|
||||
} finally {
|
||||
spawned = undefined
|
||||
try {
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
} finally {
|
||||
workdir = undefined
|
||||
}
|
||||
}
|
||||
const ownedSpawned = spawned
|
||||
const ownedWorkdir = workdir
|
||||
spawned = undefined
|
||||
workdir = undefined
|
||||
await cleanupAcpExampleTest(ownedSpawned, ownedWorkdir)
|
||||
})
|
||||
|
||||
describe('acp-agent over real stdio (no key required)', () => {
|
||||
|
||||
38
examples/acp-agent/tests/cleanup.e2e.ts
Normal file
38
examples/acp-agent/tests/cleanup.e2e.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/** Regression coverage for ACP example teardown. */
|
||||
|
||||
import { access, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
let fallbackWorkdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (fallbackWorkdir !== undefined) await rm(fallbackWorkdir, { recursive: true, force: true })
|
||||
fallbackWorkdir = undefined
|
||||
})
|
||||
|
||||
describe('cleanupAcpExampleTest', () => {
|
||||
it('removes the workspace after process shutdown fails', async () => {
|
||||
fallbackWorkdir = await mkdtemp(join(tmpdir(), 'acp-cleanup-'))
|
||||
const closeFailure = new Error('close failed')
|
||||
const spawned = { close: vi.fn().mockRejectedValue(closeFailure) }
|
||||
|
||||
await expect(cleanupAcpExampleTest(spawned, fallbackWorkdir))
|
||||
.rejects.toMatchObject({ errors: [closeFailure] })
|
||||
await expect(access(fallbackWorkdir)).rejects.toThrow()
|
||||
fallbackWorkdir = undefined
|
||||
})
|
||||
|
||||
it('reports process and workspace failures together', async () => {
|
||||
const closeFailure = new Error('close failed')
|
||||
const spawned = { close: vi.fn().mockRejectedValue(closeFailure) }
|
||||
|
||||
const failure = await cleanupAcpExampleTest(spawned, '\0').catch((error: unknown) => error)
|
||||
|
||||
expect(failure).toBeInstanceOf(AggregateError)
|
||||
expect((failure as AggregateError).errors).toHaveLength(2)
|
||||
expect((failure as AggregateError).errors[0]).toBe(closeFailure)
|
||||
})
|
||||
})
|
||||
23
examples/acp-agent/tests/cleanup.ts
Normal file
23
examples/acp-agent/tests/cleanup.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/** Shared teardown for ACP example tests. */
|
||||
|
||||
import { rm } from 'node:fs/promises'
|
||||
import type { LaunchedAcpTestAgent } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
|
||||
/**
|
||||
* Close the test agent, then remove its workspace, attempting both operations
|
||||
* and reporting every failure instead of allowing the later one to mask the
|
||||
* earlier one.
|
||||
*/
|
||||
export async function cleanupAcpExampleTest(
|
||||
spawned: Pick<LaunchedAcpTestAgent, 'close'> | undefined,
|
||||
workdir: string | undefined,
|
||||
): Promise<void> {
|
||||
const results: PromiseSettledResult<unknown>[] = []
|
||||
if (spawned !== undefined) results.push(...await Promise.allSettled([spawned.close('SIGKILL')]))
|
||||
if (workdir !== undefined) results.push(...await Promise.allSettled([rm(workdir, { recursive: true, force: true })]))
|
||||
|
||||
const failures = results
|
||||
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||||
.map(result => result.reason as unknown)
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'ACP example cleanup failed')
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type AgentUnderTest,
|
||||
type LaunchedAcpTestAgent,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
/**
|
||||
* The default ACP composition (`cordis.yml`) end to end.
|
||||
@@ -81,16 +82,11 @@ let spawned: Spawned | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await spawned?.close('SIGKILL')
|
||||
} finally {
|
||||
spawned = undefined
|
||||
try {
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
} finally {
|
||||
workdir = undefined
|
||||
}
|
||||
}
|
||||
const ownedSpawned = spawned
|
||||
const ownedWorkdir = workdir
|
||||
spawned = undefined
|
||||
workdir = undefined
|
||||
await cleanupAcpExampleTest(ownedSpawned, ownedWorkdir)
|
||||
})
|
||||
|
||||
describe('default sandbox composition keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm, writeFile, access } from 'node:fs/promises'
|
||||
import { mkdtemp, writeFile, access } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type AgentUnderTest,
|
||||
type LaunchedAcpTestAgent,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
/**
|
||||
* With-key e2e: the Claude Code hook bridge running against the REAL acp-agent
|
||||
@@ -38,16 +39,11 @@ let spawned: LaunchedAcpTestAgent | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await spawned?.close('SIGKILL')
|
||||
} finally {
|
||||
spawned = undefined
|
||||
try {
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
} finally {
|
||||
workdir = undefined
|
||||
}
|
||||
}
|
||||
const ownedSpawned = spawned
|
||||
const ownedWorkdir = workdir
|
||||
spawned = undefined
|
||||
workdir = undefined
|
||||
await cleanupAcpExampleTest(ownedSpawned, ownedWorkdir)
|
||||
})
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: a PreToolUse hook blocks bash (real model)', () => {
|
||||
|
||||
@@ -214,6 +214,9 @@ function assertSessionEventEnvelope(value: Record<string, unknown>, index: numbe
|
||||
|
||||
/** Reject request-header vocabulary removed with the legacy delta codec. */
|
||||
function assertSupportedRequestHeader(type: string, data: unknown, location: string): void {
|
||||
if (type === 'request/header-delta') {
|
||||
throw new Error(`${location} uses unsupported legacy request/header-delta format`)
|
||||
}
|
||||
if (type === 'request/header'
|
||||
&& data !== null && typeof data === 'object' && !Array.isArray(data)
|
||||
&& (data as Record<string, unknown>)['reason'] === 'fallback') {
|
||||
|
||||
@@ -62,11 +62,17 @@ describe('foldRequestHeader', () => {
|
||||
})
|
||||
|
||||
describe('legacy request-header format', () => {
|
||||
it('rejects a v0 seed containing request/header-delta', () => {
|
||||
it('rejects request/header-delta in seeds and untyped appends', () => {
|
||||
const legacy = [{
|
||||
type: 'request/header-delta', seq: 0, time: 1, data: { config: CONFIG },
|
||||
}] as unknown as SessionEvent[]
|
||||
expect(() => new Session(SessionId('legacy'), legacy)).toThrow(/unsupported legacy request\/header-delta/)
|
||||
|
||||
const session = new Session(SessionId('legacy-append-delta'))
|
||||
const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
|
||||
expect(() => appendLegacy('request/header-delta', { config: CONFIG }))
|
||||
.toThrow(/unsupported legacy request\/header-delta/)
|
||||
expect(session.events).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('rejects the removed fallback reason in seeds and untyped appends', () => {
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('SessionPersistence service registration', () => {
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects a legacy header delta buffered by a pre-change live producer', async () => {
|
||||
it('rejects a legacy header delta from a pre-change live producer', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(MemoryPersistence)
|
||||
@@ -193,10 +193,9 @@ describe('SessionPersistence service registration', () => {
|
||||
// Model the runtime shape available to JavaScript or a hot-loaded plugin
|
||||
// compiled against the obsolete event vocabulary.
|
||||
const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
|
||||
appendLegacy('request/header-delta', { config: { model: 'legacy' } })
|
||||
|
||||
await expect(ctx.sessions.flush(session))
|
||||
.rejects.toThrow(/unsupported legacy request\/header-delta event at seq 0/)
|
||||
expect(() => appendLegacy('request/header-delta', { config: { model: 'legacy' } }))
|
||||
.toThrow(/unsupported legacy request\/header-delta format/)
|
||||
expect(session.events).toHaveLength(0)
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user