@openma/deepseek-harness-acp 0.4.25 → 0.4.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -171,6 +171,15 @@ Optional wire behavior follows ACP's extension conventions:
171
171
  extension must still get normal sessions, prompts, updates, cancellation,
172
172
  auth, and config options.
173
173
 
174
+ #### DSH tool result values
175
+
176
+ The [ACP metadata registry][metadata-registry] is the authoritative list of
177
+ every `_meta` field this adapter reads or emits. It records direction, carrier,
178
+ shape, negotiation requirements, and replay behavior, including native DSH
179
+ tool values at `_meta.dsh.toolResult.value`.
180
+
181
+ [metadata-registry]: https://github.com/openma-ai/deepseek-harness-acp/blob/main/docs/metadata.md
182
+
174
183
  The current package applies this pattern to the built-in `_dsh/cordis/*`
175
184
  family used by the TUI for Client capability discovery, dynamic Package
176
185
  lifecycle, and package-private Host/Client RPC. It is an explicit, versioned
@@ -217,7 +226,7 @@ another provider.
217
226
  - **Live model catalog** — providers × models from the running composition (third-party providers added in the Web UI appear immediately), plus reasoning-effort selection that follows your product default.
218
227
  - **Slash commands** — adapter built-ins (`/status`, `/model`) plus the harness command registry (`/compact`, `/goal`, `/permission`, `/plan`, …) executed without a model turn, plus **skills** (`/skill-name` — the harness's own invocation gesture). Login and logout are ACP methods, not chat commands.
219
228
  - **Plans & usage** — `todo_write` snapshots as ACP plans; token accounting as `usage_update` and per-turn usage.
220
- - **Sessions** — `session/load` with full history replay, `session/list`, silent restore when a client prompts an old session after an agent restart, titles as `session_info_update`. Multi-root sessions are not advertised: non-empty `additionalDirectories` on `session/new` or `session/load` return `Invalid params` until [dsh supports multiple workspace roots](https://github.com/deepseek-ai/deepseek-harness/discussions/2474).
229
+ - **Sessions** — `session/resume` restores a durable session without replaying its transcript; `session/load` remains the full-history path. Also supports `session/list`, silent restore when a client prompts an old session after an agent restart, and titles as `session_info_update`. Multi-root sessions are not advertised: non-empty `additionalDirectories` on `session/new`, `session/resume`, or `session/load` return `Invalid params` until [dsh supports multiple workspace roots](https://github.com/deepseek-ai/deepseek-harness/discussions/2474).
221
230
  - **MCP servers** — per-session `mcpServers` mount `@deepseek-ai/dsh-mcp-client` instances (stdio + streamable HTTP); tools join as `mcp__<server>__<tool>`; a failing server never takes the session down.
222
231
  - **Real cancellation** — `session/cancel` interrupts the live turn through the harness agent.
223
232
 
package/dist/bridge.js CHANGED
@@ -419,6 +419,28 @@ function diffsFromToolMeta(meta) {
419
419
  }
420
420
  return result;
421
421
  }
422
+ function acknowledgementId(text, prefix) {
423
+ if (!text.startsWith(prefix)) return void 0;
424
+ const id = text.slice(prefix.length);
425
+ if (id.length === 0 || id.trim() !== id || id.includes(" ") || id.includes(" ") || id.includes("\n")) {
426
+ return void 0;
427
+ }
428
+ return id;
429
+ }
430
+ function typedToolAcknowledgement(state, text, failed) {
431
+ if (state === void 0 || failed) return void 0;
432
+ if (state.name === "bash" || state.name === "pwsh") {
433
+ const jobId = acknowledgementId(text, "started background job ");
434
+ return jobId === void 0 ? void 0 : { kind: "background", jobId };
435
+ }
436
+ if (state.name === "subagent") {
437
+ const jobId = acknowledgementId(text, "started background subagent job ");
438
+ if (jobId !== void 0) return { kind: "background", jobId };
439
+ const subagentId = acknowledgementId(text, "started subagent ");
440
+ if (subagentId !== void 0) return { kind: "continuable", subagentId };
441
+ }
442
+ return void 0;
443
+ }
422
444
  function extractToolResult(data) {
423
445
  let failed = data["error"] !== void 0 && data["error"] !== null;
424
446
  const parts = [];
@@ -532,6 +554,8 @@ var SessionProjection = class {
532
554
  title;
533
555
  streamedText = /* @__PURE__ */ new Set();
534
556
  streamedReasoning = /* @__PURE__ */ new Set();
557
+ /** Structured tool values observed live before DSH omits them from its durable event. */
558
+ toolResultValues = /* @__PURE__ */ new Map();
535
559
  /** One authoritative state per open ACP tool call. Block indexes only
536
560
  * route streaming fragments to that state; they never own lifecycle. */
537
561
  toolCalls = /* @__PURE__ */ new Map();
@@ -551,6 +575,10 @@ var SessionProjection = class {
551
575
  this.presentCall = options?.presentCall;
552
576
  this.subagent = options?.subagent;
553
577
  }
578
+ /** Stage one live `tools/result` value for the matching durable `tool/result` event. */
579
+ recordToolResult(callId, value) {
580
+ this.toolResultValues.set(callId, value);
581
+ }
554
582
  /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
555
583
  beginPrompt() {
556
584
  this.window = emptyWindow();
@@ -941,6 +969,18 @@ var SessionProjection = class {
941
969
  const state = this.toolCalls.get(callId);
942
970
  const facts = state?.facts;
943
971
  const { failed, text, diffs } = extractToolResult(data);
972
+ const hasLiveValue = this.toolResultValues.has(callId);
973
+ const liveValue = this.toolResultValues.get(callId);
974
+ this.toolResultValues.delete(callId);
975
+ const toolResultValue = hasLiveValue ? liveValue : typedToolAcknowledgement(state, text, failed);
976
+ const toolResultMeta = toolResultValue === void 0 ? void 0 : {
977
+ dsh: {
978
+ toolResult: {
979
+ value: toolResultValue
980
+ }
981
+ }
982
+ };
983
+ const rawOutput = text.length > 0 ? { output: text, isError: failed } : void 0;
944
984
  const content = diffs.map((diff) => ({
945
985
  type: "diff",
946
986
  path: diff.path,
@@ -965,8 +1005,9 @@ var SessionProjection = class {
965
1005
  sessionUpdate: "tool_call_update",
966
1006
  toolCallId: callId,
967
1007
  status: failed ? "failed" : "completed",
968
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {},
1008
+ ...rawOutput !== void 0 ? { rawOutput } : {},
969
1009
  _meta: {
1010
+ ...toolResultMeta ?? {},
970
1011
  terminal_exit: { terminal_id: callId, exit_code: failed ? 1 : 0, signal: null }
971
1012
  }
972
1013
  }
@@ -992,7 +1033,8 @@ ${text.replace(/\n+$/, "")}
992
1033
  toolCallId: callId,
993
1034
  status: failed ? "failed" : "completed",
994
1035
  ...content.length > 0 ? { content } : {},
995
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {}
1036
+ ...rawOutput !== void 0 ? { rawOutput } : {},
1037
+ ...toolResultMeta !== void 0 ? { _meta: toolResultMeta } : {}
996
1038
  }
997
1039
  ];
998
1040
  }
@@ -1009,11 +1051,28 @@ ${text.replace(/\n+$/, "")}
1009
1051
  }
1010
1052
  this.toolCallBlocks.clear();
1011
1053
  this.toolCalls.clear();
1054
+ this.toolResultValues.clear();
1012
1055
  return [];
1013
1056
  }
1014
1057
  };
1015
1058
 
1016
1059
  // src/bridge/history.ts
1060
+ function buildResumeMetadata(events) {
1061
+ let title;
1062
+ let contextWindow;
1063
+ for (const event of events) {
1064
+ if (event.type === "session/title") {
1065
+ const value = event.data?.["title"];
1066
+ if (typeof value === "string" && value.length > 0) title = value;
1067
+ continue;
1068
+ }
1069
+ if (event.type === "request/context") {
1070
+ const value = event.data?.["contextWindow"];
1071
+ if (typeof value === "number" && value > 0) contextWindow = value;
1072
+ }
1073
+ }
1074
+ return { title, contextWindow };
1075
+ }
1017
1076
  function userMessage(data) {
1018
1077
  if (data === void 0) return void 0;
1019
1078
  const source = data["source"];
@@ -2257,8 +2316,10 @@ async function apply3(ctx, config = {}) {
2257
2316
  } catch (error) {
2258
2317
  throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
2259
2318
  }
2260
- const replay = buildReplay(events);
2319
+ let restored;
2261
2320
  if (options.replay) {
2321
+ const replay = buildReplay(events);
2322
+ restored = replay;
2262
2323
  for (const update of replay.updates) notify(sessionId, update);
2263
2324
  if (replay.title !== void 0) {
2264
2325
  notify(sessionId, {
@@ -2266,6 +2327,8 @@ async function apply3(ctx, config = {}) {
2266
2327
  title: replay.title
2267
2328
  });
2268
2329
  }
2330
+ } else {
2331
+ restored = buildResumeMetadata(events);
2269
2332
  }
2270
2333
  const presets = presetsService();
2271
2334
  const storedPreset = presetFromLog(storedHeader, events);
@@ -2276,11 +2339,11 @@ async function apply3(ctx, config = {}) {
2276
2339
  ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
2277
2340
  });
2278
2341
  const cwd = options.cwd ?? storedHeader?.cwd;
2279
- const projection = new SessionProjection(replay.contextWindow, {
2342
+ const projection = new SessionProjection(restored.contextWindow, {
2280
2343
  terminalOutput: clientTerminalOutput,
2281
2344
  ...cwd !== void 0 ? { cwd } : {}
2282
2345
  });
2283
- projection.title = replay.title;
2346
+ projection.title = restored.title;
2284
2347
  const record = registerRecord(
2285
2348
  sessionId,
2286
2349
  handle.agent,
@@ -2874,6 +2937,11 @@ async function apply3(ctx, config = {}) {
2874
2937
  watchSubagentParent(agent);
2875
2938
  return record;
2876
2939
  };
2940
+ ctx.on("tools/result", (exec, result) => {
2941
+ if (exec.parent !== void 0 || exec.agent === void 0 || result.isError) return;
2942
+ const projection = ownedRecord(exec.agent)?.projection ?? subagentByChild.get(String(exec.agent.session.id))?.projection;
2943
+ projection?.recordToolResult(String(exec.callId), result.value);
2944
+ });
2877
2945
  ctx.on("session/event", (session, event) => {
2878
2946
  const sessionId = String(session.header.id);
2879
2947
  const record = sessions.get(sessionId);
@@ -2961,7 +3029,7 @@ async function apply3(ctx, config = {}) {
2961
3029
  // mcp-client's second transport. Legacy SSE does not.
2962
3030
  mcpCapabilities: { http: true, sse: false },
2963
3031
  auth: { logout: {} },
2964
- sessionCapabilities: { list: {} }
3032
+ sessionCapabilities: { list: {}, resume: {} }
2965
3033
  },
2966
3034
  authMethods: advertisedAuthMethods(
2967
3035
  providers,
@@ -3076,6 +3144,20 @@ async function apply3(ctx, config = {}) {
3076
3144
  ...options.length > 0 ? { configOptions: options } : {}
3077
3145
  };
3078
3146
  },
3147
+ async resumeSession(params) {
3148
+ assertOpen();
3149
+ await requireCredential(config.provider);
3150
+ validateCwd(params.cwd);
3151
+ validateAdditionalDirectories(params.additionalDirectories);
3152
+ syncMcpServers(params.mcpServers, params.cwd);
3153
+ requirePersistence();
3154
+ const record = await restoreSession(params.sessionId, { replay: false, cwd: params.cwd });
3155
+ const options = await configOptions(record);
3156
+ return {
3157
+ modes: modeState(record),
3158
+ ...options.length > 0 ? { configOptions: options } : {}
3159
+ };
3160
+ },
3079
3161
  async listSessions(params) {
3080
3162
  assertOpen();
3081
3163
  const persistence = requirePersistence();
package/dist/index.js CHANGED
@@ -295,11 +295,8 @@ async function bootAcpProfile(host, overrides) {
295
295
  const appBoot = await import(pathToFileURL(req.resolve("@deepseek-ai/dsh-app-boot")).href);
296
296
  const homePaths = await import(pathToFileURL(req.resolve("@deepseek-ai/dsh-home-paths")).href);
297
297
  const home = homePaths.resolveDshHome();
298
- let bundlePatches;
299
- let profilePatches = [];
300
- let profilePatchPath;
301
- let rootConfigPath = join3(ownRoot(), "profile-root.cordis.yml");
302
- let bareModuleBaseUrl = pathToFileURL(dshDir + sep2).href;
298
+ const rootConfigPath = join3(ownRoot(), "profile-root.cordis.yml");
299
+ const bareModuleBaseUrl = pathToFileURL(dshDir + sep2).href;
303
300
  const distBridge = join3(ownRoot(), "dist", "bridge.js");
304
301
  const bridgeEntry = import.meta.url.endsWith(".ts") || !existsSync3(distBridge) ? join3(ownRoot(), "src", "bundle.ts") : distBridge;
305
302
  const distServer = join3(ownRoot(), "dist", "server.js");
@@ -308,26 +305,13 @@ async function bootAcpProfile(host, overrides) {
308
305
  const pluginEntry = import.meta.url.endsWith(".ts") || !existsSync3(distPlugin) ? join3(ownRoot(), "src", "plugin.ts") : distPlugin;
309
306
  const distStdio = join3(ownRoot(), "dist", "stdio.js");
310
307
  const stdioEntry = import.meta.url.endsWith(".ts") || !existsSync3(distStdio) ? join3(ownRoot(), "src", "stdio.ts") : distStdio;
311
- const realProfileDir = join3(home, "profiles", "acp");
312
- if (existsSync3(join3(realProfileDir, "package.json"))) {
313
- appBoot.healProfilesModuleFallback(dshPkgPath);
314
- const profile = appBoot.loadProfile(BIN, "acp", dshPkgPath);
315
- bundlePatches = profile.layers.flatMap((layer) => layer.patches);
316
- profilePatches = profile.patches;
317
- profilePatchPath = join3(profile.dir, "cordis.patch.yml");
318
- const realRoot = join3(profile.dir, "cordis.yml");
319
- if (existsSync3(realRoot)) rootConfigPath = realRoot;
320
- bareModuleBaseUrl = void 0;
321
- logDebug(`profile: real (${profile.dir})`);
322
- } else {
323
- const basePatch = bundlePatchPath(req.resolve("@deepseek-ai/dsh-base/package.json"));
324
- const ownPatch = bundlePatchPath(join3(ownRoot(), "package.json"));
325
- bundlePatches = [
326
- ...appBoot.loadOverlayPatches(BIN, basePatch),
327
- ...appBoot.loadOverlayPatches(BIN, ownPatch)
328
- ];
329
- logDebug(`profile: virtual (dsh-base + ${ownRoot()})`);
330
- }
308
+ const basePatch = bundlePatchPath(req.resolve("@deepseek-ai/dsh-base/package.json"));
309
+ const ownPatch = bundlePatchPath(join3(ownRoot(), "package.json"));
310
+ const bundlePatches = [
311
+ ...appBoot.loadOverlayPatches(BIN, basePatch),
312
+ ...appBoot.loadOverlayPatches(BIN, ownPatch)
313
+ ];
314
+ logDebug(`profile: standalone (dsh-base + ${ownRoot()})`);
331
315
  for (const bundle of overrides?.bundles ?? []) {
332
316
  bundlePatches.push(...loadBundleLayer(appBoot, bundle));
333
317
  logDebug(`profile: added bundle ${bundle}`);
@@ -343,7 +327,7 @@ async function bootAcpProfile(host, overrides) {
343
327
  const homePatchPath = join3(home, "cordis.patch.yml");
344
328
  const homePatches = appBoot.loadOptionalPatches(BIN, homePatchPath) ?? [];
345
329
  const rows = /* @__PURE__ */ new Map();
346
- for (const row of appBoot.composeEntries([bundlePatches, profilePatches, homePatches])) {
330
+ for (const row of appBoot.composeEntries([bundlePatches, homePatches])) {
347
331
  if (typeof row.id === "string") rows.set(row.id, row);
348
332
  }
349
333
  const overlays = [];
@@ -376,7 +360,7 @@ async function bootAcpProfile(host, overrides) {
376
360
  overlays.push({ id: configRowId, config: { ...configRow.config ?? {}, ...patch } });
377
361
  }
378
362
  }
379
- const allPatches = [...bundlePatches, ...profilePatches, ...homePatches, ...overlays];
363
+ const allPatches = [...bundlePatches, ...homePatches, ...overlays];
380
364
  const launchEnvironment = appBoot.loadLayeredEnv("dsh");
381
365
  const launchEnvKey = (await import(pathToFileURL(req.resolve("@deepseek-ai/dsh-launch-environment")).href)).DSH_LAUNCH_ENVIRONMENT_KEY;
382
366
  const ctx = await appBoot.boot(
@@ -396,9 +380,6 @@ async function bootAcpProfile(host, overrides) {
396
380
  });
397
381
  throw error;
398
382
  }
399
- if (profilePatchPath !== void 0) {
400
- logDebug(`user patch layers are read at boot; edit ${profilePatchPath} and restart to apply`);
401
- }
402
383
  return ctx;
403
384
  }
404
385
 
package/dist/plugin.js CHANGED
@@ -421,6 +421,28 @@ function diffsFromToolMeta(meta) {
421
421
  }
422
422
  return result;
423
423
  }
424
+ function acknowledgementId(text, prefix) {
425
+ if (!text.startsWith(prefix)) return void 0;
426
+ const id = text.slice(prefix.length);
427
+ if (id.length === 0 || id.trim() !== id || id.includes(" ") || id.includes(" ") || id.includes("\n")) {
428
+ return void 0;
429
+ }
430
+ return id;
431
+ }
432
+ function typedToolAcknowledgement(state, text, failed) {
433
+ if (state === void 0 || failed) return void 0;
434
+ if (state.name === "bash" || state.name === "pwsh") {
435
+ const jobId = acknowledgementId(text, "started background job ");
436
+ return jobId === void 0 ? void 0 : { kind: "background", jobId };
437
+ }
438
+ if (state.name === "subagent") {
439
+ const jobId = acknowledgementId(text, "started background subagent job ");
440
+ if (jobId !== void 0) return { kind: "background", jobId };
441
+ const subagentId = acknowledgementId(text, "started subagent ");
442
+ if (subagentId !== void 0) return { kind: "continuable", subagentId };
443
+ }
444
+ return void 0;
445
+ }
424
446
  function extractToolResult(data) {
425
447
  let failed = data["error"] !== void 0 && data["error"] !== null;
426
448
  const parts = [];
@@ -534,6 +556,8 @@ var SessionProjection = class {
534
556
  title;
535
557
  streamedText = /* @__PURE__ */ new Set();
536
558
  streamedReasoning = /* @__PURE__ */ new Set();
559
+ /** Structured tool values observed live before DSH omits them from its durable event. */
560
+ toolResultValues = /* @__PURE__ */ new Map();
537
561
  /** One authoritative state per open ACP tool call. Block indexes only
538
562
  * route streaming fragments to that state; they never own lifecycle. */
539
563
  toolCalls = /* @__PURE__ */ new Map();
@@ -553,6 +577,10 @@ var SessionProjection = class {
553
577
  this.presentCall = options?.presentCall;
554
578
  this.subagent = options?.subagent;
555
579
  }
580
+ /** Stage one live `tools/result` value for the matching durable `tool/result` event. */
581
+ recordToolResult(callId, value) {
582
+ this.toolResultValues.set(callId, value);
583
+ }
556
584
  /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
557
585
  beginPrompt() {
558
586
  this.window = emptyWindow();
@@ -943,6 +971,18 @@ var SessionProjection = class {
943
971
  const state = this.toolCalls.get(callId);
944
972
  const facts = state?.facts;
945
973
  const { failed, text, diffs } = extractToolResult(data);
974
+ const hasLiveValue = this.toolResultValues.has(callId);
975
+ const liveValue = this.toolResultValues.get(callId);
976
+ this.toolResultValues.delete(callId);
977
+ const toolResultValue = hasLiveValue ? liveValue : typedToolAcknowledgement(state, text, failed);
978
+ const toolResultMeta = toolResultValue === void 0 ? void 0 : {
979
+ dsh: {
980
+ toolResult: {
981
+ value: toolResultValue
982
+ }
983
+ }
984
+ };
985
+ const rawOutput = text.length > 0 ? { output: text, isError: failed } : void 0;
946
986
  const content = diffs.map((diff) => ({
947
987
  type: "diff",
948
988
  path: diff.path,
@@ -967,8 +1007,9 @@ var SessionProjection = class {
967
1007
  sessionUpdate: "tool_call_update",
968
1008
  toolCallId: callId,
969
1009
  status: failed ? "failed" : "completed",
970
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {},
1010
+ ...rawOutput !== void 0 ? { rawOutput } : {},
971
1011
  _meta: {
1012
+ ...toolResultMeta ?? {},
972
1013
  terminal_exit: { terminal_id: callId, exit_code: failed ? 1 : 0, signal: null }
973
1014
  }
974
1015
  }
@@ -994,7 +1035,8 @@ ${text.replace(/\n+$/, "")}
994
1035
  toolCallId: callId,
995
1036
  status: failed ? "failed" : "completed",
996
1037
  ...content.length > 0 ? { content } : {},
997
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {}
1038
+ ...rawOutput !== void 0 ? { rawOutput } : {},
1039
+ ...toolResultMeta !== void 0 ? { _meta: toolResultMeta } : {}
998
1040
  }
999
1041
  ];
1000
1042
  }
@@ -1011,11 +1053,28 @@ ${text.replace(/\n+$/, "")}
1011
1053
  }
1012
1054
  this.toolCallBlocks.clear();
1013
1055
  this.toolCalls.clear();
1056
+ this.toolResultValues.clear();
1014
1057
  return [];
1015
1058
  }
1016
1059
  };
1017
1060
 
1018
1061
  // src/bridge/history.ts
1062
+ function buildResumeMetadata(events) {
1063
+ let title;
1064
+ let contextWindow;
1065
+ for (const event of events) {
1066
+ if (event.type === "session/title") {
1067
+ const value = event.data?.["title"];
1068
+ if (typeof value === "string" && value.length > 0) title = value;
1069
+ continue;
1070
+ }
1071
+ if (event.type === "request/context") {
1072
+ const value = event.data?.["contextWindow"];
1073
+ if (typeof value === "number" && value > 0) contextWindow = value;
1074
+ }
1075
+ }
1076
+ return { title, contextWindow };
1077
+ }
1019
1078
  function userMessage(data) {
1020
1079
  if (data === void 0) return void 0;
1021
1080
  const source = data["source"];
@@ -2259,8 +2318,10 @@ async function apply3(ctx, config = {}) {
2259
2318
  } catch (error) {
2260
2319
  throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
2261
2320
  }
2262
- const replay = buildReplay(events);
2321
+ let restored;
2263
2322
  if (options.replay) {
2323
+ const replay = buildReplay(events);
2324
+ restored = replay;
2264
2325
  for (const update of replay.updates) notify(sessionId, update);
2265
2326
  if (replay.title !== void 0) {
2266
2327
  notify(sessionId, {
@@ -2268,6 +2329,8 @@ async function apply3(ctx, config = {}) {
2268
2329
  title: replay.title
2269
2330
  });
2270
2331
  }
2332
+ } else {
2333
+ restored = buildResumeMetadata(events);
2271
2334
  }
2272
2335
  const presets = presetsService();
2273
2336
  const storedPreset = presetFromLog(storedHeader, events);
@@ -2278,11 +2341,11 @@ async function apply3(ctx, config = {}) {
2278
2341
  ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
2279
2342
  });
2280
2343
  const cwd = options.cwd ?? storedHeader?.cwd;
2281
- const projection = new SessionProjection(replay.contextWindow, {
2344
+ const projection = new SessionProjection(restored.contextWindow, {
2282
2345
  terminalOutput: clientTerminalOutput,
2283
2346
  ...cwd !== void 0 ? { cwd } : {}
2284
2347
  });
2285
- projection.title = replay.title;
2348
+ projection.title = restored.title;
2286
2349
  const record = registerRecord(
2287
2350
  sessionId,
2288
2351
  handle.agent,
@@ -2876,6 +2939,11 @@ async function apply3(ctx, config = {}) {
2876
2939
  watchSubagentParent(agent);
2877
2940
  return record;
2878
2941
  };
2942
+ ctx.on("tools/result", (exec, result) => {
2943
+ if (exec.parent !== void 0 || exec.agent === void 0 || result.isError) return;
2944
+ const projection = ownedRecord(exec.agent)?.projection ?? subagentByChild.get(String(exec.agent.session.id))?.projection;
2945
+ projection?.recordToolResult(String(exec.callId), result.value);
2946
+ });
2879
2947
  ctx.on("session/event", (session, event) => {
2880
2948
  const sessionId = String(session.header.id);
2881
2949
  const record = sessions.get(sessionId);
@@ -2963,7 +3031,7 @@ async function apply3(ctx, config = {}) {
2963
3031
  // mcp-client's second transport. Legacy SSE does not.
2964
3032
  mcpCapabilities: { http: true, sse: false },
2965
3033
  auth: { logout: {} },
2966
- sessionCapabilities: { list: {} }
3034
+ sessionCapabilities: { list: {}, resume: {} }
2967
3035
  },
2968
3036
  authMethods: advertisedAuthMethods(
2969
3037
  providers,
@@ -3078,6 +3146,20 @@ async function apply3(ctx, config = {}) {
3078
3146
  ...options.length > 0 ? { configOptions: options } : {}
3079
3147
  };
3080
3148
  },
3149
+ async resumeSession(params) {
3150
+ assertOpen();
3151
+ await requireCredential(config.provider);
3152
+ validateCwd(params.cwd);
3153
+ validateAdditionalDirectories(params.additionalDirectories);
3154
+ syncMcpServers(params.mcpServers, params.cwd);
3155
+ requirePersistence();
3156
+ const record = await restoreSession(params.sessionId, { replay: false, cwd: params.cwd });
3157
+ const options = await configOptions(record);
3158
+ return {
3159
+ modes: modeState(record),
3160
+ ...options.length > 0 ? { configOptions: options } : {}
3161
+ };
3162
+ },
3081
3163
  async listSessions(params) {
3082
3164
  assertOpen();
3083
3165
  const persistence = requirePersistence();
package/dist/server.js CHANGED
@@ -415,6 +415,28 @@ function diffsFromToolMeta(meta) {
415
415
  }
416
416
  return result;
417
417
  }
418
+ function acknowledgementId(text, prefix) {
419
+ if (!text.startsWith(prefix)) return void 0;
420
+ const id = text.slice(prefix.length);
421
+ if (id.length === 0 || id.trim() !== id || id.includes(" ") || id.includes(" ") || id.includes("\n")) {
422
+ return void 0;
423
+ }
424
+ return id;
425
+ }
426
+ function typedToolAcknowledgement(state, text, failed) {
427
+ if (state === void 0 || failed) return void 0;
428
+ if (state.name === "bash" || state.name === "pwsh") {
429
+ const jobId = acknowledgementId(text, "started background job ");
430
+ return jobId === void 0 ? void 0 : { kind: "background", jobId };
431
+ }
432
+ if (state.name === "subagent") {
433
+ const jobId = acknowledgementId(text, "started background subagent job ");
434
+ if (jobId !== void 0) return { kind: "background", jobId };
435
+ const subagentId = acknowledgementId(text, "started subagent ");
436
+ if (subagentId !== void 0) return { kind: "continuable", subagentId };
437
+ }
438
+ return void 0;
439
+ }
418
440
  function extractToolResult(data) {
419
441
  let failed = data["error"] !== void 0 && data["error"] !== null;
420
442
  const parts = [];
@@ -528,6 +550,8 @@ var SessionProjection = class {
528
550
  title;
529
551
  streamedText = /* @__PURE__ */ new Set();
530
552
  streamedReasoning = /* @__PURE__ */ new Set();
553
+ /** Structured tool values observed live before DSH omits them from its durable event. */
554
+ toolResultValues = /* @__PURE__ */ new Map();
531
555
  /** One authoritative state per open ACP tool call. Block indexes only
532
556
  * route streaming fragments to that state; they never own lifecycle. */
533
557
  toolCalls = /* @__PURE__ */ new Map();
@@ -547,6 +571,10 @@ var SessionProjection = class {
547
571
  this.presentCall = options?.presentCall;
548
572
  this.subagent = options?.subagent;
549
573
  }
574
+ /** Stage one live `tools/result` value for the matching durable `tool/result` event. */
575
+ recordToolResult(callId, value) {
576
+ this.toolResultValues.set(callId, value);
577
+ }
550
578
  /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
551
579
  beginPrompt() {
552
580
  this.window = emptyWindow();
@@ -937,6 +965,18 @@ var SessionProjection = class {
937
965
  const state = this.toolCalls.get(callId);
938
966
  const facts = state?.facts;
939
967
  const { failed, text, diffs } = extractToolResult(data);
968
+ const hasLiveValue = this.toolResultValues.has(callId);
969
+ const liveValue = this.toolResultValues.get(callId);
970
+ this.toolResultValues.delete(callId);
971
+ const toolResultValue = hasLiveValue ? liveValue : typedToolAcknowledgement(state, text, failed);
972
+ const toolResultMeta = toolResultValue === void 0 ? void 0 : {
973
+ dsh: {
974
+ toolResult: {
975
+ value: toolResultValue
976
+ }
977
+ }
978
+ };
979
+ const rawOutput = text.length > 0 ? { output: text, isError: failed } : void 0;
940
980
  const content = diffs.map((diff) => ({
941
981
  type: "diff",
942
982
  path: diff.path,
@@ -961,8 +1001,9 @@ var SessionProjection = class {
961
1001
  sessionUpdate: "tool_call_update",
962
1002
  toolCallId: callId,
963
1003
  status: failed ? "failed" : "completed",
964
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {},
1004
+ ...rawOutput !== void 0 ? { rawOutput } : {},
965
1005
  _meta: {
1006
+ ...toolResultMeta ?? {},
966
1007
  terminal_exit: { terminal_id: callId, exit_code: failed ? 1 : 0, signal: null }
967
1008
  }
968
1009
  }
@@ -988,7 +1029,8 @@ ${text.replace(/\n+$/, "")}
988
1029
  toolCallId: callId,
989
1030
  status: failed ? "failed" : "completed",
990
1031
  ...content.length > 0 ? { content } : {},
991
- ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {}
1032
+ ...rawOutput !== void 0 ? { rawOutput } : {},
1033
+ ...toolResultMeta !== void 0 ? { _meta: toolResultMeta } : {}
992
1034
  }
993
1035
  ];
994
1036
  }
@@ -1005,11 +1047,28 @@ ${text.replace(/\n+$/, "")}
1005
1047
  }
1006
1048
  this.toolCallBlocks.clear();
1007
1049
  this.toolCalls.clear();
1050
+ this.toolResultValues.clear();
1008
1051
  return [];
1009
1052
  }
1010
1053
  };
1011
1054
 
1012
1055
  // src/bridge/history.ts
1056
+ function buildResumeMetadata(events) {
1057
+ let title;
1058
+ let contextWindow;
1059
+ for (const event of events) {
1060
+ if (event.type === "session/title") {
1061
+ const value = event.data?.["title"];
1062
+ if (typeof value === "string" && value.length > 0) title = value;
1063
+ continue;
1064
+ }
1065
+ if (event.type === "request/context") {
1066
+ const value = event.data?.["contextWindow"];
1067
+ if (typeof value === "number" && value > 0) contextWindow = value;
1068
+ }
1069
+ }
1070
+ return { title, contextWindow };
1071
+ }
1013
1072
  function userMessage(data) {
1014
1073
  if (data === void 0) return void 0;
1015
1074
  const source = data["source"];
@@ -2253,8 +2312,10 @@ async function apply3(ctx, config = {}) {
2253
2312
  } catch (error) {
2254
2313
  throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
2255
2314
  }
2256
- const replay = buildReplay(events);
2315
+ let restored;
2257
2316
  if (options.replay) {
2317
+ const replay = buildReplay(events);
2318
+ restored = replay;
2258
2319
  for (const update of replay.updates) notify(sessionId, update);
2259
2320
  if (replay.title !== void 0) {
2260
2321
  notify(sessionId, {
@@ -2262,6 +2323,8 @@ async function apply3(ctx, config = {}) {
2262
2323
  title: replay.title
2263
2324
  });
2264
2325
  }
2326
+ } else {
2327
+ restored = buildResumeMetadata(events);
2265
2328
  }
2266
2329
  const presets = presetsService();
2267
2330
  const storedPreset = presetFromLog(storedHeader, events);
@@ -2272,11 +2335,11 @@ async function apply3(ctx, config = {}) {
2272
2335
  ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
2273
2336
  });
2274
2337
  const cwd = options.cwd ?? storedHeader?.cwd;
2275
- const projection = new SessionProjection(replay.contextWindow, {
2338
+ const projection = new SessionProjection(restored.contextWindow, {
2276
2339
  terminalOutput: clientTerminalOutput,
2277
2340
  ...cwd !== void 0 ? { cwd } : {}
2278
2341
  });
2279
- projection.title = replay.title;
2342
+ projection.title = restored.title;
2280
2343
  const record = registerRecord(
2281
2344
  sessionId,
2282
2345
  handle.agent,
@@ -2870,6 +2933,11 @@ async function apply3(ctx, config = {}) {
2870
2933
  watchSubagentParent(agent);
2871
2934
  return record;
2872
2935
  };
2936
+ ctx.on("tools/result", (exec, result) => {
2937
+ if (exec.parent !== void 0 || exec.agent === void 0 || result.isError) return;
2938
+ const projection = ownedRecord(exec.agent)?.projection ?? subagentByChild.get(String(exec.agent.session.id))?.projection;
2939
+ projection?.recordToolResult(String(exec.callId), result.value);
2940
+ });
2873
2941
  ctx.on("session/event", (session, event) => {
2874
2942
  const sessionId = String(session.header.id);
2875
2943
  const record = sessions.get(sessionId);
@@ -2957,7 +3025,7 @@ async function apply3(ctx, config = {}) {
2957
3025
  // mcp-client's second transport. Legacy SSE does not.
2958
3026
  mcpCapabilities: { http: true, sse: false },
2959
3027
  auth: { logout: {} },
2960
- sessionCapabilities: { list: {} }
3028
+ sessionCapabilities: { list: {}, resume: {} }
2961
3029
  },
2962
3030
  authMethods: advertisedAuthMethods(
2963
3031
  providers,
@@ -3072,6 +3140,20 @@ async function apply3(ctx, config = {}) {
3072
3140
  ...options.length > 0 ? { configOptions: options } : {}
3073
3141
  };
3074
3142
  },
3143
+ async resumeSession(params) {
3144
+ assertOpen();
3145
+ await requireCredential(config.provider);
3146
+ validateCwd(params.cwd);
3147
+ validateAdditionalDirectories(params.additionalDirectories);
3148
+ syncMcpServers(params.mcpServers, params.cwd);
3149
+ requirePersistence();
3150
+ const record = await restoreSession(params.sessionId, { replay: false, cwd: params.cwd });
3151
+ const options = await configOptions(record);
3152
+ return {
3153
+ modes: modeState(record),
3154
+ ...options.length > 0 ? { configOptions: options } : {}
3155
+ };
3156
+ },
3075
3157
  async listSessions(params) {
3076
3158
  assertOpen();
3077
3159
  const persistence = requirePersistence();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.25",
6
+ "version": "0.4.27",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/openma-ai/deepseek-harness-acp.git"
Binary file
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "archive": "dsh-runtime.tgz",
3
3
  "dsh": "0.1.1-rc.2",
4
- "sha256": "4a2d5d44fca9ef7c5afcd986309f4a6f5b4605a8d45f1197645297e3f7f5ae85"
4
+ "sha256": "a144a57cdb63bd5bc0152cc392534fd14fdf947922e68cda16d8f1d0243356e7"
5
5
  }