@toddzheng024/dscode-bundle 0.7.1 → 0.7.3

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.
Files changed (41) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/cordis.patch.yml +4 -0
  3. package/package.json +4 -2
  4. package/plugins/auto-review/index.mjs +21 -3
  5. package/plugins/auto-review/policy.mjs +25 -0
  6. package/plugins/code-review/git.mjs +24 -2
  7. package/plugins/code-review/index.mjs +13 -6
  8. package/plugins/credentials/index.mjs +7 -5
  9. package/plugins/exec/cli.mjs +63 -0
  10. package/plugins/exec/index.mjs +1 -1
  11. package/plugins/i18n/messages.mjs +6 -6
  12. package/plugins/memory/index.mjs +17 -12
  13. package/plugins/providers/catalog.mjs +134 -0
  14. package/plugins/session-cards/index.mjs +11 -8
  15. package/plugins/session-cards/manager.mjs +1 -1
  16. package/plugins/session-metrics/attribution.mjs +19 -0
  17. package/plugins/session-metrics/balance.mjs +71 -0
  18. package/plugins/session-metrics/index.mjs +39 -9
  19. package/plugins/session-metrics/pricing.mjs +39 -5
  20. package/plugins/session-metrics/view.mjs +36 -15
  21. package/plugins/tui-tools/doctor.mjs +9 -6
  22. package/plugins/tui-tools/index.mjs +2 -2
  23. package/plugins/ultra/policy.mjs +16 -0
  24. package/vendor/persistent/index.js +19 -2
  25. package/vendor/pi-ai/LICENSE +21 -0
  26. package/vendor/pi-ai/index.js +2702 -0
  27. package/vendor/pi-ai/types/adapter.d.ts +105 -0
  28. package/vendor/pi-ai/types/auth.d.ts +60 -0
  29. package/vendor/pi-ai/types/catalog.d.ts +355 -0
  30. package/vendor/pi-ai/types/config.d.ts +208 -0
  31. package/vendor/pi-ai/types/context.d.ts +42 -0
  32. package/vendor/pi-ai/types/discovery.d.ts +43 -0
  33. package/vendor/pi-ai/types/index.d.ts +69 -0
  34. package/vendor/pi-ai/types/login.d.ts +21 -0
  35. package/vendor/pi-ai/types/provider.d.ts +59 -0
  36. package/vendor/pi-ai/types/replay.d.ts +63 -0
  37. package/vendor/pi-ai/types/stream.d.ts +43 -0
  38. package/vendor/tui/dscode-providers/catalog.mjs +134 -0
  39. package/vendor/tui/index.mjs +310 -295
  40. package/vendor/tui/.dscode-viewport-probe-78546.mjs +0 -39239
  41. package/vendor/tui/dscode-email.mjs +0 -69
@@ -3,19 +3,22 @@ import { homedir } from 'node:os';
3
3
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
4
4
  import { SessionCards } from './manager.mjs';
5
5
  import { TOPIC_PROMPT } from './content.mjs';
6
+ import { chargeTo } from '../session-metrics/attribution.mjs';
6
7
  export const name = 'dscode-session-cards';
7
8
  export const inject = ['sessions', 'llm'];
