@askdkc/dsh-cli 0.13.0 → 0.13.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"session-fork.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/channel/session-fork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,wBAAwB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAa,KAAK,OAAO,EAAqB,MAAM,0BAA0B,CAAA;AASrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,CAAC,CAAA;AAE9F,8EAA8E;AAC9E,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE;IACJ,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;IACpC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC9B,MAAM,IAAI,OAAO,CAAA;IACjB,oBAAoB,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC,CAAA;CACrH,SAEgB,OAAO,CAAC,OAAO,CAAC,CAoFlC"}
1
+ {"version":3,"file":"session-fork.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/channel/session-fork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,wBAAwB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAa,KAAK,OAAO,EAAqB,MAAM,0BAA0B,CAAA;AAUrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,CAAC,CAAA;AAE9F,8EAA8E;AAC9E,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE;IACJ,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;IACpC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC9B,MAAM,IAAI,OAAO,CAAA;IACjB,oBAAoB,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC,CAAA;CACrH,SAEgB,OAAO,CAAC,OAAO,CAAC,CAiFlC"}
@@ -2,6 +2,7 @@ import { SessionId } from '@deepseek-ai/dsh-session';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { t } from '../../i18n.js';
4
4
  import { resolveDshProfileName } from '../../update.js';
5
+ import { resumeCommand } from '../../sessionHistory.js';
5
6
  import { appendSessionTitle, liveSessionCreateOptions, sliceLiveSessionSeed } from '../compat/index.js';
6
7
  import { composePreset, runningPresetOf } from '../presets.js';
7
8
  import { attachSessionToWorkspace } from '../workspace.js';
@@ -98,10 +99,7 @@ export function createForkSessionAction(ctx, state, deps) {
98
99
  reservation.abandon();
99
100
  }
100
101
  const profile = resolveDshProfileName();
101
- const boot = profile === undefined ? 'dsh --config cordis.yml' : `dsh --profile ${profile}`;
102
- const command = process.platform === 'win32'
103
- ? `dsh-tui --resume ${childId}`
104
- : `DSH_TUI_RESUME_SESSION=${childId} ${boot}`;
102
+ const command = resumeCommand(profile, String(childId));
105
103
  deps.notify(t('fork-done', { id: String(childId), command }), { timeoutMs: 8000 });
106
104
  return true;
107
105
  };
@@ -27,7 +27,7 @@ import { readEffortPref } from '../effortPrefs.js';
27
27
  import { composePreset, filterMinimalPresetTools, resolvePersistedPreset, resolvePersistedRoute, runningPresetOf } from './presets.js';
28
28
  import { registerBundledPresets } from './bundled-presets.js';
29
29
  import { registerTuiSessionEventTypes, snapshotLiveSessionEvents } from './compat/index.js';
30
- import { clearResumeTarget, resumeTargetFromArgv, writeResumeTarget } from '../sessionHistory.js';
30
+ import { clearResumeTarget, resumeCommand, resumeTargetFromArgv, writeResumeTarget } from '../sessionHistory.js';
31
31
  import { readHomePrefs } from '../homePrefs.js';
32
32
  import { resolveSessionCwd } from '../utils/workspaceRoot.js';
33
33
  import { beginRestartAttempt, checkForTuiUpdate, installedTuiVersion, isBootDeadlockTarget, isStandaloneRuntime, isVersionNewer, logRestartEvent, resolveDshProfileName, resolveTuiUpdateTarget, restartTui, updateTuiAndRestart, writeHandoffNotice } from '../update.js';
