Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md
#	.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.md
#	.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml
#	docs/architecture.i18n.yaml
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/core-data-structures/tools.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	packages/context/session-reference/README.md
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage-cases.ts
#	packages/host/runtime/tests/host-runtime.spec.ts
#	packages/llm/llm-retry/tests/retry.spec.ts
#	packages/session-persistence/session-persistence/src/coordinator.ts
#	packages/support/acp-snapshot/README.md
#	packages/support/acp-snapshot/src/normalize.ts
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/codec.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/codec.spec.ts
#	packages/ui/acp/tests/config-options.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/stream-update.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
_Kerman
2026-07-26 14:05:33 +08:00
1062 changed files with 33621 additions and 30750 deletions

View File

@@ -27,7 +27,7 @@ Each run starts a fresh session by default (its event log lands under `./.sessio
dsh --resume <prior-session-id>
```
`/resume` opens a searchable keyboard selector with titles, activity, last-turn results, model route, durable goal phase, and live/persisted state. The installed `dsh` host flushes and disposes the current app, then replaces the process with `dsh --resume <id>`. The TUI still prints that command on exit and shows it when a custom host cannot hand off. The flag sets `RESUME_SESSION_ID`, wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`); the env var still works directly for the uninstalled demo (`RESUME_SESSION_ID=<prior-session-id> pnpm run demo:tui`), and with neither set the agent starts a new session. A missing or unreadable id starts no agent and emits `agent-loop/config-start-failed`: the TUI prints the failure and exits nonzero. The selector has no cross-process session lock, so deployments with concurrent hosts must coordinate session ownership separately.
`/resume` opens a searchable keyboard selector with titles, activity, last-turn results, model route, durable goal phase, and live/persisted state. The installed `dsh` host flushes and disposes the current app, then replaces the process with `dsh --resume <id>`. The TUI still prints that command on exit and shows it when a custom host cannot hand off. `dsh --resume <id>` provides the id on the boot context, which `cordis.yml` reads (`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`); with no flag the agent starts a new session. A missing or unreadable id starts no agent and emits `agent-loop/config-start-failed`: the TUI prints the failure and exits nonzero. The selector has no cross-process session lock, so deployments with concurrent hosts must coordinate session ownership separately.
## Code Mode

View File

@@ -10,7 +10,7 @@
config:
provider: deepseek
model: deepseek-v4-pro
resumeSessionId: !!js process.env.RESUME_SESSION_ID
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
persistenceRoot: './.sessions'
resumeCommand: 'dsh --resume {session}'
workspaceContext:

View File

@@ -32,12 +32,14 @@
config:
provider: deepseek
model: deepseek-v4-pro
# Set RESUME_SESSION_ID to continue a prior persisted session (the ids live
# under ./.sessions); unset starts a fresh session each run.
resumeSessionId: !!js process.env.RESUME_SESSION_ID
# `dsh --resume <id>` provides the session id on the boot context (the ids
# live under ./.sessions); with no flag the identifier is undefined and a
# fresh session starts each run. The typeof guard tolerates a launcher that
# never provides the slot, reading undefined rather than throwing.
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
persistenceRoot: './.sessions'
# Printed on exit and listed by `/resume`; `{session}` fills the live id.
# `dsh --resume <id>` sets RESUME_SESSION_ID above, so run it from this cwd.
# `dsh --resume <id>` resumes that session, so run it from this cwd.
resumeCommand: 'dsh --resume {session}'
workspaceContext:
maxBytes: 65536

View File

@@ -29,7 +29,7 @@
# The smoke's log inspection reads plain `.jsonl`; keep the scripted
# fixture uncompressed like the other snapshot-facing configs.
persistenceCompression: none
resumeSessionId: !!js process.env.RESUME_SESSION_ID
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
resumeCommand: 'dsh --resume {session}'
workspaceContext:
maxBytes: 65536

View File

@@ -192,10 +192,12 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
await options.prepare?.(cwd)
const launch = resolveExampleLaunch({
srcBin: options.binScript,
// `configPath` is the dsh `--config <path>` tree override; `configArgs`
// is the raw-args escape (e.g. `['--resume', <id>]`) for other flags.
configArgs: options.configArgs !== undefined
? [...options.configArgs]
/* v8 ignore next -- every caller passes configPath or configArgs; the fallback keeps the type total */
: [options.configPath ?? './cordis.yml'],
: options.configPath !== undefined ? ['--config', options.configPath] : [],
tsconfigPath: options.tsconfigPath,
env: {
DSH_HOME: join(cwd, '.dsh'),

View File

@@ -1,4 +1,5 @@
import { mkdir, readdir, readFile, realpath, writeFile } from 'node:fs/promises'
import { realpathSync } from 'node:fs'
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
@@ -7,7 +8,6 @@ import { SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/d
import { logPath, toHeaderLine } from '../../../packages/session-persistence/session-persistence-jsonl/src/format.ts'
import { runTuiPtySmoke, type TuiPtySmokeOptions } from './pty-harness.ts'
const binScript = fileURLToPath(new URL('../../../packages/examples/tui-demo/src/bin.ts', import.meta.url))
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const codeModeConfigPath = fileURLToPath(new URL('../code-mode.cordis.yml', import.meta.url))
@@ -48,7 +48,7 @@ function seedWorkspace(
/** Seed one real plaintext JSONL session for the `/resume` selector and host handoff smoke. */
async function seedResumeSession(cwd: string): Promise<void> {
const sessionCwd = await realpath(cwd)
const sessionCwd = realpathSync.native(cwd)
const id = SessionId('resume-target')
const meta: SessionHeader = { version: 0, id, createdAt: 1_700_000_000_000, cwd: sessionCwd }
const events: SessionEvent[] = [
@@ -86,11 +86,11 @@ async function readLoggedSystemPrompt(cwd: string): Promise<string> {
throw new Error(`session log ${logRelPath} has no request/header event`)
}
/** Shared defaults: the keyless key, the tui-demo bin, and the live cordis.yml. */
/** Shared defaults: the keyless key, the dsh bin, and the live cordis.yml (passed as the positional config). */
function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Promise<string> {
return runTuiPtySmoke({
tempDirPrefix: 'tui-agent-smoke-',
binScript,
binScript: dshBinScript,
configPath,
tsconfigPath,
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
@@ -245,18 +245,6 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('prints a config-resume failure and exits instead of leaving a blank terminal', async () => {
const output = await smoke({
label: 'tui-agent resume failure',
tempDirPrefix: 'tui-agent-resume-',
env: {
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
RESUME_SESSION_ID: 'missing-session',
},
expectedExitCode: 1,
})
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
@@ -265,7 +253,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
label: 'dsh in-place resume',
tempDirPrefix: 'dsh-in-place-resume-',
binScript: dshBinScript,
configArgs: [scriptedConfigPath],
configPath: scriptedConfigPath,
prepare: seedResumeSession,
actions: [
{ waitFor: 'scripted TUI ready.', send: '/resume\r' },
@@ -340,9 +328,9 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
it('routes the --resume flag into the config resume intake, failing loud on a missing id', async () => {
// The flag path end to end: apps/cli parses `--resume missing-session` and
// sets RESUME_SESSION_ID, the shipped config's `!!js` reads it, and the
// resume fails loud — proving the printed `dsh --resume <id>` hint reaches
// the same intake as the env var.
// provides the id on the boot context, the shipped config's `!!js` reads it
// as a bare identifier, and the resume fails loud — proving the printed
// `dsh --resume <id>` hint reaches the config resume intake with no env var.
const output = await smoke({
label: 'dsh resume flag failure',
tempDirPrefix: 'dsh-resume-flag-',
@@ -362,7 +350,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
label: 'dsh source-path prompt',
tempDirPrefix: 'dsh-source-path-',
binScript: dshBinScript,
configArgs: [scriptedConfigPath],
configPath: scriptedConfigPath,
actions: [
...SELECT_PRO_MODEL,
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },