@toddzheng024/dscode-bundle 0.7.15 → 0.7.17

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.
@@ -25,8 +25,5 @@ Local changes: DSCODE effort, shell control, TUI commands and footer.
25
25
  @deepseek-ai/dsh-terminal-bash@0.1.5-rc.2: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/terminal/terminal-bash"}
26
26
  Local changes: DSCODE effort, shell control, TUI commands and footer.
27
27
 
28
- @deepseek-ai/dsh-compaction-basic@0.1.5-rc.2: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/compaction/compaction-basic"}
29
- Local changes: DSCODE effort, shell control, TUI commands and footer.
30
-
31
28
  dsh-code (DSCODE vendored terminal, forked from dsh-code@1.2.0): MIT; https://github.com/unlinearity/dsh-code
32
29
  Local changes: DSCODE UI (welcome header, activity line, footer telemetry, effort bar), commands, panels and paste handling.
package/bootstrap.mjs CHANGED
@@ -18,6 +18,7 @@ export function apply(ctx) {
18
18
  // resolution the launcher runs happens here before the agent preset mounts.
19
19
  process.env.DSCODE_SKILL_ANCESTOR_DIRS = JSON.stringify(ancestorSkillDirs({ cwd: process.cwd(), home: homedir() }));
20
20
  process.env.DSCODE_INSTRUCTION_HOME = writeWorkspaceInstructions({ cwd: process.cwd(), home: homedir(), stateDir: home }) ?? home;
21
+ process.env.DSCODE_SANDBOX_RUNNER = join(root, 'plugins/tui-tools/sandbox-runner.mjs');
21
22
  ctx.provide('dscodePaths', { presets: join(root, 'presets'), hooks: hookConfig.path });
22
23
  const oldPath = process.env.PATH;
23
24
  const added = join(root, 'bin');
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.15",
2
+ "version": "0.7.17",
3
3
  "type": "module",
4
4
  "license": "MIT",
5
5
  "author": "Todd Zheng",
@@ -49,7 +49,7 @@
49
49
  "./bash": "./vendor/bash/index.js",
50
50
  "./persistent": "./vendor/persistent/index.js",
51
51
  "./terminal": "./vendor/terminal/index.js",
52
- "./compaction-basic": "./vendor/compaction-basic/index.js",
52
+ "./compaction": "./plugins/compaction/engine.mjs",
53
53
  "./policy": "./plugins/dscode/index.mjs",
54
54
  "./code-review": "./plugins/code-review/index.mjs",
55
55
  "./auto-review": "./plugins/auto-review/index.mjs",
@@ -0,0 +1,304 @@
1
+ import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
2
+ import { ManualCompactionError, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
3
+ import { SessionSeq } from '@deepseek-ai/dsh-session';
4
+ import { effectiveContextWindow, fitsInWindow, prefetchThresholdTokens, pricedCompactionPolicy } from './threshold.mjs';
5
+
6
+ // DSCODE's compaction policy as a subclass of the upstream engine instead of a
7
+ // build-time patch of its source. It owns exactly the decisions this deployment
8
+ // adds - a threshold priced from the route's cache discount, the adapter's
9
+ // completion reserve, background prefetch, and overflow recovery that lets
10
+ // pruning decide first - and leaves the durable surface transaction to the
11
+ // upstream `compactRegion`, which stays the only writer.
12
+ //
13
+ // `BasicCompactionEngine` registers the automatic hooks itself and dispatches
14
+ // them through `this.compactIfNeeded`, so overriding it here replaces the policy
15
+ // without touching the installed package.
16
+ export class DscodeCompactionEngine extends BasicCompactionEngine {
17
+ constructor(ctx, config) {
18
+ super(ctx, config);
19
+ // The upstream default hides whether the deployment configured a threshold,
20
+ // so the priced policy is enabled only when the raw config left it unset.
21
+ this.dscodePricedThreshold = config?.thresholdRatio === undefined;
22
+ this.dscodePrefetch = new WeakMap();
23
+ this.dscodePendingPrefetch = new WeakMap();
24
+ this.dscodeWarnedTargets = new Set();
25
+ }
26
+
27
+ /**
28
+ * Compact for step-boundary pressure or one provider-confirmed overflow, with
29
+ * the threshold priced from the route's cache discount and the completion
30
+ * budget the adapter reserves inside the window.
31
+ * @param agent - agent whose latest durable routed request is measured.
32
+ * @param trigger - normal step-boundary pressure or context-overflow recovery.
33
+ * @param signal - live turn cancellation signal forwarded to summarization.
34
+ * @returns the latest summary compaction result, or `null` when no summary ran.
35
+ */
36
+ async compactIfNeeded(agent, trigger, signal) {
37
+ const target = this.dscodeRoutedTarget(agent.session);
38
+ if (target === undefined) return null;
39
+ const policy = this.dscodeTargetPolicy(target);
40
+ const meter = this.ctx.tokenMeter;
41
+ const prune = this.ctx.get('toolResultPruner');
42
+ let measurement = meter.measure(agent.session);
43
+
44
+ if (trigger === 'context-overflow') {
45
+ // The pruner runs first and may already return the failed request under the
46
+ // window. The caller retries whenever that prune replaced the surface, so a
47
+ // summary here would only add a model call and its stall to a request that
48
+ // no longer needs one.
49
+ const generation = agent.session.surface.replaceGeneration;
50
+ if (prune !== undefined) {
51
+ prune.pruneSession(agent.session);
52
+ measurement = meter.measure(agent.session);
53
+ }
54
+ if (agent.session.surface.replaceGeneration > generation) {
55
+ const info = await this.ctx.llm.resolveModelInfo(target.provider, target.model, signal);
56
+ if (fitsInWindow(measurement.totalTokens, info)) return null;
57
+ }
58
+ return this.dscodeCompactOldest(agent, measurement, 0, signal);
59
+ }
60
+
61
+ const modelInfo = await this.ctx.llm.resolveModelInfo(target.provider, target.model, signal);
62
+ const context = modelInfo.context;
63
+ if (context === undefined) {
64
+ const key = `${target.provider}/${target.model}`;
65
+ if (!this.dscodeWarnedTargets.has(key)) {
66
+ this.dscodeWarnedTargets.add(key);
67
+ this.ctx.logger.warn(`compaction: no context capacity for ${key}; configure contextWindow on that adapter model`);
68
+ }
69
+ return null;
70
+ }
71
+ this.dscodeAssertInactive(agent.session, 'automatic pressure compaction');
72
+ const spec = this.dscodeCompactSpec(await pricedCompactionPolicy({ ...this.config, dscodePricedThreshold: this.dscodePricedThreshold }, policy), effectiveContextWindow(context, modelInfo));
73
+
74
+ this.dscodePlanPrefetch(agent, measurement, spec, signal);
75
+ if (measurement.totalTokens < spec.thresholdTokens) return null;
76
+ if (prune !== undefined) {
77
+ prune.pruneSession(agent.session);
78
+ measurement = meter.measure(agent.session);
79
+ }
80
+ if (measurement.totalTokens < spec.thresholdTokens) return null;
81
+
82
+ let result = null;
83
+ const prefetched = await this.dscodeCommitPrefetch(agent, signal);
84
+ if (prefetched !== null) {
85
+ result = prefetched;
86
+ measurement = meter.measure(agent.session);
87
+ if (measurement.totalTokens < spec.thresholdTokens) return result;
88
+ }
89
+ for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) {
90
+ const range = this.dscodeSelectRange(agent.session, measurement, spec.retainTokens);
91
+ if (range === null) {
92
+ if (result === null) return null;
93
+ break;
94
+ }
95
+ result = await this.compactRegion(range.start, range.end, agent, signal);
96
+ measurement = meter.measure(agent.session);
97
+ if (measurement.totalTokens < spec.thresholdTokens) return result;
98
+ }
99
+ throw new Error(`compaction still above threshold after ${spec.compactionRetries + 1} compaction attempts (${measurement.totalTokens} estimated tokens >= threshold ${spec.thresholdTokens})`);
100
+ }
101
+
102
+ /**
103
+ * Summarize the replayed region through the upstream cache-reusing call, or
104
+ * return the summary a prefetch already produced for this exact commit.
105
+ * @param input - replayed conversation prefix to condense.
106
+ * @param agent - supplies routed-model history, fallback model, and session id.
107
+ * @param signal - optional cancellation forwarded to the adapter.
108
+ * @returns the summary blocks and the call envelope behind them.
109
+ */
110
+ async summarize(input, agent, signal) {
111
+ const pending = this.dscodePendingPrefetch.get(agent.session);
112
+ if (pending === undefined) return super.summarize(input, agent, signal);
113
+ this.dscodePendingPrefetch.delete(agent.session);
114
+ // The upstream transaction already opened its marker, so this wait is the visible
115
+ // compaction. A failed or invalidated prefetch falls back to a fresh call instead
116
+ // of failing a transaction that has already begun.
117
+ await pending.wait;
118
+ if (pending.failure !== null || agent.session.surface.replaceGeneration !== pending.generation) return super.summarize(input, agent, signal);
119
+ return pending.summarized;
120
+ }
121
+
122
+ /** Compact the oldest compactable span for a retention budget, or return null when none is safe. */
123
+ async dscodeCompactOldest(agent, measurement, retainTokens, signal) {
124
+ const range = this.dscodeSelectRange(agent.session, measurement, retainTokens);
125
+ if (range === null) return null;
126
+ return this.compactRegion(range.start, range.end, agent, signal);
127
+ }
128
+
129
+ /** The routed provider/model of the session's latest durable request. */
130
+ dscodeRoutedTarget(session) {
131
+ const config = session.requestHeader()?.config;
132
+ if (config === undefined || config.provider.length === 0 || config.model.length === 0) return undefined;
133
+ return { provider: config.provider, model: config.model };
134
+ }
135
+
136
+ /** Merge the exact-target override over the validated defaults for one routed target. */
137
+ dscodeTargetPolicy(target) {
138
+ const config = this.config;
139
+ const override = config.modelPolicies.find(entry => entry.provider === target.provider && entry.model === target.model);
140
+ const inherited = config.retainTokens === undefined ? { retainRatio: config.retainRatio } : { retainTokens: config.retainTokens };
141
+ const retention = override?.retainTokens !== undefined ? { retainTokens: override.retainTokens }
142
+ : override?.retainRatio !== undefined ? { retainRatio: override.retainRatio }
143
+ : inherited;
144
+ return {
145
+ target: { ...target },
146
+ thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,
147
+ ...retention,
148
+ summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,
149
+ summarizationModel: override?.summarizationModel ?? config.summarizationModel,
150
+ maxTokens: override?.maxTokens ?? config.maxTokens,
151
+ compactionRetries: override?.compactionRetries ?? config.compactionRetries,
152
+ maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries,
153
+ };
154
+ }
155
+
156
+ /** Scale one routed policy into concrete token budgets for its effective window. */
157
+ dscodeCompactSpec(policy, contextWindow) {
158
+ const key = `${policy.target.provider}/${policy.target.model}`;
159
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) throw new Error(`compaction: contextWindow (${contextWindow}) must be a positive integer for ${key}`);
160
+ const thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio);
161
+ const retainTokens = policy.retainTokens === undefined ? Math.floor(contextWindow * policy.retainRatio) : policy.retainTokens;
162
+ if (retainTokens >= thresholdTokens) throw new Error(`compaction: retainTokens (${retainTokens}) must be less than threshold tokens ${thresholdTokens} for ${key}`);
163
+ return { ...policy, contextWindow, thresholdTokens, retainTokens };
164
+ }
165
+
166
+ /**
167
+ * Resolve the next range starting at the first non-system surface node while
168
+ * retaining a priced recent tail and never splitting a tool-call/result pair.
169
+ * @param session - session supplying authoritative current surface positions.
170
+ * @param measurement - unified pressure and surface measurement from the conversation meter.
171
+ * @param retainTokens - minimum recent tail budget retained verbatim.
172
+ * @returns the positional range to compact, or `null`.
173
+ */
174
+ dscodeSelectRange(session, measurement, retainTokens) {
175
+ const pricedNodes = measurement.nodes;
176
+ if (pricedNodes.length === 0) return null;
177
+ const surfaceNodes = session.surface.nodes;
178
+ if (surfaceNodes.length !== pricedNodes.length || surfaceNodes.some((seq, index) => seq !== pricedNodes[index]?.seq)) throw new Error('compaction: token-meter surface does not match the current session surface');
179
+ const firstIdx = this.dscodeSystemHead(session, surfaceNodes[0]) === undefined ? 0 : 1;
180
+ let accumulated = 0;
181
+ let keepFromIdx = pricedNodes.length;
182
+ for (let index = pricedNodes.length - 1; index >= 0; index -= 1) {
183
+ accumulated += pricedNodes[index].tokens;
184
+ keepFromIdx = index;
185
+ if (accumulated >= retainTokens) break;
186
+ }
187
+ if (keepFromIdx <= firstIdx) return null;
188
+ while (keepFromIdx > firstIdx) {
189
+ if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx])) break;
190
+ keepFromIdx -= 1;
191
+ }
192
+ if (keepFromIdx <= firstIdx) return null;
193
+ return { start: surfaceNodes[firstIdx], end: surfaceNodes[keepFromIdx - 1], startIdx: firstIdx, endIdx: keepFromIdx - 1 };
194
+ }
195
+
196
+ /** The `system/message` at a surface head, or undefined when that node is something else. */
197
+ dscodeSystemHead(session, headSeq) {
198
+ if (headSeq === undefined) return undefined;
199
+ const head = session.eventAt(headSeq);
200
+ return head.type === 'system/message' ? head : undefined;
201
+ }
202
+
203
+ /** The replayed conversation prefix a summary call condenses: system head, tools, then the shadowed messages. */
204
+ dscodeSummarizationInput(session, shadowedSeqs) {
205
+ const header = session.requestHeader();
206
+ const nodes = session.surface.nodes;
207
+ const system = this.dscodeSystemHead(session, nodes[0]);
208
+ const head = system === undefined ? null : session.deriveEventMessage(system);
209
+ const region = shadowedSeqs.map(seq => session.deriveEventMessage(session.eventAt(seq))).filter(message => message !== null);
210
+ return { ...header?.tools === undefined ? {} : { tools: header.tools }, messages: head === null ? region : [head, ...region] };
211
+ }
212
+
213
+ /** The newest unmatched compaction start and end-seed boundary of one session. */
214
+ dscodeEntryState(session) {
215
+ let unmatchedCompactionStart;
216
+ let entryStateKnown = false;
217
+ let latestEndSeedSeq;
218
+ for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
219
+ const event = session.eventAt(SessionSeq(seq));
220
+ if (latestEndSeedSeq === undefined && event.type === 'session/end-seed') latestEndSeedSeq = event.seq;
221
+ if (!entryStateKnown) {
222
+ if (event.type === 'compaction/start') {
223
+ unmatchedCompactionStart = event;
224
+ entryStateKnown = true;
225
+ } else if (event.type === 'compaction/end') entryStateKnown = true;
226
+ }
227
+ if (entryStateKnown && latestEndSeedSeq !== undefined) break;
228
+ }
229
+ return { unmatchedCompactionStart, latestEndSeedSeq };
230
+ }
231
+
232
+ /** Reject a second compaction while the durable compaction lock is active. */
233
+ dscodeAssertInactive(session, stage) {
234
+ const { unmatchedCompactionStart, latestEndSeedSeq } = this.dscodeEntryState(session);
235
+ if (unmatchedCompactionStart === undefined || (latestEndSeedSeq !== undefined && latestEndSeedSeq > unmatchedCompactionStart.seq)) return;
236
+ throw new ManualCompactionError('busy', `${stage}: compaction already in progress; the session compaction lock is already active`);
237
+ }
238
+
239
+ /**
240
+ * Start one background summarization of the oldest compactable span once the
241
+ * measured pressure crosses the mark one lead below the priced threshold.
242
+ * Nothing is appended and no compaction lock is taken, so an invalidated or
243
+ * cancelled prefetch costs only the summarization call.
244
+ * @param agent - agent whose pressure the caller just priced.
245
+ * @param measurement - the measurement the caller priced.
246
+ * @param spec - the caller's resolved spec for this route.
247
+ * @param signal - live turn signal forwarded to the summarizer.
248
+ */
249
+ dscodePlanPrefetch(agent, measurement, spec, signal) {
250
+ const session = agent.session;
251
+ const existing = this.dscodePrefetch.get(session);
252
+ if (existing !== undefined) {
253
+ if (existing.failure === null) return;
254
+ this.dscodePrefetch.delete(session);
255
+ }
256
+ if (measurement.totalTokens >= spec.thresholdTokens) return;
257
+ if (measurement.totalTokens < prefetchThresholdTokens(spec.thresholdTokens, spec.contextWindow)) return;
258
+ if (this.dscodeEntryState(session).unmatchedCompactionStart !== undefined) return;
259
+ let range;
260
+ try {
261
+ range = this.dscodeSelectRange(session, measurement, spec.retainTokens);
262
+ } catch (error) {
263
+ return;
264
+ }
265
+ if (range === null) return;
266
+ const shadowedSeqs = session.surface.nodes.slice(range.startIdx, range.endIdx + 1);
267
+ const prefetch = { range: { start: range.start, end: range.end }, generation: session.surface.replaceGeneration, summarized: null, failure: null, wait: null };
268
+ this.dscodePrefetch.set(session, prefetch);
269
+ prefetch.wait = (async () => {
270
+ try {
271
+ const input = this.dscodeSummarizationInput(session, shadowedSeqs);
272
+ prefetch.summarized = await this.summarize(input, agent, signal);
273
+ } catch (error) {
274
+ prefetch.failure = error;
275
+ }
276
+ })();
277
+ }
278
+
279
+ /**
280
+ * Commit the pending prefetch when its span is still the same replacement
281
+ * target, or return null so the caller summarizes afresh. The upstream
282
+ * `compactRegion` owns the marker pair, the surface replacement and every
283
+ * stability check; `summarize` then returns the summary already produced.
284
+ * @param agent - agent whose pressure triggered the automatic compaction.
285
+ * @param signal - live turn signal forwarded to the transaction.
286
+ * @returns the committed compaction result, or null when no prefetch was usable.
287
+ */
288
+ async dscodeCommitPrefetch(agent, signal) {
289
+ const session = agent.session;
290
+ const prefetch = this.dscodePrefetch.get(session);
291
+ if (prefetch === undefined) return null;
292
+ this.dscodePrefetch.delete(session);
293
+ if (prefetch.failure !== null) return null;
294
+ if (session.surface.replaceGeneration !== prefetch.generation) return null;
295
+ this.dscodePendingPrefetch.set(session, prefetch);
296
+ try {
297
+ return await this.compactRegion(prefetch.range.start, prefetch.range.end, agent, signal);
298
+ } finally {
299
+ this.dscodePendingPrefetch.delete(session);
300
+ }
301
+ }
302
+ }
303
+
304
+ export default DscodeCompactionEngine;
@@ -28,6 +28,46 @@ export function prefetchThresholdTokens(thresholdTokens, contextWindow, leadRati
28
28
  return Math.max(0, thresholdTokens - Math.floor(contextWindow * leadRatio));
29
29
  }
30
30
 
31
+ /**
32
+ * The window the session messages may occupy. An adapter that keeps the completion
33
+ * budget inside the context window rejects a request once messages plus completion
34
+ * exceed it, so a threshold priced from the full window sits above a ceiling the
35
+ * provider enforces first: the pressure path never fires and every compaction arrives
36
+ * through overflow recovery, which summarizes synchronously and stalls the turn.
37
+ *
38
+ * A reported budget is always subtracted: every OpenAI-compatible adapter counts
39
+ * `max_tokens` toward the same limit, and an adapter that reports none keeps the full
40
+ * window, so the subtraction can only make compaction earlier than the low-level
41
+ * threshold would, never later than the provider allows.
42
+ * @param context - the resolved model info context, `{ contextWindow }`.
43
+ * @param modelInfo - the resolved model info, whose `defaultMaxTokens` is that budget.
44
+ */
45
+ /** The completion budget an adapter reserves inside the window, 0 when it reports none. */
46
+ function completionReserve(contextWindow, modelInfo) {
47
+ const reserve = modelInfo?.defaultMaxTokens;
48
+ if (!Number.isInteger(reserve) || reserve <= 0 || reserve >= contextWindow) return 0;
49
+ return reserve;
50
+ }
51
+
52
+ export function effectiveContextWindow(context, modelInfo) {
53
+ const contextWindow = context?.contextWindow;
54
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) return contextWindow;
55
+ return contextWindow - completionReserve(contextWindow, modelInfo);
56
+ }
57
+
58
+ /**
59
+ * Whether a measured request envelope plus the reserved completion budget fits the models
60
+ * window, which is the rule the provider enforces. Overflow recovery uses it to tell a
61
+ * request that pruning alone returned under the window from one that still needs a summary.
62
+ * @param totalTokens - the measured request envelope.
63
+ * @param modelInfo - the resolved model info: `context.contextWindow` and `defaultMaxTokens`.
64
+ */
65
+ export function fitsInWindow(totalTokens, modelInfo) {
66
+ const contextWindow = modelInfo?.context?.contextWindow;
67
+ if (!Number.isInteger(totalTokens) || !Number.isInteger(contextWindow) || contextWindow <= 0) return false;
68
+ return totalTokens + completionReserve(contextWindow, modelInfo) <= contextWindow;
69
+ }
70
+
31
71
  /** The route's threshold ratio, waiting for the OpenRouter listing when it has not loaded yet. */
32
72
  export async function pricedThresholdRatio(provider, model, now = Date.now()) {
33
73
  if (provider === 'openrouter') await ensureOpenRouterModels({ home: process.env.DSH_HOME, now });
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ // A custom runner for @deepseek-ai/dsh-sandbox-local, selected through the `sandbox`
3
+ // row's `runnerCommand`. The provider appends a bwrap-compatible profile and then
4
+ // `--` and the command:
5
+ //
6
+ // dsh-sandbox-runner --ro-bind / / --dev /dev --unshare-pid --proc /proc
7
+ // --die-with-parent [--tmpfs /tmp] [--bind <root> <root>] -- <command...>
8
+ //
9
+ // We translate the profile into a Seatbelt one and add the single grant the built-in
10
+ // profile lacks: /dev/ptmx. Without it a confined command cannot allocate a PTY
11
+ // (posix_openpt returns EPERM), which silently breaks nested harnesses, tmux, expect
12
+ // and any node-pty based suite. When the kernel refuses to apply another profile —
13
+ // which is exactly what happens inside an already-confined process — the command
14
+ // inherits the enclosing profile instead of nesting a second one.
15
+ import { spawn, spawnSync } from 'node:child_process';
16
+ import { realpathSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ export const NAME = 'dscode-sandbox-runner';
22
+ // Configure this string as runnerFailureSignatures so the provider recognises our
23
+ // own failures instead of reading them as a denied command.
24
+ export const FATAL_PREFIX = `${NAME}: fatal: `;
25
+ // Informational output must never carry the configured failure signature: the provider
26
+ // turns any non-zero exit whose stderr matches it into a sandbox failure, so a notice
27
+ // would misreport a failing command as a broken runner.
28
+ export const NOTICE_PREFIX = `${NAME}: notice: `;
29
+ export const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
30
+
31
+ const OPERAND_FLAGS = new Map([['--ro-bind', 2], ['--bind', 2], ['--tmpfs', 1], ['--dev', 1], ['--proc', 1], ['--dir', 1]]);
32
+
33
+ const canonical = path => {
34
+ try { return realpathSync(path); } catch { return resolve(path); }
35
+ };
36
+
37
+ const sbpl = path => `"${String(path).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
38
+
39
+ /** Split the appended bwrap-compatible profile from the command after `--`. */
40
+ export function parseProfile(argv) {
41
+ const writable = [];
42
+ let index = 0;
43
+ while (index < argv.length) {
44
+ const token = argv[index];
45
+ if (token === '--') { index += 1; break; }
46
+ if (!token.startsWith('--')) throw new Error(`unexpected profile argument ${JSON.stringify(token)}`);
47
+ const operands = OPERAND_FLAGS.get(token) ?? 0;
48
+ if (operands === 2) {
49
+ const [, destination] = [argv[index + 1], argv[index + 2]];
50
+ if (!destination) throw new Error(`${token} needs two operands`);
51
+ if (token === '--bind') writable.push(destination);
52
+ } else if (operands === 1 && !argv[index + 1]) throw new Error(`${token} needs an operand`);
53
+ index += operands + 1;
54
+ }
55
+ return { writable, command: argv.slice(index) };
56
+ }
57
+
58
+ /** Writable roots that mirror the provider's own Seatbelt grant, plus the temp areas. */
59
+ export function writableRoots(parsed, { temp = tmpdir() } = {}) {
60
+ return [...new Set([...parsed.writable, '/tmp', temp].map(canonical))];
61
+ }
62
+
63
+ /** The SBPL profile: upstream's deny-by-default write policy plus /dev/ptmx. */
64
+ export function seatbeltProfile(roots) {
65
+ const forms = [
66
+ '(version 1)',
67
+ '(allow default)',
68
+ '(deny file-write*)',
69
+ `(allow file-write* (literal ${sbpl('/dev/null')}))`,
70
+ `(allow file-write* (literal ${sbpl('/dev/ptmx')}))`,
71
+ ];
72
+ if (roots.length) forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbpl(root)})`).join(' ')})`);
73
+ return forms.join(' ');
74
+ }
75
+
76
+ /** Whether this process may apply a Seatbelt profile at all. */
77
+ export function seatbeltApplies(exec = SANDBOX_EXEC) {
78
+ const probe = spawnSync(exec, ['-p', '(version 1)(allow default)', '/usr/bin/true'], { stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf8' });
79
+ if (probe.error) return probe.error.code === 'ENOENT' ? { ok: false, reason: 'missing' } : { ok: false, reason: probe.error.message };
80
+ if (probe.status === 0) return { ok: true };
81
+ return { ok: false, reason: (probe.stderr ?? '').trim().split('\n').at(-1) || `exit ${probe.status}` };
82
+ }
83
+
84
+ const SIGNAL_CODES = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
85
+
86
+ function runUnder(program, args) {
87
+ return new Promise(resolvePromise => {
88
+ const child = spawn(program, args, { stdio: 'inherit' });
89
+ const forward = signal => () => { try { child.kill(signal); } catch { /* already gone */ } };
90
+ const handlers = Object.keys(SIGNAL_CODES).map(signal => [signal, forward(signal)]);
91
+ for (const [signal, handler] of handlers) process.on(signal, handler);
92
+ child.on('error', error => { process.stderr.write(`${FATAL_PREFIX}${error.message}\n`); resolvePromise(126); });
93
+ child.on('exit', (code, signal) => resolvePromise(code ?? SIGNAL_CODES[signal] ?? 1));
94
+ });
95
+ }
96
+
97
+ export async function run(argv, { exec = SANDBOX_EXEC, stderr = process.stderr } = {}) {
98
+ const parsed = parseProfile(argv);
99
+ if (parsed.command.length === 0) {
100
+ stderr.write(`${FATAL_PREFIX}no command after --\n`);
101
+ return 126;
102
+ }
103
+ const [program, ...args] = parsed.command;
104
+ const applies = seatbeltApplies(exec);
105
+ if (!applies.ok && applies.reason === 'missing') {
106
+ stderr.write(`${FATAL_PREFIX}${exec} is not available; refusing to run unconfined\n`);
107
+ return 126;
108
+ }
109
+ if (!applies.ok) {
110
+ // Applying a profile is what the kernel refuses inside an existing one, so this
111
+ // process is already confined: inherit that profile rather than nest a second.
112
+ stderr.write(`${NOTICE_PREFIX}inheriting the enclosing profile (${applies.reason})\n`);
113
+ return runUnder(program, args);
114
+ }
115
+ return runUnder(exec, ['-p', seatbeltProfile(writableRoots(parsed)), '--', program, ...args]);
116
+ }
117
+
118
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
119
+ run(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch(error => {
120
+ process.stderr.write(`${FATAL_PREFIX}${error.message}\n`);
121
+ process.exitCode = 126;
122
+ });
123
+ }
@@ -198,7 +198,7 @@
198
198
  toolResultPruner: true
199
199
  config:
200
200
  - id: compaction-basic
201
- name: '@toddzheng024/dscode-bundle/compaction-basic'
201
+ name: '@toddzheng024/dscode-bundle/compaction'
202
202
 
203
203
  - id: command-compact
204
204
  name: '@deepseek-ai/dsh-command-compact'
@@ -32,7 +32,7 @@ import { dscodeChatLines } from './dscode/chat.mjs';
32
32
  import { readFileSync } from 'node:fs';
33
33
  import { footerFor as dscodeFooterFor } from '../../../plugins/session-metrics/view.mjs';
34
34
  import { newerVersion as dscodeNewerVersion } from '../../../plugins/tui-tools/update.mjs';
35
- import { t as dscodeMessage, normalizeLanguage as dscodeNormalizeLanguage } from '../../../plugins/i18n/messages.mjs';
35
+ import { languageName as dscodeLanguageName, normalizeLanguage as dscodeNormalizeLanguage, t as dscodeMessage } from '../../../plugins/i18n/messages.mjs';
36
36
  import { dscodeTelemetryNodes } from './dscode/telemetry.mjs';
37
37
  import { dscodeFooterHeader } from './render/status.mjs';
38
38
  import { dscodePadEnd, welcomeArtRows, welcomePath, WELCOME_ART, WELCOME_ART_SMALL } from './dscode/welcome.mjs';
@@ -4604,9 +4604,10 @@ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, de
4604
4604
  const argument = text.slice('/language'.length).trim();
4605
4605
  if (argument === '')
4606
4606
  openLanguage();
4607
- else if (argument === 'en' || argument === 'zh') {
4608
- saveLanguage(parseLanguageName(argument));
4609
- notify(t('notice.languageSaved', { name: argument }));
4607
+ else if (dscodeNormalizeLanguage(argument) !== null) {
4608
+ const name = parseLanguageName(argument);
4609
+ saveLanguage(name);
4610
+ notify(t('notice.languageSaved', { name: dscodeLanguageName(name) }));
4610
4611
  refresh();
4611
4612
  }
4612
4613
  else
@@ -6,26 +6,32 @@
6
6
  * choice lives in language.json next to theme.json (see the runner's
7
7
  * persistence block).
8
8
  *
9
+ * The selectable list and the alias table come from the DSCODE message
10
+ * tables, so `/language` offers everything those tables translate. The
11
+ * terminal's own catalogues still only cover English and Simplified Chinese:
12
+ * any other choice paints the shell in English and DSCODE's labels in the
13
+ * chosen language, which is exactly what `/language` promises.
14
+ *
9
15
  * @module @deepseek-ai/dsh-tui/i18n
10
16
  */
11
17
  import { en } from './locales/en.mjs';
12
18
  import { zh } from './locales/zh.mjs';
13
- /** Valid language names for argument parsing. */
14
- export const LANGUAGE_NAMES = ['en', 'zh'];
19
+ import { LANGUAGES as DSCODE_LANGUAGES, normalizeLanguage } from '../../../plugins/i18n/messages.mjs';
20
+ /** Valid language names for argument parsing, in picker order. */
21
+ export const LANGUAGE_NAMES = DSCODE_LANGUAGES.map(language => language.code);
15
22
  /** The /language picker rows in canonical order. */
16
- export const LANGUAGES = [
17
- { id: 'en', label: 'English' },
18
- { id: 'zh', label: '中文' },
19
- ];
20
- const CATALOGS = { en, zh };
23
+ export const LANGUAGES = DSCODE_LANGUAGES.map(language => ({ id: language.code, label: language.name }));
24
+ /** Catalogues this module can paint; anything else keeps English. */
25
+ const CATALOGS = { en, 'zh-CN': zh, 'zh-TW': zh };
21
26
  /** The language in force. */
22
27
  let activeName = 'en';
23
28
  /**
24
- * Parse a persisted or typed language name: only 'en' and 'zh' survive;
25
- * anything else falls back to English.
29
+ * Parse a persisted or typed language name through the DSCODE alias table, so
30
+ * `zh`, `jp` or `简体中文` all resolve. An unknown value falls back to English.
26
31
  */
27
32
  export function parseLanguageName(value) {
28
- return value === 'zh' ? 'zh' : 'en';
33
+ const code = normalizeLanguage(value);
34
+ return code !== null && LANGUAGE_NAMES.includes(code) ? code : 'en';
29
35
  }
30
36
  /** The language name in force. */
31
37
  export function getLanguage() {
@@ -37,11 +43,11 @@ export function setLanguage(name) {
37
43
  }
38
44
  /**
39
45
  * One message from the active catalog, with `{n}`-style placeholders filled
40
- * from the params record. Unknown placeholders stay literal; a missing key
41
- * falls back to the English entry so a catalog gap degrades visibly but
42
- * never crashes.
46
+ * from the params record. Unknown placeholders stay literal; a language the
47
+ * terminal cannot paint, or a missing key, falls back to the English entry so
48
+ * a catalog gap degrades visibly but never crashes.
43
49
  */
44
50
  export function t(key, params = {}) {
45
- const template = CATALOGS[activeName][key] ?? en[key];
51
+ const template = CATALOGS[activeName]?.[key] ?? en[key];
46
52
  return template.replace(/\{(\w+)\}/gu, (whole, name) => params[name] === undefined ? whole : String(params[name]));
47
53
  }
@@ -26,7 +26,7 @@ import { internals } from './internals.mjs';
26
26
  import { syncModelCapabilities } from './model-capabilities.mjs';
27
27
  import { ensureProviderRoute as dscodeEnsureProviderRoute, migrateOpenRouterProfile as dscodeMigrateOpenRouter } from '../../../plugins/providers/catalog.mjs';
28
28
  import { grokStatusSnapshot } from '../../../plugins/grok/status.mjs';
29
- import { compactionPreview as dscodeCompactionPreview, pricedThresholdRatio as dscodePricedThresholdRatio } from '../../../plugins/compaction/threshold.mjs';
29
+ import { compactionPreview as dscodeCompactionPreview, effectiveContextWindow as dscodeEffectiveContextWindow, pricedThresholdRatio as dscodePricedThresholdRatio } from '../../../plugins/compaction/threshold.mjs';
30
30
  import { dscodeLoadOpenRouterAccountFor, dscodeManagementKeyStatus, dscodeSaveManagementKey } from './app.mjs';
31
31
  import { buildModelSelection, applyModelSelectionToConfig, loadModelDirectory, modelSelectionLabel, pendingModelSelection, resolveEffectiveSelection } from './models.mjs';
32
32
  import { discoverProviderModels, loadProviderSettings, removeProviderSettings, saveProviderCredential, saveProviderConfiguration, subscribeProviderSettings, unsetProviderCredential, } from './provider-settings.mjs';
@@ -1462,7 +1462,7 @@ async function run(ctx, startup, io) {
1462
1462
  const info = await llm.resolveModelInfo(row.provider, row.model);
1463
1463
  return dscodeCompactionPreview({
1464
1464
  used,
1465
- contextWindow: info?.context?.contextWindow,
1465
+ contextWindow: dscodeEffectiveContextWindow(info?.context, info),
1466
1466
  thresholdRatio: await dscodePricedThresholdRatio(row.provider, row.model),
1467
1467
  label: row.provider + '/' + row.model,
1468
1468
  });
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 DeepSeek
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.