@@ -1975,20 +1975,6 @@ export function handleStartupError(ctx, error) {
1975
1975
  function disposeRootAndExit(ctx, code) {
1976
1976
  disposeRootAndThen(ctx, () => process.exit(code), code);
1977
1977
  }
1978
- /**
1979
- * The real way back into a session after the TUI process is gone. The
1980
- * package ships no `dsh-tui` bin — resuming means feeding the session id
1981
- * through `DSH_TUI_RESUME_SESSION` (what cordis.patch.yml's `sessionId`
1982
- * reads) and
1983
- * booting the same profile; on Windows the repo's dsh-tui.cmd wrapper
1984
- * does this via --resume + ~/.dsh-tui/resume.txt.
1985
- */
1986
- function resumeCommand(profile, sessionId) {
1987
- const boot = profile === undefined ? 'dsh --config cordis.yml' : `dsh --profile ${profile}`;
1988
- return process.platform === 'win32'
1989
- ? `dsh-tui --resume ${sessionId}`
1990
- : `DSH_TUI_RESUME_SESSION=${sessionId} ${boot}`;
1991
- }
1992
1978
  /**
1993
1979
  * Dispose the Cordis tree, then run a process-level handoff action. The
1994
1980
  * fallback exit keeps the caller's intended code when disposal stalls — the
@@ -1,3 +1,5 @@
1
+ /** Render exit and fork guidance using the installed CLI for its own profile. */
2
+ export declare function resumeCommand(profile: string | undefined, sessionId: string, platform?: NodeJS.Platform): string;
1
3
  /**
2
4
  * Store the session to resume and report the launcher invocation.
3
5
  * @param sessionId - Session id for `dsh-tui --resume` on the next launch.
@@ -1 +1 @@
1
- {"version":3,"file":"sessionHistory.d.ts","sourceRoot":"","sources":["../../src/sessionHistory.ts"],"names":[],"mappings":"AAyCA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGzD;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAehF;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyB/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAUpD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBxE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAQ7D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAU9D"}
1
+ {"version":3,"file":"sessionHistory.d.ts","sourceRoot":"","sources":["../../src/sessionHistory.ts"],"names":[],"mappings":"AAoBA,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,kBAAmB,GAAG,MAAM,CAMjH;AAuBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGzD;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAehF;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyB/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAUpD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBxE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAQ7D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAU9D"}
@@ -16,6 +16,14 @@ const RESUME_FILE = join(DIR, 'resume.txt');
16
16
  const LAST_USED_FILE = join(DIR, 'last-used.json');
17
17
  let lastUsedStamp;
18
18
  let lastUsedCache;
19
+ /** Render exit and fork guidance using the installed CLI for its own profile. */
20
+ export function resumeCommand(profile, sessionId, platform = process.platform) {
21
+ const boot = profile === 'dsh-cli' ? 'dsh-cli'
22
+ : profile === undefined ? 'dsh --config cordis.yml' : `dsh --profile ${profile}`;
23
+ return platform === 'win32'
24
+ ? `${boot} --resume ${sessionId}`
25
+ : `DSH_TUI_RESUME_SESSION=${sessionId} ${boot}`;
26
+ }
19
27
  function lastUsedFileStamp() {
20
28
  try {
21
29
  const stats = statSync(LAST_USED_FILE);
@@ -158,13 +158,22 @@ export class OpenCodeAdapter extends LlmAdapter {
158
158
  }
159
159
  const input = part.usage.inputTokens;
160
160
  const output = part.usage.outputTokens;
161
- yield { type: 'usage', usage: { inputTokens: input.noCache ?? Math.max(0, (input.total ?? 0) - (input.cacheRead ?? 0) - (input.cacheWrite ?? 0)), outputTokens: output.total ?? 0, cacheReadTokens: input.cacheRead, cacheWriteTokens: input.cacheWrite, reasoningTokens: output.reasoning, ...(input.total !== undefined && output.total !== undefined ? { totalTokens: input.total + output.total } : {}) } };
161
+ // DSH snapshots chunks losslessly: optional SDK values must be absent,
162
+ // not own properties containing undefined. Preserve reported zeroes.
163
+ yield { type: 'usage', usage: {
164
+ inputTokens: input.noCache ?? Math.max(0, (input.total ?? 0) - (input.cacheRead ?? 0) - (input.cacheWrite ?? 0)),
165
+ outputTokens: output.total ?? 0,
166
+ ...(input.cacheRead !== undefined ? { cacheReadTokens: input.cacheRead } : {}),
167
+ ...(input.cacheWrite !== undefined ? { cacheWriteTokens: input.cacheWrite } : {}),
168
+ ...(output.reasoning !== undefined ? { reasoningTokens: output.reasoning } : {}),
169
+ ...(input.total !== undefined && output.total !== undefined ? { totalTokens: input.total + output.total } : {}),
170
+ } };
162
171
  if (!blocks.size)
163
172
  throw new LlmError('OpenCode returned no content', 'EMPTY_RESPONSE');
164
173
  const finish = part.finishReason.unified;
165
174
  if (finish === 'error' || finish === 'content-filter' || finish === 'other')
166
175
  throw new LlmError(`OpenCode generation ended: ${finish}`, 'PROVIDER_ERROR');
167
- yield { type: 'finish', reason: { kind: finish === 'tool-calls' ? 'tool-calls' : finish === 'length' ? 'max-tokens' : 'stop' }, replayState: { response: { kind: 'dsh-opencode', version: 1, protocol: descriptor.protocol, provider: options.provider, model: descriptor.id, responseId }, blocks: [...blocks.values()].map(entry => ({ type: entry.block.type, ...(entry.providerOptions ? { providerOptions: entry.providerOptions } : {}) })) } };
176
+ yield { type: 'finish', reason: { kind: finish === 'tool-calls' ? 'tool-calls' : finish === 'length' ? 'max-tokens' : 'stop' }, replayState: { response: { kind: 'dsh-opencode', version: 1, protocol: descriptor.protocol, provider: options.provider, model: descriptor.id, ...(responseId !== undefined ? { responseId } : {}) }, blocks: [...blocks.values()].map(entry => ({ type: entry.block.type, ...(entry.providerOptions ? { providerOptions: entry.providerOptions } : {}) })) } };
168
177
  return;
169
178
  }
170
179
  }
@@ -1,10 +1,12 @@
1
1
  /** Convert DSH conversation blocks into low-level SDK prompts without an agent loop. */
2
2
  import { LlmError, projectToolUpdates, fileHandleText } from '@deepseek-ai/dsh-llm';
3
3
  import { projectReplay, requireReplaySignature } from './opencode-replay.js';
4
+ import { createHash } from 'node:crypto';
4
5
  export async function encodePrompt(options, model, attachments, warn) {
5
6
  const prompt = [];
6
7
  const names = new Map();
7
8
  const callIds = new Map();
9
+ const wireIds = new Set();
8
10
  if (options.system)
9
11
  prompt.push({ role: 'system', content: options.system });
10
12
  const projection = projectToolUpdates(options.messages, options.tools, undefined, options.toolHistory);
@@ -61,14 +63,24 @@ export async function encodePrompt(options, model, attachments, warn) {
61
63
  else if (block.type === 'file')
62
64
  content.push({ type: 'text', text: fileHandleText(block.attachment, attachments?.()?.fileHostPath(block.attachment)) });
63
65
  else if (block.type === 'reasoning' && message.role === 'assistant') {
66
+ if (replay.foreign)
67
+ continue;
64
68
  requireReplaySignature(block, model.protocol, providerOptions);
65
69
  content.push({ type: 'reasoning', text: block.text, providerOptions });
66
70
  }
67
71
  else if (block.type === 'tool-call' && message.role === 'assistant') {
68
- if (model.protocol === 'responses' && block.id.includes('|') && !replay.callIds.has(block.id))
72
+ // Foreign pi Responses ids contain an opaque item suffix. Give the
73
+ // call/result pair a portable id without restoring that foreign item.
74
+ if (replay.foreign && model.protocol === 'responses' && block.id.includes('|'))
75
+ callIds.set(block.id, `dsh_${createHash('sha256').update(block.id).digest('hex').slice(0, 48)}`);
76
+ if (model.protocol === 'responses' && block.id.includes('|') && !callIds.has(block.id))
69
77
  throw new LlmError('Cannot restore legacy Responses tool call pairing', 'INVALID_REPLAY_STATE');
70
78
  if (names.has(block.id))
71
79
  throw new LlmError('Duplicate stored tool call id', 'INVALID_REPLAY_STATE');
80
+ const wireId = callIds.get(block.id) ?? block.id;
81
+ if (wireIds.has(wireId))
82
+ throw new LlmError('Duplicate wire tool call id', 'INVALID_REPLAY_STATE');
83
+ wireIds.add(wireId);
72
84
  if (model.protocol === 'google' && model.reasoning && !providerOptions?.google?.thoughtSignature && !message.content.slice(0, index).some(part => part.type === 'tool-call'))
73
85
  throw new LlmError('Cannot replay Google tool call without its verified thought signature', 'INVALID_REPLAY_STATE');
74
86
  names.set(block.id, block.name);
@@ -79,7 +91,7 @@ export async function encodePrompt(options, model, attachments, warn) {
79
91
  catch {
80
92
  throw new LlmError('Stored tool arguments are not valid JSON', 'INVALID_REPLAY_STATE');
81
93
  }
82
- content.push({ type: 'tool-call', toolCallId: callIds.get(block.id) ?? block.id, toolName: block.name, input, providerOptions });
94
+ content.push({ type: 'tool-call', toolCallId: wireId, toolName: block.name, input, providerOptions });
83
95
  }
84
96
  else if (block.type === 'image') {
85
97
  if (message.role === 'assistant')
@@ -87,7 +99,8 @@ export async function encodePrompt(options, model, attachments, warn) {
87
99
  content.push(await imagePart(block));
88
100
  }
89
101
  }
90
- prompt.push({ role: message.role, content });
102
+ if (content.length || !replay.foreign)
103
+ prompt.push({ role: message.role, content });
91
104
  }
92
105
  return prompt;
93
106
  }
@@ -8,6 +8,7 @@ export interface ReplayProjection {
8
8
  blocks: (SharedV4ProviderOptions | undefined)[];
9
9
  callIds: Map<string, string>;
10
10
  trusted: boolean;
11
+ foreign: boolean;
11
12
  }
12
13
  /** Validate the entire aligned envelope before restoring any signature or item id. */
13
14
  export declare function projectReplay(source: unknown, content: readonly ContentBlock[], model: OpenCodeModel, route: string, warn?: (reason: string) => void): ReplayProjection;
@@ -33,9 +33,14 @@ function completeResponseSignatures(blocks, content) {
33
33
  const APIS = { messages: 'anthropic-messages', responses: 'openai-responses', chat: 'openai-completions', google: 'google-generative-ai' };
34
34
  /** Validate the entire aligned envelope before restoring any signature or item id. */
35
35
  export function projectReplay(source, content, model, route, warn) {
36
- const empty = () => ({ blocks: content.map(() => undefined), callIds: new Map(), trusted: false });
37
36
  const message = record(source);
38
37
  const envelope = record(message?.replayState);
38
+ // A model switch deliberately discards provider-specific replay data. It is
39
+ // distinct from a damaged native envelope, whose signature checks still apply.
40
+ const foreign = typeof message?.provider === 'string' && !!message.provider && typeof message.model === 'string' && !!message.model && (message.provider !== route || message.model !== model.id);
41
+ const empty = () => ({ blocks: content.map(() => undefined), callIds: new Map(), trusted: false, foreign });
42
+ if (foreign)
43
+ return empty();
39
44
  if (!envelope) {
40
45
  if (message?.replayState !== undefined)
41
46
  warn?.('OpenCode history: malformed replay envelope; retaining message content');
@@ -122,7 +127,7 @@ export function projectReplay(source, content, model, route, warn) {
122
127
  });
123
128
  if (model.protocol === 'responses')
124
129
  completeResponseSignatures(blocks, content);
125
- return { blocks, callIds, trusted: true };
130
+ return { blocks, callIds, trusted: true, foreign: false };
126
131
  }
127
132
  catch (error) {
128
133
  warn?.(`OpenCode history: ${error instanceof Error ? error.message : 'invalid replay'}; retaining message content`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askdkc/dsh-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Interactive terminal interface for DeepSeek Harness agents, sessions and tools.",
5
5
  "keywords": [
6
6
  "dsh",