8
9
  export function apply(ctx, config = {}) {
9
10
  const home = process.env.DSH_HOME ?? process.env.DSCODE_HOME ?? join(homedir(), '.local/share/dscode-hub');
10
- const cards = new SessionCards({ root: join(home, 'session-cards'), config, generate: async (input, route, signal) => {
11
+ const cards = new SessionCards({ root: join(home, 'session-cards'), config, generate: async (input, route, signal, sessionId) => {
11
12
  const assembler = new BlockAssembler(); let finished = false, usage;
12
- for await (const chunk of ctx.llm.stream({ ...route, reasoningEffort: 'low', system: TOPIC_PROMPT,
13
- messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
14
- maxTokens: 2000, signal })) {
15
- signal.throwIfAborted(); assembler.push(chunk);
16
- if (chunk.type === 'finish') finished = true;
17
- if (chunk.type === 'usage') usage = chunk.usage;
18
- }
13
+ await chargeTo(sessionId, 'session-card', async () => {
14
+ for await (const chunk of ctx.llm.stream({ ...route, reasoningEffort: 'low', system: TOPIC_PROMPT,
15
+ messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
16
+ maxTokens: 2000, signal })) {
17
+ signal.throwIfAborted(); assembler.push(chunk);
18
+ if (chunk.type === 'finish') finished = true;
19
+ if (chunk.type === 'usage') usage = chunk.usage;
20
+ }
21
+ });
19
22
  if (!finished || assembler.finish.kind !== 'stop') throw Error('Incomplete topic response');
20
23
  const blocks = assembler.blocks();
21
24
  if (blocks.some(b => !['text', 'reasoning'].includes(b.type))) throw Error('Unexpected topic tool call');
@@ -105,7 +105,7 @@ export class SessionCards {
105
105
  try {
106
106
  const signal = AbortSignal.any([this.stop.signal, controller.signal, AbortSignal.timeout(this.config.timeoutMs)]);
107
107
  const result = await this.generate({ messages, topicCount: this.config.topicCount },
108
- { provider: this.config.provider ?? state.route.provider, model: this.config.model ?? state.route.model }, signal);
108
+ { provider: this.config.provider ?? state.route.provider, model: this.config.model ?? state.route.model }, signal, state.session.id);
109
109
  usage = result.usage;
110
110
  signal.throwIfAborted();
111
111
  if (this.states.get(state.session.id) !== state || hash !== this.hash(state)) return;
@@ -0,0 +1,19 @@
1
+ // Charge model calls a plugin makes for a session (review, memory, session cards,
2
+ // /doctor) to that session's cost ledger without putting the session on the wire:
3
+ // a `sessionId` request option also drives session-log delivery, provider cache
4
+ // affinity and agent-only prompt shaping, which these calls must not trigger. The
5
+ // ledger middleware runs when the caller iterates the stream, so it reads the charge
6
+ // from the caller's async context.
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+
9
+ const charges = new AsyncLocalStorage();
10
+
11
+ /** Run `fn` with its model calls charged to `sessionId` under `purpose`; no session runs it uncharged. */
12
+ export function chargeTo(sessionId, purpose, fn) {
13
+ return sessionId ? charges.run({ sessionId, purpose }, fn) : fn();
14
+ }
15
+
16
+ /** The charge of the model call being iterated, if any. */
17
+ export function currentCharge() {
18
+ return charges.getStore();
19
+ }
@@ -0,0 +1,71 @@
1
+ // Remaining provider balance, refreshed on a long cache. DeepSeek's response
2
+ // also carries the trusted clock: its `Date` header anchors peak/off-peak pricing
3
+ // without a second network call. OpenRouter has no peak window, so only its
4
+ // remaining credits are read.
5
+ const SOURCES = {
6
+ 'deepseek-official': { url: 'https://api.deepseek.com/user/balance', env: 'DEEPSEEK_API_KEY', parse: body => parseBalance(body), clock: true },
7
+ openrouter: { url: 'https://openrouter.ai/api/v1/credits', env: 'OPENROUTER_API_KEY', parse: body => parseOpenRouterCredits(body), clock: false },
8
+ };
9
+ export const BALANCE_PROVIDERS = Object.freeze(Object.keys(SOURCES));
10
+ const CACHE_MS = 5 * 60 * 1000;
11
+ const RETRY_MS = 60 * 1000;
12
+ const snapshots = new Map();
13
+ let clock = { anchor: null, skewMs: 0 };
14
+ const snapshotOf = provider => snapshots.get(provider) ?? { balance: null, fetchedAt: 0, pending: false };
15
+
16
+ /** Best-effort positive USD balance from a `/user/balance` body. */
17
+ export function parseBalance(body) {
18
+ const infos = Array.isArray(body?.balance_infos) ? body.balance_infos : [];
19
+ const usd = infos.find(info => String(info?.currency ?? '').toUpperCase() === 'USD') ?? infos[0];
20
+ const total = Number(usd?.total_balance);
21
+ if (!Number.isFinite(total) || total < 0) return null;
22
+ return body?.is_available === false ? null : total;
23
+ }
24
+
25
+ /** Remaining USD credits from an OpenRouter `/credits` body: purchased minus used. */
26
+ export function parseOpenRouterCredits(body) {
27
+ const credits = Number(body?.data?.total_credits), used = Number(body?.data?.total_usage);
28
+ if (body?.data?.total_credits == null || body?.data?.total_usage == null) return null;
29
+ if (!Number.isFinite(credits) || !Number.isFinite(used) || credits < 0 || used < 0) return null;
30
+ return Math.max(0, credits - used);
31
+ }
32
+
33
+ export function balanceNow(provider = 'deepseek-official') { return snapshotOf(provider).balance; }
34
+ /** Clock anchored to the last DeepSeek balance response's `Date` header, else the local one. */
35
+ export function trustedNow() {
36
+ const local = Date.now();
37
+ return clock.anchor === null ? local : local + clock.skewMs;
38
+ }
39
+
40
+ /** Refresh one provider at most once per cache window; never throws into the render path. */
41
+ export async function refreshBalance(options = {}) {
42
+ const provider = options.provider ?? 'deepseek-official';
43
+ const source = SOURCES[provider];
44
+ if (!source) return null;
45
+ const snapshot = snapshotOf(provider);
46
+ const now = Date.now();
47
+ if (snapshot.pending || now - snapshot.fetchedAt < CACHE_MS) return snapshot.balance;
48
+ const retrySoon = () => Date.now() - (CACHE_MS - RETRY_MS);
49
+ const key = options.key ?? process.env[source.env];
50
+ if (!key) { snapshots.set(provider, { ...snapshot, fetchedAt: retrySoon(), pending: false }); return snapshot.balance; }
51
+ const fetchImpl = options.fetch ?? globalThis.fetch;
52
+ if (typeof fetchImpl !== 'function') return snapshot.balance;
53
+ snapshots.set(provider, { ...snapshot, pending: true });
54
+ try {
55
+ const response = await fetchImpl(source.url, { headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' } });
56
+ const header = source.clock ? response.headers?.get?.('date') : null;
57
+ const anchor = header ? Date.parse(header) : NaN;
58
+ const body = await response.json();
59
+ const parsed = response.ok ? source.parse(body) : null;
60
+ if (Number.isFinite(anchor)) clock = { anchor, skewMs: anchor - Date.now() };
61
+ snapshots.set(provider, {
62
+ balance: parsed === null && response.ok ? null : parsed ?? snapshot.balance,
63
+ fetchedAt: parsed === null ? retrySoon() : Date.now(),
64
+ pending: false,
65
+ });
66
+ } catch {
67
+ // A transient failure keeps the last known balance and retries sooner.
68
+ snapshots.set(provider, { ...snapshot, fetchedAt: retrySoon(), pending: false });
69
+ }
70
+ return snapshotOf(provider).balance;
71
+ }
@@ -1,8 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { appendMetric } from './store.mjs';
3
- import { estimateCost, PRICE_VERSION } from './pricing.mjs';
3
+ import { estimateCost, priceVersionFor } from './pricing.mjs';
4
4
  import { setMetricSource } from './view.mjs';
5
+ import { BALANCE_PROVIDERS, refreshBalance } from './balance.mjs';
6
+ import { providerSpec } from '../providers/catalog.mjs';
5
7
  import { createWindowRate } from './rate.mjs';
8
+ import { currentCharge } from './attribution.mjs';
9
+
10
+ // Chunks that carry generated output; the first one marks time to first token.
11
+ const OUTPUT_CHUNKS = new Set(['text-delta', 'reasoning-delta', 'tool-call-delta']);
6
12
  export const name = 'dscode-session-metrics';
7
13
  export const inject = ['llm', 'agents', 'tokenMeter', 'sessionProjections'];
8
14
  export function apply(ctx) {
@@ -21,29 +27,53 @@ export function apply(ctx) {
21
27
  return { ...value, currentTps: liveRate.get(session) };
22
28
  }));
23
29
  const home = process.env.DSH_HOME;
30
+ // Remaining balance is best-effort decoration: resolve the key lazily (never
31
+ // inject the credentials service, so a missing one cannot fail startup), keep
32
+ // the request on a five-minute cache, and never let it reach the render path.
33
+ const credentials = ctx.get?.('credentials');
34
+ const refresh = () => Promise.all(BALANCE_PROVIDERS.map(async provider => {
35
+ try {
36
+ const ref = providerSpec(provider).credentialRef;
37
+ const resolved = await credentials?.resolve?.(ref);
38
+ const key = typeof resolved === 'string' ? resolved : resolved?.value;
39
+ await refreshBalance({ provider, key: key ?? process.env[ref] });
40
+ } catch {
41
+ /* balance stays unknown */
42
+ }
43
+ }));
44
+ refresh();
45
+ const balanceTimer = setInterval(refresh, 5 * 60 * 1000);
46
+ if (typeof balanceTimer.unref === 'function') balanceTimer.unref();
47
+ ctx.effect(() => () => clearInterval(balanceTimer));
24
48
  const record = (id, entry) => { try { appendMetric(home, id, entry); } catch { ctx.logger.warn('Session cost telemetry could not be saved.'); } };
25
49
  ctx.on('llm/stream', async function* (options, next) {
26
- if (!options.sessionId || !home) { yield* next(); return; }
50
+ // Plugin calls made for a session carry no sessionId on the wire; they are charged through the async context.
51
+ const charge = options.sessionId ? undefined : currentCharge();
52
+ const sessionId = options.sessionId ?? charge?.sessionId;
53
+ if (!sessionId || !home) { yield* next(); return; }
27
54
  const id = randomUUID(), time = Date.now();
28
- const recipients = new Set([options.sessionId]);
29
- let child = ctx.agents.get(options.sessionId);
55
+ const purpose = options.purpose ?? charge?.purpose ?? 'agent';
56
+ const recipients = new Set([sessionId]);
57
+ let child = ctx.agents.get(sessionId);
30
58
  while (child?.session.header.origin === 'subagent' && child.session.header.parentSession && !recipients.has(child.session.header.parentSession)) {
31
59
  recipients.add(child.session.header.parentSession);
32
60
  child = ctx.agents.get(child.session.header.parentSession);
33
61
  }
34
- const save = entry => { for (const recipient of recipients) record(recipient, { ...entry, sessionId: options.sessionId }); };
35
- save( { kind: 'start', id, time, provider: options.provider, model: options.model, purpose: options.purpose ?? 'agent' });
36
- let usage;
37
- const liveSession = !options.purpose || options.purpose === 'agent' ? ctx.agents.get(options.sessionId)?.session : undefined;
62
+ const save = entry => { for (const recipient of recipients) record(recipient, { ...entry, sessionId }); };
63
+ save({ kind: 'start', id, time, provider: options.provider, model: options.model, purpose });
64
+ let usage, firstTokenTime;
65
+ const liveSession = purpose === 'agent' ? ctx.agents.get(sessionId)?.session : undefined;
38
66
  try {
39
67
  for await (const chunk of next()) {
40
68
  if (chunk.type === 'usage') usage = chunk.usage;
69
+ else if (firstTokenTime === undefined && OUTPUT_CHUNKS.has(chunk.type)) firstTokenTime = Date.now();
41
70
  if (liveSession) liveRate.add(liveSession, chunk);
42
71
  yield chunk;
43
72
  }
44
73
  } finally {
45
74
  if (liveSession) liveRate.calibrate(liveSession, usage?.outputTokens);
46
- save({ kind: 'end', id, time, usage: usage ?? null, cost: estimateCost(options.provider, options.model, usage, time), priceVersion: PRICE_VERSION });
75
+ // `time` stays the start (it prices the call); `endTime` and `firstTokenTime` time it.
76
+ save({ kind: 'end', id, time, endTime: Date.now(), ...(firstTokenTime === undefined ? {} : { firstTokenTime }), usage: usage ?? null, cost: estimateCost(options.provider, options.model, usage, time), priceVersion: priceVersionFor(options.provider) });
47
77
  }
48
78
  });
49
79
  }
@@ -2,17 +2,51 @@
2
2
  // https://api-docs.deepseek.com/quick_start/pricing/
3
3
  export const PRICE_SOURCE = 'https://api-docs.deepseek.com/quick_start/pricing/';
4
4
  export const PRICE_VERSION = 'deepseek-2026-09-11';
5
+ // OpenRouter list prices for the DeepSeek models `/provider openrouter` declares,
6
+ // from the pinned pi-ai 0.85.1 catalog (its OpenRouter /models snapshot).
7
+ // OpenRouter bills no peak window. [cache read, input, output].
8
+ export const OPENROUTER_PRICE_VERSION = 'openrouter-pi-ai-0.85.1';
9
+ const OPENROUTER_PRICES = {
10
+ 'deepseek/deepseek-v4-flash': [0.017052, 0.08526, 0.17052],
11
+ 'deepseek/deepseek-v4-pro': [0.074196, 0.890358, 1.780716],
12
+ 'deepseek/deepseek-v4-flash-vision-exp': [0.007, 0.22, 0.66],
13
+ };
14
+
15
+ /** The price table a provider's ledger entries are estimated with. */
16
+ export function priceVersionFor(provider) {
17
+ return provider === 'openrouter' ? OPENROUTER_PRICE_VERSION : PRICE_VERSION;
18
+ }
19
+
5
20
  export function estimateCost(provider, model, usage, time) {
6
- if (provider !== 'deepseek-official' || !usage || !Number.isFinite(time)) return null;
21
+ if (!usage || !Number.isFinite(time)) return null;
22
+ if (provider === 'openrouter') return OPENROUTER_PRICES[model] ? charge(usage, OPENROUTER_PRICES[model]) : null;
23
+ if (provider !== 'deepseek-official') return null;
7
24
  // Earlier requests require an older price table; never back-price them at today's rate.
8
25
  if (time < Date.UTC(2026, 8, 11)) return null;
9
26
  const flash = ['deepseek-flash', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp'].includes(model)
10
27
  || model === 'deepseek-v4-pro' && time >= Date.UTC(2026, 8, 14, 4);
11
28
  if (!flash && model !== 'deepseek-v4-pro') return null;
12
- const date = new Date(time), day = date.getUTCDay(), hour = date.getUTCHours();
13
- const peak = day >= 1 && day <= 5 && (hour >= 1 && hour < 4 || hour >= 6 && hour < 10);
14
- const [read, input, output] = flash ? [0.003, 0.15, 0.6] : [0.022, 0.66, 1.98];
29
+ const cost = charge(usage, flash ? [0.003, 0.15, 0.6] : [0.022, 0.66, 1.98]);
30
+ return cost === null ? null : cost * (isPeak(time) ? 2 : 1);
31
+ }
32
+
33
+ function charge(usage, [read, input, output]) {
15
34
  const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens ?? 0, usage.cacheWriteTokens ?? 0];
16
35
  if (!values.every(n => Number.isFinite(n) && n >= 0) || values[3] !== 0) return null;
17
- return (values[0] * input + values[1] * output + values[2] * read) * (peak ? 2 : 1) / 1e6;
36
+ return (values[0] * input + values[1] * output + values[2] * read) / 1e6;
37
+ }
38
+
39
+ /**
40
+ * Peak/off-peak window in UTC: weekdays 01:00-04:00 and 06:00-10:00.
41
+ * @param time - Unix ms timestamp.
42
+ * @returns Whether that instant bills at the peak rate.
43
+ */
44
+ export function isPeak(time) {
45
+ const date = new Date(time), day = date.getUTCDay(), hour = date.getUTCHours();
46
+ return day >= 1 && day <= 5 && (hour >= 1 && hour < 4 || hour >= 6 && hour < 10);
47
+ }
48
+
49
+ /** Footer marker for the billing window: fire for peak, snowflake for off-peak. */
50
+ export function peakEmoji(time) {
51
+ return isPeak(time) ? '🔥' : '❄️';
18
52
  }
@@ -1,7 +1,9 @@
1
1
  import { readMetrics } from './store.mjs';
2
2
  import { t } from '../i18n/messages.mjs';
3
- import { estimateCost } from './pricing.mjs';
3
+ import { estimateCost, peakEmoji } from './pricing.mjs';
4
+ import { balanceNow, trustedNow } from './balance.mjs';
4
5
  import { sessionAverageTps } from './rate.mjs';
6
+ import { providerOfHeader } from '../providers/catalog.mjs';
5
7
  let source;
6
8
  export function setMetricSource(next) { source = next; return () => { if (source === next) source = undefined; }; }
7
9
  export function summarize(rows, events = [], corrupt = false) {
@@ -37,31 +39,50 @@ export function summarize(rows, events = [], corrupt = false) {
37
39
  }
38
40
  return { cost, unknown, calls, pending, cache: input > 0 && !cacheUnknown ? Math.min(100, hit / input * 100) : null };
39
41
  }
40
- /** Terminal columns of a string: East Asian wide characters (including the | separator) take two. */
42
+ /** Terminal columns of a string: East Asian wide characters (such as the cache label's CJK glyphs) take two. */
41
43
  export function displayWidth(text) {
42
44
  let width = 0;
43
- for (const char of text) width += /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]/.test(char) ? 2 : 1;
45
+ for (const char of text) width += /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6\u2600-\u27bf\u{1f300}-\u{1faff}]/u.test(char) ? 2 : 1;
44
46
  return width;
45
47
  }
46
- export function formatFooter(metrics, context, columns = 80, rates, locale = 'en') {
48
+ export function formatFooter(metrics, context, columns = 80, rates, locale = 'en', header = '', provider = providerOfHeader(header) ?? 'deepseek-official') {
47
49
  const label = key => t(locale, key);
48
50
  const ctx = Number.isFinite(context) ? `${Math.round(context)}%` : '--';
49
51
  const cache = metrics.cache === null ? '--' : `${metrics.cache.toFixed(1)}%`;
50
- const dollars = metrics.unknown && metrics.cost === 0 ? '--' : `~$${metrics.cost.toFixed(metrics.cost < 1 ? 4 : 2)}${metrics.unknown ? '+' : ''}${metrics.pending ? '…' : ''}`;
51
- const parts = rates ? [
52
+ // The balance belongs to the provider the header names; only DeepSeek's official route bills by a peak window.
53
+ const balance = balanceNow(provider);
54
+ const spend = metrics.unknown && metrics.cost === 0 ? '--' : `$${metrics.cost.toFixed(2)}${metrics.unknown ? '+' : ''}${metrics.pending ? '…' : ''}`;
55
+ const dollars = `${spend} / ${balance === null ? '$--' : '$' + balance.toFixed(2)}${provider === 'deepseek-official' ? ' ' + peakEmoji(trustedNow()) : ''}`;
56
+ const base = rates ? [
52
57
  `${label('footer.current')}: ${Number.isFinite(rates.current) ? '~' + rates.current.toFixed(1) : '--'} tps`,
53
58
  `${label('footer.average')}: ${Number.isFinite(rates.average) ? rates.average.toFixed(1) : '--'} tps`,
54
- `${label('footer.context')}: ${ctx}`, dollars, `${label('footer.cache')} ${cache}`,
55
- ] : [`${label('footer.context')}: ${ctx}`, dollars, `${label('footer.cache')} ${cache}`];
56
- for (let count = parts.length; count > 0; count--) {
57
- const value = parts.slice(0, count).join(' | ');
58
- if (displayWidth(value) <= columns) return value;
59
+ `${label('footer.context')}: ${ctx}`, dollars, `${label('footer.cache')}: ${cache}`,
60
+ ] : [`${label('footer.context')}: ${ctx}`, dollars, `${label('footer.cache')}: ${cache}`];
61
+ // Narrow terminals shed the quietest figures first: average, cache hit, current. The
62
+ // model header then falls back to its bare `model @ effort` form, then context goes,
63
+ // and only then the header itself — the running cost is the last thing standing.
64
+ const drops = rates ? [1, 4, 0, 2] : [2, 0];
65
+ const offset = header === '' ? 0 : 1;
66
+ const parts = header === '' ? base : [header, ...base];
67
+ const short = header.replace(/^[^:]+: /, '');
68
+ const heads = header === '' ? [''] : short === header ? [header] : [header, short];
69
+ const render = ({ omit, head }) => parts
70
+ .map((part, index) => (index === 0 && header !== '' ? head : part))
71
+ .filter((part, index) => part !== '' && !omit.has(index))
72
+ .join(' | ');
73
+ for (let dropped = 0; dropped <= drops.length; dropped++) {
74
+ const omit = new Set(drops.slice(0, dropped).map(index => index + offset));
75
+ for (const head of heads) {
76
+ const value = render({ omit, head });
77
+ if (displayWidth(value) <= columns) return value;
78
+ }
59
79
  }
80
+ const floor = render({ omit: new Set(drops.map(index => index + offset)), head: '' });
60
81
  let clipped = '';
61
- for (const char of parts[0]) { if (displayWidth(clipped + char) > columns) break; clipped += char; }
82
+ for (const char of floor) { if (displayWidth(clipped + char) > columns) break; clipped += char; }
62
83
  return clipped;
63
84
  }
64
- export function footerFor(id, stats, columns, locale = 'en') {
85
+ export function footerFor(id, stats, columns, header = '', locale = 'en') {
65
86
  try {
66
87
  const data = id ? source?.(id) : undefined;
67
88
  const ledger = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id) : { rows: [], corrupt: false };
@@ -69,6 +90,6 @@ export function footerFor(id, stats, columns, locale = 'en') {
69
90
  const used = data?.used;
70
91
  const capacity = data?.capacity ?? stats.contextWindow;
71
92
  const average = sessionAverageTps(data?.events ?? []);
72
- return formatFooter(summary, Number.isFinite(used) && capacity > 0 ? used / capacity * 100 : undefined, columns, { current: data?.currentTps, average }, locale);
73
- } catch { return formatFooter({ cost: 0, unknown: true, cache: null }, undefined, columns, { current: null, average: null }, locale); }
93
+ return formatFooter(summary, Number.isFinite(used) && capacity > 0 ? used / capacity * 100 : undefined, columns, { current: data?.currentTps, average }, locale, header);
94
+ } catch { return formatFooter({ cost: 0, unknown: true, cache: null }, undefined, columns, { current: null, average: null }, locale, header); }
74
95
  }
@@ -4,6 +4,7 @@ import { Logger } from '@deepseek-ai/cordis';
4
4
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
5
5
  import { redact } from '../auto-review/policy.mjs';
6
6
  import { t, readLanguage } from '../i18n/messages.mjs';
7
+ import { chargeTo } from '../session-metrics/attribution.mjs';
7
8
  const L = (key, params) => t(readLanguage(), key, params);
8
9
 
9
10
  const MAX_LOG_BYTES = 1024 * 1024;
@@ -118,7 +119,7 @@ export function localDoctorReport(evidence) {
118
119
  return `${L('doctor.evidence', { traces: evidence.traces.length, logs: evidence.logs.length })}${evidence.logs.length ? '' : evidence.logCoverage ?? ''}\n${local.length ? local.slice(-8).join('\n') : L('doctor.noFindings')}\n${near300.length >= 2 ? `${L('doctor.nearTimeout', { count: near300.length })}\n` : ''}${evidence.logs.slice(-5).map(l => `${time(l.time)} ${l.level} ${l.source}: ${l.detail}`).join('\n')}`;
119
120
  }
120
121
 
121
- export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { model = true } = {}) {
122
+ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { model = true, sessionId } = {}) {
122
123
  const fallback = localDoctorReport(evidence);
123
124
  if (!model) return fallback;
124
125
  if (!route?.provider || !route?.model) return `${fallback}\n${L('doctor.noRoute')}`;
@@ -126,11 +127,13 @@ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { mode
126
127
  const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(45000)]);
127
128
  try {
128
129
  let finished = false;
129
- for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, reasoningEffort: 'low', maxTokens: 4096, system: SYSTEM,
130
- messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
131
- deadline.throwIfAborted(); assembler.push(chunk);
132
- if (chunk.type === 'finish') finished = true;
133
- }
130
+ await chargeTo(sessionId, 'doctor', async () => {
131
+ for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, reasoningEffort: 'low', maxTokens: 4096, system: SYSTEM,
132
+ messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
133
+ deadline.throwIfAborted(); assembler.push(chunk);
134
+ if (chunk.type === 'finish') finished = true;
135
+ }
136
+ });
134
137
  if (!finished || assembler.finish.kind !== 'stop') throw Error(`model response was incomplete (${safe(JSON.stringify(assembler.finish ?? { kind: 'no finish' }))})`);
135
138
  const blocks = assembler.blocks();
136
139
  if (blocks.some(b => !['text', 'reasoning'].includes(b.type))) throw Error('model returned unexpected tool output');
@@ -92,7 +92,7 @@ export function apply(ctx) {
92
92
  `Session: ${session.id}`, `Workspace: ${session.header.cwd ?? process.cwd()}`,
93
93
  `Agent: ${agent.status} | preset: ${session.header.agentPreset ?? 'standard'}`,
94
94
  `Model: ${route?.provider ?? 'default'} / ${route?.model ?? 'default'}`,
95
- `Effort: ${route?.reasoningEffort ?? 'model default'}${route?.reasoningEffort === 'ultra' ? ' (DeepSeek wire: max; collaboration enabled)' : ''}`,
95
+ `Effort: ${route?.reasoningEffort ?? 'model default'}${route?.reasoningEffort === 'ultra' ? ` (${route.provider === 'openrouter' ? 'OpenRouter wire: xhigh' : 'DeepSeek wire: max'}; collaboration enabled)` : ''}`,
96
96
  `Permission: ${show(ctx.permissionPresets.current(session))}`,
97
97
  `Tokens: ${show(usage ?? 'no provider usage yet')}`,
98
98
  `Context: ${pressure?.pressureTokens ?? pressure?.surfaceTokens ?? '?'} / ${pressure?.contextWindow ?? '?'} tokens`,
@@ -120,7 +120,7 @@ export function apply(ctx) {
120
120
  const evidence = await collectDoctorEvidence(ctx, { agent, signal });
121
121
  if (action === 'preview') return ok(JSON.stringify(evidence, null, 2));
122
122
  const route = agent.session.requestHeader()?.config ?? agent.options;
123
- return ok(`${health}\n\n${await analyzeDoctorEvidence(ctx, evidence, route, signal, { model: action !== 'local' })}`);
123
+ return ok(`${health}\n\n${await analyzeDoctorEvidence(ctx, evidence, route, signal, { model: action !== 'local', sessionId: agent.session.id })}`);
124
124
  });
125
125
  register('mcp', 'MCP list, tools <id>, enable/disable/reconnect <id>', async ({ agent, rawInput }) => {
126
126
  const [action = 'list', id, extra] = rawInput.trim().split(/\s+/).filter(Boolean);
@@ -16,6 +16,22 @@ export function flashRequest(options, messages) {
16
16
  return copy;
17
17
  }
18
18
 
19
+ /**
20
+ * The same shaping for pi-ai routes (OpenRouter), on harness-format options:
21
+ * Ultra sends max and adds the collaboration policy; below Ultra the delegation
22
+ * tools are not offered. Returns a copy; the logged input is never mutated.
23
+ */
24
+ export function piAiRequest(options) {
25
+ const ultra = options.reasoningEffort === 'ultra';
26
+ const hidden = ultra ? ['workflow', 'ralph'] : ['subagent', 'subagent_fork', 'workflow', 'ralph'];
27
+ const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => !hidden.includes(tool.name)) } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
28
+ if (!ultra || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return next;
29
+ if (typeof next.system === 'string') return { ...next, system: next.system + '\n\n' + ULTRA_POLICY };
30
+ const [first, ...rest] = next.messages ?? [];
31
+ if (first?.role === 'system' && Array.isArray(first.content)) return { ...next, messages: [{ ...first, content: [...first.content, { type: 'text', text: '\n\n' + ULTRA_POLICY }] }, ...rest] };
32
+ return { ...next, messages: [{ role: 'system', content: [{ type: 'text', text: ULTRA_POLICY }] }, ...(next.messages ?? [])] };
33
+ }
34
+
19
35
  export function ultraRequest(options, messages) {
20
36
  if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return messages;
21
37
  const copy = messages.map(m => ({ ...m }));
@@ -1,3 +1,6 @@
1
+ // dscode-stdin-stall-v2
2
+ const STDIN_STALL_MS = 5000;
3
+ const STDIN_STALL_NOTE = "\n[Interrupted after 5s with no output: the command held the terminal without producing anything, and this tool cannot supply terminal input. Give it stdin (a heredoc, `< file` or `< /dev/null`) or use a non-interactive form.]";
1
4
  // dscode-shell-reset-v1
2
5
  import { randomUUID } from "node:crypto";
3
6
  import z from "@deepseek-ai/schemastery";
@@ -255,6 +258,9 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
255
258
  const id = await shells.get(owner, commandDeadline.signal);
256
259
  const marker = markers();
257
260
  const wrapped = wrapCommand(command, marker);
261
+ const startedAt = Date.now();
262
+ let stdinWaited = false;
263
+ let stdinStalled = false;
258
264
  let first = true;
259
265
  let fallback = "";
260
266
  let fallbackTruncated = false;
@@ -298,10 +304,21 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
298
304
  }
299
305
  if (latest.text.includes(marker.end)) {
300
306
  const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker);
301
- if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars);
307
+ if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars) + (stdinStalled ? STDIN_STALL_NOTE : "");
302
308
  }
303
309
  if (result.sessionStatus.kind === "exited") return await respondToSessionExit(ctx, shells, owner, id, result.sessionStatus, marker, fallback, fallbackTruncated, config);
304
- if (result.waitReason === "stdin_read") return renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
310
+ const partial = renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
311
+ if (result.waitReason === "stdin_read") stdinWaited = true;
312
+ if (result.waitReason === "stdin_read" && partial.length > 0) return partial;
313
+ if (stdinWaited && partial.length === 0 && !stdinStalled && Date.now() - startedAt >= STDIN_STALL_MS) {
314
+ stdinStalled = true;
315
+ try {
316
+ await ctx.terminals.signal(owner, id, "SIGINT");
317
+ } catch (_unsignallableForeground) {
318
+ stdinStalled = false;
319
+ return partial;
320
+ }
321
+ }
305
322
  await pause();
306
323
  }
307
324
  } catch (e_1) {
@@ -0,0 +1,21 @@
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.