@kasenri/dsh-orbit 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/dsh-host.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { resolve } from 'node:path';
4
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
5
  import { collectTurnToolFacts } from "./evidence.js";
3
6
  import { redactText, truncateSafe } from "./sanitize.js";
4
7
  import { classifyTurnSettlement } from "./settlement.js";
@@ -10,6 +13,9 @@ function contentToText(blocks) {
10
13
  .map((block) => (block.type === 'text' ? block.text : `[${block.type}]`))
11
14
  .join('\n');
12
15
  }
16
+ function visibleContentToText(blocks) {
17
+ return blocks?.flatMap((block) => block.type === 'text' && block.text.trim() !== '' ? [block.text] : []).join('\n') ?? '';
18
+ }
13
19
  /**
14
20
  * Wire the Orbit supervisor to DeepSeek Harness native Agent/Subagent services.
15
21
  *
@@ -24,6 +30,7 @@ export class DshOrbitHost {
24
30
  ownedChildren = new Set();
25
31
  interruptedChildren = new Set();
26
32
  childParents = new Map();
33
+ childGrants = new Map();
27
34
  nowFn;
28
35
  sleepFn;
29
36
  constructor(ctx, options = {}) {
@@ -39,12 +46,14 @@ export class DshOrbitHost {
39
46
  }
40
47
  async startRole(request) {
41
48
  const parent = this.parent();
49
+ if (request.workspace !== undefined && resolve(parent.session.header.cwd ?? process.cwd()) !== resolve(request.workspace)) {
50
+ throw new Error('ORBIT_WORKSPACE_MISMATCH: 子代理工作目录必须与 Orbit 持有的 workspace 一致。');
51
+ }
42
52
  const prompt = [{ type: 'text', text: request.prompt }];
43
53
  const agentOptions = {
44
54
  provider: request.route.provider,
45
55
  model: request.route.model,
46
- ...(request.route.reasoningEffort ? { reasoningEffort: request.route.reasoningEffort } : {}),
47
- ...(request.route.maxTokens ? { maxTokens: request.route.maxTokens } : {}),
56
+ reasoningEffort: request.route.reasoningEffort,
48
57
  };
49
58
  if (request.role === 'executor') {
50
59
  return this.startExecutor(parent, request, prompt, agentOptions);
@@ -70,11 +79,12 @@ export class DshOrbitHost {
70
79
  ...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
71
80
  ...(request.outputSchema ? { outputSchema: request.outputSchema } : {}),
72
81
  }));
73
- this.registerChild(run.id, parent);
82
+ this.registerChild(run.id, parent, request);
74
83
  const result = run.result
75
84
  .then((value) => ({
76
85
  childId: run.id,
77
86
  output: contentToText(value.output),
87
+ visibleOutput: visibleContentToText(value.output),
78
88
  interrupted: value.stopReason !== 'completed',
79
89
  ...(value.stopReason !== 'completed' ? { reason: value.stopReason } : {}),
80
90
  ...(value.diagnostic ? { testSummary: [value.diagnostic] } : {}),
@@ -106,13 +116,18 @@ export class DshOrbitHost {
106
116
  if (request.resumeOf) {
107
117
  const existingId = request.resumeOf;
108
118
  const existing = this.ctx.agents.get(existingId);
109
- if (existing) {
119
+ const grant = this.childGrants.get(existingId);
120
+ const requestedTools = new Set(request.toolFilter?.allow ?? []);
121
+ const sameGrant = grant?.role === 'executor' && grant.workspace === (request.workspace ?? parent.session.header.cwd ?? process.cwd())
122
+ && grant.tools.size === requestedTools.size && [...grant.tools].every((tool) => requestedTools.has(tool));
123
+ if (existing && sameGrant && this.childParents.get(existingId) === parent && this.ctx.agents.isOwnedBy(SessionId(existingId), parent)) {
110
124
  const agent = existing;
111
125
  this.interruptedChildren.delete(existingId);
112
- const done = this.waitForExecutorSettlement(agent, existingId);
126
+ const previousTurns = (agent.session?.snapshotEvents?.() ?? []).filter((event) => event.type === 'turn/start').length;
113
127
  await this.ctx.subagents.sendMessage(parent, existingId, prompt, {
114
- signal: new AbortController().signal,
128
+ signal: request.signal ?? new AbortController().signal,
115
129
  });
130
+ const done = this.waitForExecutorSettlement(agent, existingId, previousTurns);
116
131
  return {
117
132
  childId: existingId,
118
133
  result: done,
@@ -124,19 +139,28 @@ export class DshOrbitHost {
124
139
  }
125
140
  const controller = new AbortController();
126
141
  request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
127
- const started = await this.ctx.subagents.startContinuable({
128
- provider: 'spawn',
129
- label: request.label,
130
- request: {
131
- prompt,
132
- parent,
133
- agentOptions,
134
- ...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
135
- },
136
- signal: controller.signal,
137
- });
138
- const childId = String(started.childId);
139
- this.registerChild(childId, parent);
142
+ const reservedId = SessionId(randomUUID());
143
+ const childId = String(reservedId);
144
+ this.registerChild(childId, parent, request);
145
+ let started;
146
+ try {
147
+ started = await this.ctx.subagents.startContinuable({
148
+ provider: 'spawn',
149
+ label: request.label,
150
+ childId: reservedId,
151
+ request: {
152
+ prompt,
153
+ parent,
154
+ agentOptions,
155
+ ...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
156
+ },
157
+ signal: controller.signal,
158
+ });
159
+ }
160
+ catch (error) {
161
+ this.forgetChild(childId);
162
+ throw error;
163
+ }
140
164
  const agent = this.ctx.agents.get(started.childId);
141
165
  const result = agent
142
166
  ? this.waitForExecutorSettlement(agent, childId)
@@ -149,19 +173,26 @@ export class DshOrbitHost {
149
173
  runtimeSnapshot: agent ? () => this.snapshotAgent(agent) : undefined,
150
174
  };
151
175
  }
152
- registerChild(childId, parent) {
176
+ registerChild(childId, parent, request) {
153
177
  this.ownedChildren.add(childId);
154
178
  this.childParents.set(childId, parent);
179
+ this.childGrants.set(childId, {
180
+ role: request.role,
181
+ workspace: resolve(request.workspace ?? parent.session.header.cwd ?? process.cwd()),
182
+ tools: new Set(request.toolFilter?.allow ?? []),
183
+ });
155
184
  }
156
185
  forgetChild(childId) {
157
186
  this.ownedChildren.delete(childId);
158
187
  this.interruptedChildren.delete(childId);
159
188
  this.childParents.delete(childId);
189
+ this.childGrants.delete(childId);
160
190
  }
161
191
  async interruptExecutor(childId, reason) {
162
192
  this.interruptedChildren.add(childId);
163
193
  const parent = this.childParents.get(childId);
164
194
  this.ctx.subagents.interrupt(childId, parent ? { kind: 'ancestor', agent: parent } : { kind: 'user', parentSessionId: childId });
195
+ await this.ctx.agents.get(SessionId(childId))?.whenIdle();
165
196
  void reason;
166
197
  }
167
198
  async drainExecutor(parent, childId) {
@@ -173,8 +204,8 @@ export class DshOrbitHost {
173
204
  }
174
205
  this.forgetChild(childId);
175
206
  }
176
- async waitForExecutorSettlement(agent, childId) {
177
- await this.waitForTurnOrIdle(agent);
207
+ async waitForExecutorSettlement(agent, childId, previousTurns = 0) {
208
+ await this.waitForTurnOrIdle(agent, previousTurns);
178
209
  const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
179
210
  const classified = classifyTurnSettlement(events);
180
211
  const output = this.readFinalOutput(agent);
@@ -214,20 +245,22 @@ export class DshOrbitHost {
214
245
  return { childId, output, interrupted: true, reason: 'EXECUTOR_BLOCKED', telemetry, ...evidence };
215
246
  case 'max-tokens':
216
247
  return { childId, output, interrupted: true, reason: 'EXECUTOR_MAX_TOKENS', telemetry, ...evidence };
248
+ case 'interrupted':
249
+ return { childId, output, interrupted: true, reason: 'EXECUTOR_INTERRUPTED', telemetry, ...evidence };
217
250
  default:
218
251
  return { childId, output, interrupted: true, reason: 'EXECUTOR_NO_TURN', telemetry, ...evidence };
219
252
  }
220
253
  }
221
- async waitForTurnOrIdle(agent) {
254
+ async waitForTurnOrIdle(agent, previousTurns = 0) {
222
255
  const deadline = this.nowFn() + EXECUTOR_TURN_START_TIMEOUT_MS;
223
- while (this.nowFn() < deadline && !this.hasTurnStarted(agent)) {
256
+ while (this.nowFn() < deadline && !this.hasTurnStarted(agent, previousTurns)) {
224
257
  await this.sleepFn(20);
225
258
  }
226
259
  await agent.whenIdle?.();
227
260
  }
228
- hasTurnStarted(agent) {
261
+ hasTurnStarted(agent, previousTurns) {
229
262
  const events = agent.session?.snapshotEvents?.() ?? [];
230
- return events.some((event) => event.type === 'turn/start');
263
+ return events.filter((event) => event.type === 'turn/start').length > previousTurns;
231
264
  }
232
265
  readFinalOutput(agent) {
233
266
  const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
@@ -308,6 +341,53 @@ export class DshOrbitHost {
308
341
  const agent = this.ctx.agents.currentInitiator();
309
342
  return this.ctx.tools.get(name, agent) !== undefined;
310
343
  }
344
+ async validateRoutes(routes, signal) {
345
+ const issues = [];
346
+ const labels = { commander: '指挥官', executor: '执行员', watchdog: '监控模型' };
347
+ for (const role of ['commander', 'executor', 'watchdog']) {
348
+ const route = routes[role];
349
+ try {
350
+ const llm = this.ctx.reflect.get('llm');
351
+ if (!llm)
352
+ throw new Error('DSH LLM registry 不可用');
353
+ const deadline = AbortSignal.timeout(10_000);
354
+ const activeSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
355
+ const info = await llm.resolveModelInfo(route.provider, route.model, activeSignal);
356
+ const catalog = await llm.listModels(route.provider);
357
+ if (catalog.length > 0 && !catalog.some((entry) => entry.id === route.model)) {
358
+ issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前模型目录中不存在,请重新选择。`);
359
+ continue;
360
+ }
361
+ if (route.reasoningEffort !== undefined) {
362
+ const efforts = info.reasoning?.efforts ?? [];
363
+ if (!efforts.some((effort) => effort.id === route.reasoningEffort)) {
364
+ issues.push(`${labels[role]}:ORBIT_REASONING_EFFORT_UNAVAILABLE (${route.provider}/${route.model}/${route.reasoningEffort}),当前模型未声明此推理等级。`);
365
+ }
366
+ }
367
+ }
368
+ catch (error) {
369
+ issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前不可用,请重新选择:${truncateSafe(error instanceof Error ? error.message : String(error), 200)}`);
370
+ }
371
+ }
372
+ return issues;
373
+ }
374
+ isMutationAuthorized(agent, cwd, tool) {
375
+ const id = String(agent?.id ?? '');
376
+ const grant = this.childGrants.get(id);
377
+ const parent = this.childParents.get(id);
378
+ return grant?.role === 'executor' && grant.workspace === resolve(cwd) && grant.tools.has(tool)
379
+ && !this.interruptedChildren.has(id) && this.ctx.agents.get(SessionId(id)) === agent
380
+ && parent !== undefined && this.ctx.agents.isOwnedBy(SessionId(id), parent);
381
+ }
382
+ async revokeWorkspace(cwd) {
383
+ const ids = [...this.childGrants].filter(([, grant]) => grant.workspace === resolve(cwd) && grant.role === 'executor').map(([id]) => id);
384
+ for (const id of ids) {
385
+ const parent = this.childParents.get(id);
386
+ await this.interruptExecutor(id, 'ORBIT_WORKSPACE_RELEASED');
387
+ if (parent)
388
+ await this.drainExecutor(parent, id);
389
+ }
390
+ }
311
391
  /**
312
392
  * Mutation ownership is about top-level autonomous drivers, not about every
313
393
  * running agent. The calling parent, Orbit's own children, and ordinary
@@ -316,17 +396,19 @@ export class DshOrbitHost {
316
396
  * at tool start by the Orbit mutation guard.
317
397
  */
