@toddzheng024/dscode-bundle 0.7.26 → 0.7.28

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 (99) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +10 -10
  3. package/bootstrap.mjs +1 -1
  4. package/cordis.patch.yml +418 -45
  5. package/package.json +279 -245
  6. package/plugins/auto-review/index.mjs +8 -5
  7. package/plugins/code-review/index.mjs +1 -1
  8. package/plugins/computer-use/index.mjs +37 -0
  9. package/plugins/cordis-config/plain.mjs +31 -0
  10. package/plugins/exec/cli.mjs +1 -1
  11. package/plugins/grok/index.mjs +28 -24
  12. package/plugins/memory/index.mjs +1 -1
  13. package/plugins/message-source/kind.mjs +20 -0
  14. package/plugins/openrouter/adapter.mjs +29 -13
  15. package/plugins/openrouter/index.mjs +31 -28
  16. package/plugins/session-bridge/communication.mjs +6 -3
  17. package/plugins/session-bridge/server.mjs +3 -2
  18. package/plugins/session-bridge/tasks.mjs +3 -1
  19. package/plugins/session-cards/index.mjs +1 -1
  20. package/plugins/time-marks/index.mjs +3 -2
  21. package/plugins/time-marks/marks.mjs +6 -5
  22. package/plugins/triggers/config.mjs +1 -1
  23. package/plugins/triggers/tools.mjs +1 -1
  24. package/plugins/tui-tools/doctor.mjs +1 -1
  25. package/plugins/tui-tools/index.mjs +16 -4
  26. package/plugins/ultra/policy.mjs +16 -0
  27. package/vendor/bash/index.js +455 -186
  28. package/vendor/bash/types/background.d.ts +38 -7
  29. package/vendor/bash/types/index.d.ts +20 -4
  30. package/vendor/bash/types/render.d.ts +28 -9
  31. package/vendor/command-goal/index.js +3 -1
  32. package/vendor/deepseek/index.js +1378 -1246
  33. package/vendor/deepseek/types/adapter.d.ts +16 -155
  34. package/vendor/deepseek/types/config.d.ts +127 -0
  35. package/vendor/deepseek/types/defaults.d.ts +24 -0
  36. package/vendor/deepseek/types/file-store.d.ts +5 -3
  37. package/vendor/deepseek/types/files-api.d.ts +12 -9
  38. package/vendor/deepseek/types/image-tokens.d.ts +24 -9
  39. package/vendor/deepseek/types/images.d.ts +35 -0
  40. package/vendor/deepseek/types/index.d.ts +10 -94
  41. package/vendor/deepseek/types/messages-api.d.ts +10 -0
  42. package/vendor/deepseek/types/model-info.d.ts +16 -0
  43. package/vendor/deepseek/types/models.d.ts +4 -0
  44. package/vendor/deepseek/types/replay.d.ts +27 -0
  45. package/vendor/deepseek/types/request-extensions.d.ts +16 -0
  46. package/vendor/deepseek/types/request-files.d.ts +49 -0
  47. package/vendor/deepseek/types/request-pricing.d.ts +32 -20
  48. package/vendor/deepseek/types/serialize.d.ts +17 -82
  49. package/vendor/deepseek/types/sse.d.ts +6 -22
  50. package/vendor/deepseek/types/translate.d.ts +13 -36
  51. package/vendor/deepseek/types/transport.d.ts +15 -0
  52. package/vendor/deepseek/types/types.d.ts +106 -167
  53. package/vendor/deepseek/types/wire-types.d.ts +62 -0
  54. package/vendor/persistent/index.js +9 -3
  55. package/vendor/subagent/index.js +11 -8
  56. package/vendor/subagent/model-selection-settings.js +12 -38
  57. package/vendor/subagent/types/index.d.ts +4 -3
  58. package/vendor/subagent/types/model-selection-settings.d.ts +12 -10
  59. package/vendor/subagent-core/index.js +401 -264
  60. package/vendor/subagent-core/typert.host.js +103 -113
  61. package/vendor/subagent-core/typert.remote-client.d.ts +1 -3
  62. package/vendor/subagent-core/typert.remote-client.js +23 -69
  63. package/vendor/subagent-core/types/archive-admission.d.ts +16 -0
  64. package/vendor/subagent-core/types/archive-admission.js +151 -0
  65. package/vendor/subagent-core/types/assistant-output.d.ts +2 -2
  66. package/vendor/subagent-core/types/catalog.d.ts +17 -10
  67. package/vendor/subagent-core/types/catalog.js +12 -6
  68. package/vendor/subagent-core/types/child-agent.d.ts +8 -5
  69. package/vendor/subagent-core/types/child-agent.js +11 -5
  70. package/vendor/subagent-core/types/continuation-activation.d.ts +17 -1
  71. package/vendor/subagent-core/types/continuation-activation.js +41 -4
  72. package/vendor/subagent-core/types/continuation-messages.d.ts +1 -1
  73. package/vendor/subagent-core/types/continuation-messages.js +7 -3
  74. package/vendor/subagent-core/types/continuation.d.ts +1 -1
  75. package/vendor/subagent-core/types/continuation.js +2 -2
  76. package/vendor/subagent-core/types/control-types.d.ts +29 -36
  77. package/vendor/subagent-core/types/control-types.js +2 -3
  78. package/vendor/subagent-core/types/control.d.ts +2 -28
  79. package/vendor/subagent-core/types/control.js +3 -40
  80. package/vendor/subagent-core/types/index.d.ts +45 -41
  81. package/vendor/subagent-core/types/index.js +46 -82
  82. package/vendor/subagent-core/types/internal.d.ts +2 -2
  83. package/vendor/subagent-core/types/internal.js +2 -2
  84. package/vendor/subagent-core/types/lifecycle.d.ts +1 -1
  85. package/vendor/subagent-core/types/lifecycle.js +5 -3
  86. package/vendor/subagent-core/types/list-children.d.ts +15 -32
  87. package/vendor/subagent-core/types/list-children.js +45 -49
  88. package/vendor/subagent-core/types/out-of-process.d.ts +1 -1
  89. package/vendor/subagent-core/types/projection-types.d.ts +9 -1
  90. package/vendor/subagent-core/types/projection.d.ts +5 -0
  91. package/vendor/subagent-core/types/projection.js +10 -4
  92. package/vendor/subagent-core/types/run-settlement.js +1 -1
  93. package/vendor/subagent-core/types/types.d.ts +2 -2
  94. package/vendor/terminal/index.js +94 -30
  95. package/vendor/tui/lib/index.mjs +2 -1
  96. package/vendor/tui/lib/kernel-panels.mjs +1 -1
  97. package/vendor/tui/lib/render/projection.mjs +50 -36
  98. package/presets/dscode/agent.cordis.yml +0 -321
  99. package/presets/dscode/preset.yml +0 -3
