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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user