318
398
  async otherMutationDrivers(cwd) {
319
- void cwd;
320
399
  const drivers = [];
321
400
  const initiator = this.ctx.agents.currentInitiator();
322
401
  // `ctx.reflect.get` is the official service lookup that does not require an
323
402
  // inject declaration, so Orbit stays loadable in profiles without dsh-goal.
324
403
  const reflect = this.ctx.reflect;
325
404
  const goals = reflect?.get('goals');
326
- if (goals && initiator) {
405
+ const agents = this.ctx.agents.list?.() ?? (initiator ? [initiator] : []);
406
+ for (const candidate of agents) {
407
+ if (!goals || resolve(candidate.session.header.cwd ?? process.cwd()) !== resolve(cwd) || this.ownedChildren.has(String(candidate.id)))
408
+ continue;
327
409
  try {
328
- const goal = goals.get(initiator);
329
- if (goal?.phase === 'active')
410
+ const goal = goals.get(candidate);
411
+ if (goal?.phase === 'active' && !drivers.includes('goal'))
330
412
  drivers.push('goal');
331
413
  }
332
414
  catch {
package/lib/evidence.js CHANGED
@@ -147,3 +147,27 @@ export function buildEvidenceBundle(input) {
147
147
  export function formatEvidenceBundle(bundle) {
148
148
  return bounded(JSON.stringify(bundle), EVIDENCE_LIMITS.total);
149
149
  }
150
+ /** Persist only compact result facts, never a child transcript. */
151
+ export function buildStepResult(stepId, attempt, bundle, tests = []) {
152
+ return {
153
+ step_id: bounded(stepId, 80),
154
+ attempt,
155
+ summary: bounded(bundle.executor_summary ?? '', EVIDENCE_LIMITS.executorSummary),
156
+ changed_files: bundle.changed_files.map((entry) => bounded(entry, EVIDENCE_LIMITS.entry)).slice(0, EVIDENCE_LIMITS.changedFiles),
157
+ test_summary: [...tests, ...bundle.tests.map((entry) => `${entry.command}: ${entry.status}`)]
158
+ .slice(0, EVIDENCE_LIMITS.testEvidence).map((entry) => bounded(entry, EVIDENCE_LIMITS.entry)),
159
+ evidence: formatEvidenceBundle(bundle),
160
+ };
161
+ }
162
+ /** Fair per-step quotas keep every step visible in a bounded final review. */
163
+ export function formatStepResults(state) {
164
+ const steps = state.plan.steps;
165
+ const quota = Math.max(1, Math.floor(EVIDENCE_LIMITS.total / Math.max(1, steps.length)) - 100);
166
+ return steps.map((step) => {
167
+ const result = state.step_results?.find((entry) => entry.step_id === step.id);
168
+ const content = result === undefined ? '未记录持久化结果' : JSON.stringify({
169
+ summary: result.summary, test_summary: result.test_summary, changed_files: result.changed_files, evidence: result.evidence,
170
+ });
171
+ return `${bounded(step.id, 80)}[${step.status}] ${bounded(content, quota)}`;
172
+ }).join('\n');
173
+ }
package/lib/guard.js CHANGED
@@ -10,23 +10,23 @@ const GITHUB_REMOTE_PATTERNS = [
10
10
  /\bgit\s+remote\s+(?:set-url|add|remove)\b/i,
11
11
  ];
12
12
  const DANGEROUS_PATTERNS = [
13
- ['destructive_operation', /\brm\s+-[A-Za-z]*r[A-Za-z]*f|\brm\s+-rf\b/i, 'recursive force deletion'],
14
- ['destructive_operation', /\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[A-Za-z]*f/i, 'irreversible git reset/clean'],
15
- ['production_operation', /\bdrop\s+database\b|\b(?:drop|truncate)\s+(?:table|schema)\b/i, 'irreversible database operation'],
13
+ ['destructive_operation', /\brm\s+-[A-Za-z]*r[A-Za-z]*f|\brm\s+-rf\b/i, '递归强制删除'],
14
+ ['destructive_operation', /\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[A-Za-z]*f/i, '不可逆的 git reset/clean'],
15
+ ['production_operation', /\bdrop\s+database\b|\b(?:drop|truncate)\s+(?:table|schema)\b/i, '不可逆的数据库操作'],
16
16
  [
17
17
  'production_operation',
18
18
  /\b(?:production|prod)\b.*\b(?:deploy|migrate|database|restart)\b|\b(?:deploy|migrate)\b.*\b(?:production|prod)\b/i,
19
- 'production operation',
19
+ '生产环境操作',
20
20
  ],
21
21
  [
22
22
  'production_operation',
23
23
  /\b(?:kubectl|helm)\s+(?:apply|delete|upgrade|rollback)\b|\bterraform\s+apply\b|\bdocker\s+push\b/i,
24
- 'deployment or remote registry write',
24
+ '部署或远程 registry 写入',
25
25
  ],
26
- ['production_operation', /\b(?:alembic|prisma|sequelize|rails)\b.*\b(?:upgrade|migrate|db:migrate)\b/i, 'database migration'],
27
- ['production_operation', /\b(?:ufw|iptables)\b.*\b(?:allow|insert|append)\b|\bdocker\s+run\b.*\s-p\s/i, 'public network exposure'],
28
- ['secret_operation', /\b(?:cat|less|more|head|tail)\b[^\n]*(?:\.env|secret|credential|auth\.json|cookie)/i, 'credential or secret output'],
29
- ['secret_operation', /\b(?:curl|wget)\b[^\n]*(?:Authorization|Bearer|api[_-]?key|token)=?/i, 'credential-bearing network request'],
26
+ ['production_operation', /\b(?:alembic|prisma|sequelize|rails)\b.*\b(?:upgrade|migrate|db:migrate)\b/i, '数据库迁移'],
27
+ ['production_operation', /\b(?:ufw|iptables)\b.*\b(?:allow|insert|append)\b|\bdocker\s+run\b.*\s-p\s/i, '暴露公共网络端口'],
28
+ ['secret_operation', /\b(?:cat|less|more|head|tail)\b[^\n]*(?:\.env|secret|credential|auth\.json|cookie)/i, '输出凭证或敏感信息'],
29
+ ['secret_operation', /\b(?:curl|wget)\b[^\n]*(?:Authorization|Bearer|api[_-]?key|token)=?/i, '携带凭证的网络请求'],
30
30
  ];
31
31
  const SENSITIVE_ENV_NAME = /(?:^|_)(?:KEY|TOKEN|PASSWORD|PASSWD|SECRET|COOKIE|AUTH|CREDENTIAL|PRIVATE_KEY|ACCESS_TOKEN|REFRESH_TOKEN)(?:_|$)/i;
32
32
  const DURABLE_STATE_PATH = /(?:^|[\s/'"])(?:\.\/)?\.cx(?:[/'"\s]|$)/i;
@@ -82,25 +82,25 @@ export function guardBashCommand(command, intent = {}) {
82
82
  if (intent.github_allowed !== true) {
83
83
  for (const pattern of GITHUB_REMOTE_PATTERNS) {
84
84
  if (pattern.test(trimmed))
85
- return block('github_remote_write', `GitHub remote write is not allowed: ${pattern.source}`);
85
+ return block('github_remote_write', `未授权 GitHub 远程写入:${pattern.source}`);
86
86
  }
87
87
  }
88
88
  if (isDurableStateWrite(trimmed)) {
89
- return block('durable_state_write', 'Only the Orbit controller may write .cx durable state.');
89
+ return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态。');
90
90
  }
91
91
  if (looksLikeEnvSecret(trimmed)) {
92
- return block('secret_operation', 'Reading or exporting secrets is not allowed.');
92
+ return block('secret_operation', '不允许读取或导出敏感凭证。');
93
93
  }
94
94
  if (/^\s*export\s+[A-Za-z_][A-Za-z0-9_]*\s*=/i.test(trimmed)) {
95
95
  const assignment = /^\s*export\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/i.exec(trimmed);
96
96
  if (assignment && SENSITIVE_ENV_NAME.test(assignment[1] ?? '')) {
97
- return block('secret_operation', 'Exporting a secret environment variable is not allowed.');
97
+ return block('secret_operation', '不允许导出敏感环境变量。');
98
98
  }
99
99
  }
100
100
  if (/^\s*set\s+[A-Za-z_][A-Za-z0-9_]*=/i.test(trimmed)) {
101
101
  const assignment = /^\s*set\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/i.exec(trimmed);
102
102
  if (assignment && SENSITIVE_ENV_NAME.test(assignment[1] ?? '')) {
103
- return block('secret_operation', 'Setting a secret environment variable is not allowed.');
103
+ return block('secret_operation', '不允许设置敏感环境变量。');
104
104
  }
105
105
  }
106
106
  for (const [code, pattern, reason] of DANGEROUS_PATTERNS) {
@@ -118,11 +118,11 @@ export function guardToolPath(toolName, targetPath, deps) {
118
118
  const cwd = deps.cwd;
119
119
  const absolute = isAbsolute(targetPath) ? targetPath : resolve(cwd, targetPath);
120
120
  if (isInsideDurableState(absolute, cwd)) {
121
- return block('durable_state_write', 'Only the Orbit controller may write .cx durable state.');
121
+ return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态。');
122
122
  }
123
123
  const realpath = deps.realpath ?? safeRealpath;
124
124
  if (isInsideDurableState(realpathWithin(absolute, realpath), cwd)) {
125
- return block('durable_state_write', 'Only the Orbit controller may write .cx durable state (symlink resolved).');
125
+ return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态(已解析符号链接)。');
126
126
  }
127
127
  return { allowed: true };
128
128
  }
@@ -153,5 +153,5 @@ function safeRealpath(path) {
153
153
  }
154
154
  }
155
155
  export function guardReason(decision) {
156
- return `Blocked by Orbit safety guard (${decision.code}): ${decision.reason}`;
156
+ return `Orbit 安全护栏阻断当前调用 (${decision.code}):${decision.reason}`;
157
157
  }
package/lib/index.js CHANGED
@@ -1,19 +1,16 @@
1
- import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
1
+ import { AssistantStreamAccumulator, boundContextSummary, createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
2
2
  import z from '@deepseek-ai/schemastery';
3
3
  import { installOrbitGestureBoundary, registerOrbitCommand, registerOrbitToggleCommand } from "./activation.js";
4
- import { estimateLoopCount } from "./kernel.js";
5
4
  import { createOrbitPreExecuteHandler } from "./pipeline-guard.js";
6
- import { resolveEffectiveRoutes, sessionModelStateOf, sessionSelectionOf } from "./routes.js";
5
+ import { agentDefaultSelectionOf, resolveEffectiveRoutes, sessionModelSelectionOf, sessionModelStateOf } from "./routes.js";
7
6
  import { installOrbitSessionProjection, orbitEnabledOf } from "./session-state.js";
8
7
  import { OrbitService } from "./service.js";
9
8
  import { createOrbitTool } from "./tool.js";
10
9
  /**
11
- * Minimum loop budget for a hard-activated run. The Commander is asked for a
12
- * 2-5 step plan and every executed step consumes one loop slot, so the host
13
- * must grant a budget that can finish a full plan; the goal complexity
14
- * estimate raises it further when the goal asks for more.
10
+ * Fixed hard-activation budget. Explicit tool calls may still supply a bounded
11
+ * user-owned budget; ordinary activation never guesses complexity from words.
15
12
  */
16
- const HARD_ACTIVATION_MIN_LOOPS = 5;
13
+ const HARD_ACTIVATION_LOOP_BUDGET = 5;
17
14
  export const name = 'dsh-orbit';
18
15
  export const inject = ['tools', 'agents', 'subagents'];
19
16
  const Route = z.object({
@@ -39,66 +36,60 @@ export const Config = z.object({
39
36
  executor: Route,
40
37
  watchdog: Route,
41
38
  })
42
- .default({
43
- commander: { provider: 'deepseek-official', model: 'deepseek-v4-pro', reasoningEffort: 'high' },
44
- executor: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'high' },
45
- watchdog: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'low' },
46
- }),
39
+ .default({}),
47
40
  browserTools: z.array(z.string()).default(['agent_browser']),
48
41
  commanderReadOnlyTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep', 'web_search', 'web_fetch']),
49
42
  watchdogTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep']),
50
43
  executorTools: z
51
44
  .array(z.string())
52
- .default(['read', 'read_image', 'glob', 'grep', 'bash', 'write', 'edit', 'str_replace_editor', 'web_search', 'web_fetch']),
45
+ .default(['read', 'read_image', 'glob', 'grep']),
53
46
  executorTimeoutMs: z.natural().default(480_000),
54
47
  registerTool: z.boolean().default(true),
55
48
  registerGuards: z.boolean().default(true),
56
49
  slashCommand: z.boolean().default(true),
57
50
  });
58
51
  export function apply(ctx, config) {
59
- // Orbit settings bridge: register the `orbit` namespace with the composition
60
- // routes as its base/default, so a user who never opens the model UI keeps
61
- // today's behavior exactly. Lazily injected: minimal/headless compositions
62
- // without a settings provider keep running from `config.routes`.
63
- let routeSettings;
52
+ // Orbit settings bridge. The package ships no model defaults; only explicit
53
+ // profile routes can act as a headless compatibility fallback.
54
+ let readRouteSettings = () => undefined;
64
55
  ctx.inject(['settings'], (settingsCtx) => {
65
- const baseRoute = (route) => ({
66
- provider: route.provider,
67
- model: route.model,
68
- reasoningEffort: route.reasoningEffort ?? '',
69
- });
70
56
  const scope = settingsCtx.settings.register('orbit', OrbitRouteSettingsSchema, {
71
57
  base: {
72
- commander: baseRoute(config.routes.commander),
73
- watchdog: baseRoute(config.routes.watchdog),
58
+ commander: { provider: '', model: '', reasoningEffort: '' },
59
+ watchdog: { provider: '', model: '', reasoningEffort: '' },
74
60
  },
75
61
  });
76
- const sync = () => {
77
- routeSettings = scope.get();
78
- };
79
- sync();
80
- scope.watch(() => {
81
- sync();
82
- });
62
+ readRouteSettings = () => scope.get();
63
+ settingsCtx.effect(() => () => { readRouteSettings = () => undefined; });
83
64
  });
84
65
  // A NEW run resolves its three routes exactly once: Commander/Watchdog from
85
66
  // the Orbit settings (base = config), Executor from the initiating Session's
86
- // durable `modelSelection` projection (pending → lastUsed), with the public
87
- // request-header seam and the config route as compatibility fallbacks for
88
- // surfaces without one. Existing runs resume from their frozen
89
- // `state.routes`.
67
+ // current model with DSH `selectionFor(agent)` semantics (pending choice
68
+ // logged header deployment default). Explicit config routes are only for
69
+ // sessions without a DSH model source. Existing runs resume from frozen routes.
90
70
  const resolveRoutes = () => {
91
71
  const agent = ctx.agents.currentInitiator();
92
72
  return resolveEffectiveRoutes({
93
73
  configRoutes: config.routes,
94
- settings: routeSettings,
95
- sessionModel: sessionModelStateOf(ctx, agent?.session),
96
- sessionSelection: sessionSelectionOf(agent),
74
+ settings: readRouteSettings(),
75
+ hasSession: agent !== undefined,
76
+ sessionSelection: sessionModelSelectionOf({
77
+ sessionModel: sessionModelStateOf(ctx, agent?.session),
78
+ requestHeader: agent?.session?.requestHeader?.(),
79
+ agentDefault: agent === undefined ? undefined : agentDefaultSelectionOf(ctx),
80
+ }),
97
81
  });
98
82
  };
83
+ // The DSH Session driving the current operation: a new run records it, and
84
+ // only that Session may answer the run's NEEDS_USER question.
85
+ const resolveOwnerSessionId = () => {
86
+ const agent = ctx.agents.currentInitiator();
87
+ return agent === undefined ? undefined : String(agent.session.id);
88
+ };
99
89
  const serviceConfig = {
100
90
  routes: config.routes,
101
91
  resolveRoutes,
92
+ resolveOwnerSessionId,
102
93
  browserTools: config.browserTools,
103
94
  commanderReadOnlyTools: config.commanderReadOnlyTools,
104
95
  watchdogTools: config.watchdogTools,
@@ -129,7 +120,7 @@ export function apply(ctx, config) {
129
120
  });
130
121
  installOrbitGestureBoundary(ctx, {
131
122
  sessionEnabled: (session) => orbitEnabledOf(ctx, session),
132
- activate: async (agent, goal, signal) => {
123
+ activate: async (agent, goal, position, signal) => {
133
124
  const cwd = agent.session.header.cwd ?? process.cwd();
134
125
  let result;
135
126
  try {
@@ -137,13 +128,17 @@ export function apply(ctx, config) {
137
128
  // the parent model is never asked to decide or to run the task.
138
129
  result = await ctx.agents.withInitiator(agent, () => service.run({
139
130
  goal,
140
- approved_loop_count: Math.max(estimateLoopCount(goal), HARD_ACTIVATION_MIN_LOOPS),
131
+ approved_loop_count: HARD_ACTIVATION_LOOP_BUDGET,
141
132
  }, cwd, signal));
142
133
  }
143
134
  catch (error) {
144
135
  appendOrbitNotice(agent.session, `Orbit 启动失败:${error instanceof Error ? error.message : String(error)}`);
145
136
  return;
146
137
  }
138
+ if (result.ok && result.phase === 'SUCCESS' && result.final_output !== undefined) {
139
+ appendOrbitFinalOutput(agent.session, position, result.final_output);
140
+ return;
141
+ }
147
142
  const notice = activationNotice(result);
148
143
  if (notice !== undefined)
149
144
  appendOrbitNotice(agent.session, notice);
@@ -155,8 +150,9 @@ export function apply(ctx, config) {
155
150
  ctx.inject(['sessionProjections'], (projectionCtx) => {
156
151
  installOrbitSessionProjection(projectionCtx);
157
152
  });
158
- if (config.registerGuards) {
153
+ {
159
154
  const handler = createOrbitPreExecuteHandler(service, {
155
+ contentGuards: config.registerGuards,
160
156
  competingDriver: (agent) => {
161
157
  const reflect = ctx.reflect;
162
158
  const goals = reflect?.get('goals');
@@ -176,6 +172,26 @@ export function apply(ctx, config) {
176
172
  ctx.on('tools/pre-execute', (exec, next) => handler(exec, next));
177
173
  }
178
174
  }
175
+ /** Append the existing Final Commander answer as this consumed turn's sole assistant result. */
176
+ function appendOrbitFinalOutput(session, position, output) {
177
+ const message = createAssistantMessage({
178
+ content: [{ type: 'text', text: output.text }],
179
+ source: { provider: output.provider, model: output.model },
180
+ });
181
+ const stream = new AssistantStreamAccumulator();
182
+ const time = Date.now();
183
+ stream.push({ time, chunk: { type: 'block-start', index: 0, blockType: 'text' } });
184
+ stream.push({ time, chunk: { type: 'text-delta', index: 0, text: output.text } });
185
+ stream.push({ time, chunk: { type: 'block-end', index: 0, block: { type: 'text', text: output.text } } });
186
+ stream.push({ time, chunk: { type: 'finish', reason: { kind: 'stop' } } });
187
+ session.append('step/start', position);
188
+ try {
189
+ session.append('assistant/message', { ...position, message, stream: [...stream.snapshot()] }, { surfaceOp: 'append' });
190
+ }
191
+ finally {
192
+ session.append('step/end', position);
193
+ }
194
+ }
179
195
  /**
180
196
  * One durable, user-visible Orbit notice. The DSH `plugin` + `form: 'notice'`
181
197
  * source renders as a collapsed notice row instead of a user message, and the
@@ -198,6 +214,11 @@ function activationNotice(result) {
198
214
  return undefined;
199
215
  const lastError = result.data?.['last_error'];
200
216
  const reason = result.message ?? (typeof lastError === 'string' && lastError !== '' ? lastError : undefined);
217
+ if (result.message?.startsWith('ORBIT_NEEDS_USER_OTHER_SESSION') === true || result.message?.startsWith('ORBIT_NEEDS_USER_OWNER_UNKNOWN') === true) {
218
+ // Another Session owns the waiting run: report instead of consuming the
219
+ // message as its reply.
220
+ return result.message;
221
+ }
201
222
  if (result.phase === 'NEEDS_USER') {
202
223
  return reason !== undefined && !reason.startsWith('COMMANDER_')
203
224
  ? `Orbit 需要你的回复:${reason}`