@@ -24,6 +24,9 @@ export function apply(ctx, config) {
24
24
  const calls = new WeakMap();
25
25
  const budgets = new WeakMap();
26
26
  const mode = agent => ctx.permissionPresets.current(agent.session);
27
+ // The preset this plugin reviews for. DSH 0.1.7 reserved `auto` for its own
28
+ // integration, so the reviewed preset carries DSCODE's own name.
29
+ const REVIEWED = 'auto-review';
27
30
  const stateFor = agent => {
28
31
  const events = agent.session.snapshotEvents();
29
32
  const turn = events.findLast(e => e.type === 'turn/start')?.seq;
@@ -54,7 +57,7 @@ export function apply(ctx, config) {
54
57
  });
55
58
 
56
59
  const announce = (agent, text) => agent.inject(createUserMessage({
57
- content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: name },
60
+ content: [{ type: 'text', text }], source: { kind: name },
58
61
  }));
59
62
  const record = (req, data) => {
60
63
  audit.append(req.agent.session.id, { sessionSeq: req.agent.session.seq, callId: req.callId ?? null, toolName: req.toolName, ...data });
@@ -64,7 +67,7 @@ export function apply(ctx, config) {
64
67
  async function review(req, next, state) {
65
68
  if (req.signal?.aborted) return 'cancelled';
66
69
  // A user can switch to ask while a prior review waits in the queue.
67
- if (mode(req.agent) !== 'auto') return ctx.approval?.effectivePolicy?.(req.agent.session) === 'never' ? 'rejected' : next();
70
+ if (mode(req.agent) !== REVIEWED) return ctx.approval?.effectivePolicy?.(req.agent.session) === 'never' ? 'rejected' : next();
68
71
  const exec = calls.get(req.agent)?.get(req.callId);
69
72
  const fallback = async (reason, details = {}) => {
70
73
  record(req, { decision: 'human', reason, ...details });
@@ -82,7 +85,7 @@ export function apply(ctx, config) {
82
85
  record(req, { decision: 'cancelled', reason: 'Caller cancelled review.', ...details });
83
86
  return 'cancelled';
84
87
  }
85
- if (mode(req.agent) !== 'auto') return fallback('Permission mode changed while review was pending.', details);
88
+ if (mode(req.agent) !== REVIEWED) return fallback('Permission mode changed while review was pending.', details);
86
89
  if (decision.decision === 'human') return fallback(decision.reason, details);
87
90
  // Bind approval to the still-pending immutable invocation; no grant cache.
88
91
  if (calls.get(req.agent)?.get(req.callId) !== exec || fingerprint(action) !== actionHash) return fallback('Pending action changed during review.', details);
@@ -174,7 +177,7 @@ export function apply(ctx, config) {
174
177
  ...(effort === undefined ? {} : { reasoningEffort: effort }),
175
178
  messages: [createSystemMessage(REVIEW_POLICY, name), createUserMessage({
176
179
  content: [{ type: 'text', text: JSON.stringify({ action, context }) }],
177
- source: { kind: 'plugin', plugin: name },
180
+ source: { kind: name },
178
181
  })],
179
182
  maxTokens: config.maxOutputTokens, signal,
180
183
  })) {
@@ -231,7 +234,7 @@ export function apply(ctx, config) {
231
234
  }
232
235
  announce(req.agent, `Escalation budget reached (${grantBudget} per turn); asking the user.`);
233
236
  }
234
- if (mode(req.agent) !== 'auto') return next();
237
+ if (mode(req.agent) !== REVIEWED) return next();
235
238
  const state = stateFor(req.agent);
236
239
  // Serialize per agent so simultaneous calls cannot race the denial budget.
237
240
  const pending = state.tail.then(() => review(req, next, state));
@@ -141,7 +141,7 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
141
141
  for await (const chunk of ctx.llm.stream({
142
142
  provider: route.provider, model: route.model, reasoningEffort, purpose: 'review',
143
143
  maxTokens: tokens,
144
- system: POLICY, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(request) }], source: { kind: 'plugin', plugin: name } })], signal: deadline,
144
+ system: POLICY, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(request) }], source: { kind: name } })], signal: deadline,
145
145
  })) {
146
146
  deadline.throwIfAborted();
147
147
  assembler.push(chunk);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Computer Use, mounted through DSCODE so a retired settings API still resolves.
3
+ *
4
+ * `@anionex/dsh-computer-use` reads its configuration with `ctx.settings.register`, an API
5
+ * DSH removed in 0.1.7: Settings now projects forms from the profile entry itself, and the
6
+ * missing method made the whole Computer Use bundle fail to activate. The package is
7
+ * pinned at 0.3.2 with no newer release, so this row restores the method — as a read-only
8
+ * view over the composition config, which is where DSCODE configures Computer Use anyway —
9
+ * and then mounts the upstream bundle as its child, which is what makes the order
10
+ * dependable: the shim is installed before the provider's constructor runs.
11
+ *
12
+ * Retire this wrapper when the package reads volatile configuration, and mount
13
+ * `@anionex/dsh-computer-use` directly again.
14
+ *
15
+ * @module dscode-computer-use
16
+ */
17
+
18
+ import ComputerUseBundle from '@anionex/dsh-computer-use';
19
+
20
+ export const name = 'dscode-computer-use';
21
+ export const inject = ['settings'];
22
+
23
+ /** Section handle the 0.3.x provider expects: one read of the composition config, no live writes. */
24
+ function staticSection(base) {
25
+ return { get: () => base, watch: () => () => {} };
26
+ }
27
+
28
+ export function apply(ctx, config = {}) {
29
+ const settings = ctx.settings;
30
+ // Only ever adds what upstream no longer defines; a Settings service that carries its
31
+ // own `register` again is left untouched, and the shim is dropped with this row.
32
+ if (typeof settings.register !== 'function') {
33
+ settings.register = (_namespace, _schema, options = {}) => staticSection(options.base ?? {});
34
+ ctx.effect(() => () => { delete settings.register; });
35
+ }
36
+ ctx.plugin(ComputerUseBundle, config);
37
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * One plain snapshot of a Cordis plugin configuration.
3
+ *
4
+ * Two shapes make a foreign entry's `config` unusable as a plain object. DSH 0.1.7 hands
5
+ * a `.volatile()` field to its own plugin as a reference rather than a value, and the
6
+ * Loader keeps a YAML `!!js` scalar as an unevaluated `{ __jsExpr }` node until the entry
7
+ * activates — which a reader inspecting another entry's row (the skills conflict report
8
+ * walks every `skill-filesystem` entry, and a preset's rows carry such expressions) sees
9
+ * instead of the value. References are read; an expression cannot be evaluated outside the
10
+ * Loader's own scope, so it reads as absent and the caller applies its own fallback.
11
+ *
12
+ * @param config - a configuration object whose fields may be references or expressions.
13
+ * @returns a shallow copy carrying only plainly readable values.
14
+ */
15
+ export function plainConfig(config) {
16
+ if (config === null || typeof config !== 'object') return {};
17
+ const entries = Object.entries(config)
18
+ .map(([key, value]) => [key, isReference(value) ? value.get() : value])
19
+ .filter(([, value]) => !isExpression(value));
20
+ return Object.fromEntries(entries);
21
+ }
22
+
23
+ /** Whether a configuration field is a Cordis value reference rather than a plain value. */
24
+ function isReference(value) {
25
+ return value !== null && typeof value === 'object' && typeof value.get === 'function';
26
+ }
27
+
28
+ /** Whether a configuration field is still an unevaluated Loader expression node. */
29
+ function isExpression(value) {
30
+ return value !== null && typeof value === 'object' && '__jsExpr' in value;
31
+ }
@@ -9,7 +9,7 @@ Options:
9
9
  --cwd DIR workspace for the agent (default: current directory)
10
10
  --model PROVIDER/ID model route (default: the saved default model)
11
11
  --effort LEVEL reasoning effort the model offers: off, minimal, low, medium, high, xhigh, max or ultra
12
- --permission PRESET permission preset: auto, ask, workspace-write, read-only, danger-full-access
12
+ --permission PRESET permission preset: auto-review, ask, workspace-write, read-only, danger-full-access
13
13
  --approve-all answer every approval request with allow (no human is present)
14
14
  --resume SESSION_ID continue an existing session instead of starting a new one
15
15
  --json emit JSON lines (session, text, tool, result) instead of plain text
@@ -1,6 +1,8 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { RetryPolicySchema, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
3
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
+ import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
5
+ import { plainConfig } from "../cordis-config/plain.mjs";
4
6
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
5
7
  import { GrokAdapter, PROVIDER } from "./adapter.mjs";
6
8
  import { ensureGrokModels } from "./models.mjs";
@@ -19,14 +21,18 @@ const DEFAULT_BASE_URL = "https://api.x.ai/v1";
19
21
  /** The client version the CLI proxy is asked with; xAI refuses requests without one. */
20
22
  const DEFAULT_CLIENT_VERSION = "1.0.34";
21
23
 
24
+ // DSH 0.1.7 replaced settings sections with volatile configuration: each `.volatile()`
25
+ // field arrives as a reference the Settings form and a profile edit both write, so the
26
+ // reads below always see the current value.
22
27
  export const Config = z.object({
23
- apiKeyEnv: z.string().role("credential-ref").default(GROK_TOKEN_REF),
24
- baseURL: z.string().default(DEFAULT_BASE_URL),
25
- clientVersion: z.string().default(DEFAULT_CLIENT_VERSION),
26
- streamIdleTimeoutMs: z.number().min(1).default(300000),
27
- retryPolicy: RetryPolicySchema,
28
+ apiKeyEnv: z.string().role("credential-ref").default(GROK_TOKEN_REF).volatile(),
29
+ baseURL: z.string().default(DEFAULT_BASE_URL).volatile(),
30
+ clientVersion: z.string().default(DEFAULT_CLIENT_VERSION).volatile(),
31
+ streamIdleTimeoutMs: z.number().min(1).default(300000).volatile(),
32
+ retryPolicy: RetryPolicySchema.volatile(),
28
33
  });
29
34
 
35
+
30
36
  /** Validated connection facts from one config snapshot. */
31
37
  export function resolveOptions(config = {}) {
32
38
  const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? 300000;
@@ -50,18 +56,18 @@ export function loginHint(state) {
50
56
  }
51
57
 
52
58
  export function apply(ctx, config = {}) {
53
- let current = () => config, lastRaw, lastGood;
59
+ // The TUI renders this route's own page, so Settings must not generate a form for it.
60
+ ctx.inject(["settings"], settingsCtx => {
61
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
62
+ });
63
+ let lastGood;
54
64
  const options = () => {
55
- const raw = current();
56
- if (raw === lastRaw && lastGood !== undefined) return lastGood;
57
65
  try {
58
- lastGood = resolveOptions(raw);
59
- lastRaw = raw;
66
+ lastGood = resolveOptions(plainConfig(config));
60
67
  return lastGood;
61
68
  } catch (error) {
62
69
  if (lastGood === undefined) throw error;
63
- lastRaw = raw;
64
- ctx.logger.error(name + ": keeping the last good configuration after an invalid settings section");
70
+ ctx.logger.error(name + ": keeping the last good configuration after an invalid edit");
65
71
  return lastGood;
66
72
  }
67
73
  };
@@ -89,19 +95,17 @@ export function apply(ctx, config = {}) {
89
95
  await ensureGrokModels({ home, token, version: options().clientVersion });
90
96
  };
91
97
  const adapter = new GrokAdapter({ options, ensureModels, resolveToken });
92
- ctx.llm.registerConfigurableProviders([{ provider: PROVIDER, displayName: "Grok", settingsNs: NS, settingsPath: [] }]);
98
+ ctx.llm.registerConfigurableProviders([{ provider: PROVIDER, displayName: "Grok", settingsNs: ctx.fiber.entry?.options.id ?? NS, settingsPath: [] }]);
93
99
  const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
94
- let registeredPolicy = JSON.stringify(options().retryPolicy);
95
- ctx.inject(["settings"], settingsCtx => {
96
- settingsCtx.settings.installSection(ctx, NS, Config, config, {
97
- setSource: source => { current = source; },
98
- onChange: () => {
99
- const policy = JSON.stringify(options().retryPolicy);
100
- if (policy === registeredPolicy) return;
101
- registration.replace([PROVIDER]);
102
- registeredPolicy = policy;
103
- },
104
- });
100
+ // The retry policy is captured at registration, and a reference carries no change
101
+ // callback: re-register the route when an edit lands on a different policy.
102
+ let registeredPolicy = options().retryPolicy;
103
+ ctx.on("loader/volatile-update", () => {
104
+ let policy;
105
+ try { policy = options().retryPolicy; } catch (error) { ctx.logger.warn(error); return; }
106
+ if (deepEqualJson(policy, registeredPolicy)) return;
107
+ registration.replace([PROVIDER]);
108
+ registeredPolicy = policy;
105
109
  });
106
110
  // Warm the catalog and the weekly window for a user who already ran `grok login`, then keep
107
111
  // the window fresh at the interval the official CLI itself watches (subscription_watch_interval_secs).
@@ -48,7 +48,7 @@ export function apply(ctx, options = {}) {
48
48
  await chargeTo(live.has(lastSession) ? lastSession : undefined, 'memory', async () => {
49
49
  for await (const chunk of ctx.llm.stream({
50
50
  ...target, ...(reasoningEffort ? { reasoningEffort } : {}),
51
- system, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
51
+ system, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: name } })],
52
52
  maxTokens: 12000, signal: deadline,
53
53
  })) {
54
54
  deadline.throwIfAborted(); assembler.push(chunk);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Producer identity of one message source.
3
+ *
4
+ * DSH 0.1.7 retired the shared `plugin` kind: every producer now declares its own
5
+ * `kind`, and DSCODE's plugins name themselves the same way. Durable history carries two
6
+ * older shapes — the raw `{ kind: 'plugin', plugin: '<producer>' }` of a session written
7
+ * before the upgrade, and the `plugin:<producer>` kind the session format's v3-to-v4
8
+ * migration writes for a producer it does not know — so every reader of history resolves
9
+ * all three through this one function.
10
+ *
11
+ * @param source - a message source from any release.
12
+ * @returns the producing subsystem's name, or `''` when the source names none.
13
+ */
14
+ export function producerKind(source) {
15
+ if (source === null || typeof source !== 'object') return '';
16
+ const { kind, plugin } = source;
17
+ if (kind === 'plugin') return typeof plugin === 'string' ? plugin : '';
18
+ if (typeof kind !== 'string') return '';
19
+ return kind.startsWith('plugin:') ? kind.slice('plugin:'.length) : kind;
20
+ }
@@ -1,4 +1,4 @@
1
- import { LlmAdapter, LlmError, ReasoningEffortId, attributionHeaders, contentHasImage, offloadRequestImagesWithPolicy, offloadedImageText } from '@deepseek-ai/dsh-llm';
1
+ import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ReasoningEffortId, attributionHeaders, contentHasImage, offloadedImageText, projectOffloadedImages, requiredImageOffload } from '@deepseek-ai/dsh-llm';
2
2
  import { listOpenRouterModels, openRouterModel } from './models.mjs';
3
3
  import { PROVIDER, effortInfo, errorCode, errorMessage, modelReasoning, requestBody, retryAfterMs, sseData, translate } from './wire.mjs';
4
4
 
@@ -157,22 +157,38 @@ export class OpenRouterAdapter extends LlmAdapter {
157
157
  }
158
158
  }
159
159
 
160
- /** Read request versions of every image, oldest beyond the byte budget replaced by text first. */
160
+ /**
161
+ * Read request versions of every retained image.
162
+ *
163
+ * DSH 0.1.7 made the offloaded set a durable surface fact: a route projects what the
164
+ * surface already offloaded into text and reports how much more must go with
165
+ * `IMAGE_OFFLOAD_REQUIRED`, instead of dropping the oldest occurrences itself. The
166
+ * `compaction-image-offload` executor records that choice and retries, so the decision
167
+ * survives a route change, resume and replay rather than being remade per request.
168
+ */
161
169
  async prepareImages(options, entry, connection, signal) {
162
- if (!options.messages.some(message => contentHasImage(message.content))) return undefined;
163
- if (!entry?.inputModalities?.includes('image')) throw new LlmError(`OpenRouter model "${options.model}" does not accept image input.`, 'UNSUPPORTED_CONTENT');
164
170
  const attachments = this.config.resolveAttachments?.();
171
+ const access = ref => (attachments === undefined ? undefined : this.config.resolveImageAccess?.(attachments, ref));
172
+ const messages = projectOffloadedImages(options.messages, ref => offloadedImageText(ref, access(ref)));
173
+ const withImages = messages.some(message => contentHasImage(message.content));
174
+ if (!withImages) return messages === options.messages ? undefined : { options: { ...options, messages: [...messages] }, versions: new Map(), access };
175
+ if (!entry?.inputModalities?.includes('image')) throw new LlmError(`OpenRouter model "${options.model}" does not accept image input.`, 'UNSUPPORTED_CONTENT');
165
176
  if (attachments === undefined) throw new LlmError('OpenRouter image input requires the durable attachment service.', 'UNSUPPORTED_CONTENT');
166
- const access = ref => this.config.resolveImageAccess?.(attachments, ref);
167
- const bounded = policyBytes => offloadRequestImagesWithPolicy(policyBytes.messages, {
168
- representation: 'base64', maxBytes: connection.maxRequestImageBytes, byteQuantum: 1,
169
- byteLength: policyBytes.byteLength, placeholder: ref => offloadedImageText(ref, access(ref)),
170
- });
171
- const estimated = bounded({ messages: options.messages, byteLength: ref => Math.min(ref.bytes, IMAGE_POLICY.maxBytes) });
172
177
  const refs = new Map();
173
- for (const message of estimated) collectImages(message.content, refs);
178
+ for (const message of messages) collectImages(message.content, refs);
174
179
  const versions = new Map(await Promise.all([...refs.values()].map(async ref => [ref.attachmentId, await attachments.readImageRequest(ref, IMAGE_POLICY, signal)])));
175
- const exact = bounded({ messages: estimated, byteLength: ref => versions.get(ref.attachmentId).bytes });
176
- return { options: { ...options, messages: [...exact] }, versions, access };
180
+ const offloadImages = requiredImageOffload(
181
+ messages,
182
+ { representation: 'base64', maxBytes: connection.maxRequestImageBytes, byteQuantum: 1 },
183
+ block => versions.get(block.attachment.attachmentId).bytes,
184
+ );
185
+ if (offloadImages > 0) {
186
+ throw new LlmError(
187
+ `OpenRouter request images exceed ${connection.maxRequestImageBytes} bytes; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
188
+ IMAGE_OFFLOAD_REQUIRED_CODE,
189
+ { offloadImages },
190
+ );
191
+ }
192
+ return { options: { ...options, messages: [...messages] }, versions, access };
177
193
  }
178
194
  }
@@ -1,28 +1,34 @@
1
1
  import z from '@deepseek-ai/schemastery';
2
2
  import { LlmError, RetryPolicySchema, assertUsableApiKey, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
3
3
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
4
+ import { deepEqualJson } from '@deepseek-ai/dsh-util-values';
5
+ import { plainConfig } from '../cordis-config/plain.mjs';
4
6
  import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
5
7
  import { OpenRouterAdapter, PROVIDER } from './adapter.mjs';
6
8
  import { ensureOpenRouterModels } from './models.mjs';
7
9
  import { OpenRouterSearchProvider, RoutedSearchProvider } from './search.mjs';
8
10
 
9
11
  // The `openrouter` route: DSCODE's own OpenRouter adapter over its live model
10
- // listing, configured by the `llm-openrouter` settings section, plus web search
11
- // that follows the session's route.
12
+ // listing, configured by this entry's own profile row, plus web search that
13
+ // follows the session's route.
12
14
  export const name = 'dscode-openrouter';
13
15
  export const inject = ['llm'];
14
16
  const NS = 'llm-openrouter';
15
17
  const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1';
16
18
 
19
+ // DSH 0.1.7 replaced settings sections with volatile configuration: a field marked
20
+ // `.volatile()` reaches the plugin as a reference the Settings form and a profile edit
21
+ // both write, so each read below sees the current value without a change callback.
17
22
  export const Config = z.object({
18
- apiKeyEnv: z.string().role('credential-ref').default('OPENROUTER_API_KEY'),
19
- baseURL: z.string().default(DEFAULT_BASE_URL),
20
- streamIdleTimeoutMs: z.number().min(1).default(300000),
21
- maxRequestImageBytes: z.number().step(1).min(1).default(20 * 1024 * 1024),
22
- searchModel: z.string().default('deepseek/deepseek-v4-flash'),
23
- retryPolicy: RetryPolicySchema,
23
+ apiKeyEnv: z.string().role('credential-ref').default('OPENROUTER_API_KEY').volatile(),
24
+ baseURL: z.string().default(DEFAULT_BASE_URL).volatile(),
25
+ streamIdleTimeoutMs: z.number().min(1).default(300000).volatile(),
26
+ maxRequestImageBytes: z.number().step(1).min(1).default(20 * 1024 * 1024).volatile(),
27
+ searchModel: z.string().default('deepseek/deepseek-v4-flash').volatile(),
28
+ retryPolicy: RetryPolicySchema.volatile(),
24
29
  });
25
30
 
31
+
26
32
  /** Validated connection facts from one config snapshot. */
27
33
  export function resolveOptions(config = {}) {
28
34
  const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? 300000;
@@ -40,18 +46,18 @@ export function resolveOptions(config = {}) {
40
46
  }
41
47
 
42
48
  export function apply(ctx, config = {}) {
43
- let current = () => config, lastRaw, lastGood;
49
+ // The TUI renders this route's own page, so Settings must not generate a form for it.
50
+ ctx.inject(['settings'], settingsCtx => {
51
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
52
+ });
53
+ let lastGood;
44
54
  const options = () => {
45
- const raw = current();
46
- if (raw === lastRaw && lastGood !== undefined) return lastGood;
47
55
  try {
48
- lastGood = resolveOptions(raw);
49
- lastRaw = raw;
56
+ lastGood = resolveOptions(plainConfig(config));
50
57
  return lastGood;
51
58
  } catch (error) {
52
59
  if (lastGood === undefined) throw error;
53
- lastRaw = raw;
54
- ctx.logger.error(`${name}: keeping the last good configuration after an invalid settings section`);
60
+ ctx.logger.error(`${name}: keeping the last good configuration after an invalid edit`);
55
61
  ctx.logger.error(error);
56
62
  return lastGood;
57
63
  }
@@ -75,20 +81,17 @@ export function apply(ctx, config = {}) {
75
81
  resolveAttachments: () => ctx.get('attachments'),
76
82
  resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, hostPath => ctx.get('fs')?.processPathFromHostPath(hostPath), ref),
77
83
  });
78
- ctx.llm.registerConfigurableProviders([{ provider: PROVIDER, displayName: 'OpenRouter', settingsNs: NS, settingsPath: [] }]);
84
+ ctx.llm.registerConfigurableProviders([{ provider: PROVIDER, displayName: 'OpenRouter', settingsNs: ctx.fiber.entry?.options.id ?? NS, settingsPath: [] }]);
79
85
  const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
80
- let registeredPolicy = JSON.stringify(options().retryPolicy);
81
- ctx.inject(['settings'], settingsCtx => {
82
- settingsCtx.settings.installSection(ctx, NS, Config, config, {
83
- setSource: source => { current = source; },
84
- // The retry policy is captured at registration: re-register the route when it changes.
85
- onChange: () => {
86
- const policy = JSON.stringify(options().retryPolicy);
87
- if (policy === registeredPolicy) return;
88
- registration.replace([PROVIDER]);
89
- registeredPolicy = policy;
90
- },
91
- });
86
+ // The retry policy is captured at registration, and a reference carries no change
87
+ // callback: re-register the route when an edit lands on a different policy.
88
+ let registeredPolicy = options().retryPolicy;
89
+ ctx.on('loader/volatile-update', () => {
90
+ let policy;
91
+ try { policy = options().retryPolicy; } catch (error) { ctx.logger.warn(error); return; }
92
+ if (deepEqualJson(policy, registeredPolicy)) return;
93
+ registration.replace([PROVIDER]);
94
+ registeredPolicy = policy;
92
95
  });
93
96
  ctx.inject(['web'], webCtx => {
94
97
  const openrouter = new OpenRouterSearchProvider(() => {
@@ -1,9 +1,10 @@
1
1
  import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm';
2
2
  import { Mailbox, fail } from './mailbox.mjs';
3
+ import { producerKind } from '../message-source/kind.mjs';
3
4
  import { discover, request as socketRequest } from './client.mjs';
4
5
 
5
6
  const plugin = 'dscode-session-bridge';
6
- export const communicationId = m => m?.source?.plugin === plugin ? m.source.communicationId : undefined;
7
+ export const communicationId = m => producerKind(m?.source) === plugin ? m.source.communicationId : undefined;
7
8
  export const isHuman = m => ['user', 'human'].includes(m?.source?.kind);
8
9
  const eligible = a => a?.session.header.agentPreset === 'dscode' || a?.session.header.origin === 'subagent';
9
10
 
@@ -17,7 +18,9 @@ export class CommunicationService {
17
18
  this.pending = new Set();
18
19
  this.closed = false;
19
20
  this.disposers = [
20
- ctx.on('agent/session-start', ({ agent }) => this.start(agent)),
21
+ // DSH 0.1.7 retired `agent/session-start`: `agent/created` is awaited at the same
22
+ // point, once the session is entered and announced.
23
+ ctx.on('agent/created', ({ agent }) => this.start(agent)),
21
24
  ctx.on('agent/pre-step', (payload, next) => this.preStep(payload, next)),
22
25
  ctx.on('session/event', (session, event) => this.observe(session, event)),
23
26
  ctx.on('agent/inbox/discarded', ({ agent, message }) => {
@@ -102,7 +105,7 @@ export class CommunicationService {
102
105
  // Identity must survive retries even when the prior native inbox insertion did not persist.
103
106
  return freezeMessage({ ...createUserMessage({ content: [{ type: 'text', text:
104
107
  `[External source: ${e.from.kind === 'session' ? `session:${e.from.sessionId}` : e.from.source}] [${e.kind}/${e.mode}]\nMessage ID: ${e.messageId}${e.inReplyTo ? `; reply to: ${e.inReplyTo}` : ''}\n${e.text}` }],
105
- source: { kind: 'plugin', plugin, form: 'relay', communicationId: e.messageId, requestId: e.idempotencyKey,
108
+ source: { kind: plugin, form: 'relay', communicationId: e.messageId, requestId: e.idempotencyKey,
106
109
  label: e.from.kind === 'session' ? `session:${e.from.sessionId}` : e.from.source, mode: e.mode, composedAt: e.createdAt } }), id: e.messageId });
107
110
  }
108
111
  async confirm(state) {
@@ -5,6 +5,7 @@ import { randomUUID, createHash } from 'node:crypto';
5
5
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
6
6
  import { normalizeSessionTitle } from '@deepseek-ai/dsh-session-title';
7
7
  import { ensureSocketDirectory } from './paths.mjs';
8
+ import { producerKind } from '../message-source/kind.mjs';
8
9
 
9
10
  export const BRIDGE_SOURCE = 'dscode-session-bridge';
10
11
  const MAX_REQUEST = 128 * 1024, MAX_BUFFER = 8 * 1024 * 1024;
@@ -51,7 +52,7 @@ export class SessionBridge {
51
52
  if (!cache) { cache = { seq: 0, values: new Map() }; this.receipts.set(session, cache); }
52
53
  for (const event of session.snapshotEvents(cache.seq)) {
53
54
  const messages = event.type === 'agent/inbox/spliced' ? event.data.inserted : event.type === 'user/message' ? [event.data] : [];
54
- for (const message of messages) if (message.source.kind === 'plugin' && message.source.plugin === BRIDGE_SOURCE && message.source.requestId) {
55
+ for (const message of messages) if (producerKind(message.source) === BRIDGE_SOURCE && message.source.requestId) {
55
56
  cache.values.set(message.source.requestId, { digest: message.source.digest, messageId: message.id });
56
57
  }
57
58
  }
@@ -82,7 +83,7 @@ export class SessionBridge {
82
83
  let messageId = previous?.messageId;
83
84
  if (!previous) {
84
85
  const message = createUserMessage({ content: [{ type: 'text', text: `[External source: ${source}]\n${text}` }],
85
- source: { kind: 'plugin', plugin: BRIDGE_SOURCE, form: 'relay', requestId, digest, label: source, mode } });
86
+ source: { kind: BRIDGE_SOURCE, form: 'relay', requestId, digest, label: source, mode } });
86
87
  // Explicit naming uses the native title service, including sanitization,
87
88
  // persistence, UI events and cancellation of stale automatic title work.
88
89
  // A duplicate request must never undo a newer title.
@@ -21,6 +21,8 @@ const MAX_WAITING_REQUESTS = 16
21
21
  const MAX_PREVIEW_CHARS = 72
22
22
 
23
23
  /** The plugin name the session bridge stamps on its relayed messages. */
24
+ import { producerKind } from '../message-source/kind.mjs'
25
+
24
26
  const BRIDGE_PLUGIN = 'dscode-session-bridge'
25
27
 
26
28
  /** Tool names that address another session. */
@@ -67,7 +69,7 @@ function relayFields(data) {
67
69
  if (data === null || typeof data !== 'object') return undefined
68
70
  const source = data.source
69
71
  if (source === null || typeof source !== 'object') return undefined
70
- if (source.kind !== 'plugin' || source.plugin !== BRIDGE_PLUGIN || source.form !== 'relay') return undefined
72
+ if (producerKind(source) !== BRIDGE_PLUGIN || source.form !== 'relay') return undefined
71
73
  const label = typeof source.label === 'string' && source.label !== '' ? source.label : undefined
72
74
  const id = typeof source.communicationId === 'string' && source.communicationId !== '' ? source.communicationId : undefined
73
75
  if (label === undefined && id === undefined) return undefined
@@ -16,7 +16,7 @@ export function apply(ctx, config = {}) {
16
16
  const reasoningEffort = await effortFor(ctx.llm, base, 'low', signal);
17
17
  await chargeTo(sessionId, 'session-card', async () => {
18
18
  for await (const chunk of ctx.llm.stream({ ...base, ...(reasoningEffort ? { reasoningEffort } : {}), system: TOPIC_PROMPT,
19
- messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
19
+ messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: name } })],
20
20
  maxTokens: 2000, signal })) {
21
21
  signal.throwIfAborted(); assembler.push(chunk);
22
22
  if (chunk.type === 'finish') finished = true;
@@ -12,6 +12,7 @@
12
12
  import z from '@deepseek-ai/schemastery';
13
13
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
14
14
  import { arrivalMark, resolveTimeZone, turnEndMark } from './marks.mjs';
15
+ import { producerKind } from '../message-source/kind.mjs';
15
16
  export const name = 'dscode-time-marks';
16
17
  export const Config = z.object({
17
18
  /** Fallback display zone; empty uses the host zone. */
@@ -42,7 +43,7 @@ export function apply(ctx, config = {}) {
42
43
  for (const message of payload.messages) {
43
44
  // Marks are injected context, never an inbox arrival; skipping our own
44
45
  // producer keeps a future delivery path from marking itself.
45
- if (message.source?.plugin === name) continue;
46
+ if (producerKind(message.source) === name) continue;
46
47
  const line = arrivalMark(message, at, timeZone);
47
48
  if (line !== null && lines.length < MARK_LIMIT) lines.push(line);
48
49
  }
@@ -60,7 +61,7 @@ export function apply(ctx, config = {}) {
60
61
  // the prompt there, not this mark.
61
62
  messages: [createUserMessage({
62
63
  content: [{ type: 'text', text: lines.join('\n') }],
63
- source: { kind: 'plugin', plugin: name, form: 'snapshot' },
64
+ source: { kind: name, form: 'snapshot' },
64
65
  }), ...decision.messages],
65
66
  };
66
67
  }, { prepend: true });
@@ -7,6 +7,8 @@
7
7
  * @module dscode-time-marks/marks
8
8
  */
9
9
 
10
+ import { producerKind } from '../message-source/kind.mjs'
11
+
10
12
  /** Shortest queue wait worth naming; below it the message simply arrived. */
11
13
  const WAIT_FLOOR_MS = 1_000
12
14
 
@@ -108,15 +110,14 @@ export function elapsedText(ms) {
108
110
  * @returns the attribution clause.
109
111
  */
110
112
  export function describeSource(source) {
111
- const kind = plain(source?.kind)
112
- if (kind === 'user') return 'user message'
113
- if (kind !== 'plugin') return kind === '' ? 'message' : `message via ${kind}`
114
- if (source.form === 'relay') {
113
+ const producer = producerKind(source)
114
+ if (producer === 'user') return 'user message'
115
+ if (source?.form === 'relay') {
115
116
  const mode = plain(source.mode) || 'queue'
116
117
  const label = plain(source.label) || 'another session'
117
118
  return `${mode} relay from ${label}`
118
119
  }
119
- return `message from ${plain(source.plugin) || 'a plugin'}`
120
+ return producer === '' ? 'message' : `message from ${producer}`
120
121
  }
121
122
 
122
123
  /**
@@ -34,7 +34,7 @@ export const DEFAULTS = Object.freeze({
34
34
  });
35
35
 
36
36
  /** Permission presets an unattended run may use; `ask` can never complete. */
37
- const UNATTENDED_PERMISSIONS = Object.freeze(['auto', 'workspace-write', 'read-only', 'danger-full-access']);
37
+ const UNATTENDED_PERMISSIONS = Object.freeze(['auto-review', 'auto', 'workspace-write', 'read-only', 'danger-full-access']);
38
38
 
39
39
  const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
40
40
 
@@ -12,7 +12,7 @@ export function mutationProblem(ctx, agent) {
12
12
  const plan = ctx.get('planMode')?.get(agent);
13
13
  if (plan?.active || plan?.pending) return 'Schedule management is unavailable in plan mode';
14
14
  const permission = ctx.permissionPresets.current(agent.session);
15
- if (!['auto', 'ask', 'workspace-write', 'danger-full-access'].includes(permission)) return 'Schedule management requires a writable permission preset';
15
+ if (!['auto-review', 'auto', 'ask', 'workspace-write', 'danger-full-access'].includes(permission)) return 'Schedule management requires a writable permission preset';
16
16
  }
17
17
 
18
18
  export function registerTriggerTools(ctx, { home, ...deps }) {
@@ -131,7 +131,7 @@ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { mode
131
131
  const reasoningEffort = await effortFor(ctx.llm, route, 'low', deadline);
132
132
  await chargeTo(sessionId, 'doctor', async () => {
133
133
  for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, ...(reasoningEffort ? { reasoningEffort } : {}), maxTokens: 4096, system: SYSTEM,
134
- messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
134
+ messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'dscode-doctor' } })], signal: deadline })) {
135
135
  deadline.throwIfAborted(); assembler.push(chunk);
136
136
  if (chunk.type === 'finish') finished = true;
137
137
  }