odai-dsh-agent 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -13,7 +13,7 @@ $DSH_HOME/.agent-presets/odai/
13
13
  .odai-agent.json
14
14
  ```
15
15
 
16
- The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only.
16
+ The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only. For DSH compaction on the same provider/model, an omitted summarizer reasoning setting inherits the session's current user-selected reasoning effort so the auxiliary call remains cache-compatible; explicit and cross-model settings are not changed.
17
17
 
18
18
  ## Install
19
19
 
@@ -53,6 +53,16 @@ The controller calls `odai_routing_config` to persist that explicit choice. The
53
53
 
54
54
  Planner, executor, and reviewer are independent optional responsibilities. If any needed one has no mapping, high-impact work fails closed and remains read-only; lower-impact work continues only where it does not depend on the missing independent responsibility. Odai never chooses a model on the user's behalf.
55
55
 
56
+ ## Skill sources
57
+
58
+ The Agent keeps the managed preset's complete skill copy as its `bundled` default, so existing installations do not change behavior. When the user explicitly asks to show, set, or reset the Odai skill source, the controller uses `odai_skill_source_config` and stores the choice in `$DSH_HOME/odai/source.json`, outside the managed preset:
59
+
60
+ - `bundled`: use the skill shipped with this Agent release.
61
+ - `auto`: allow a compatible current-project `.dsh/skills/odai` or `.agents/skills/odai` bundle, then DSH custom roots and newer user installs under `$DSH_HOME/skills/odai` or `$DSH_AGENTS_HOME/skills/odai` (default `~/.agents/skills/odai`), with bundled fallback.
62
+ - `user`: ignore project roots and require a compatible custom or user-level bundle. An unusable source produces an explicit bundled fallback diagnostic so it can be repaired through the same tool.
63
+
64
+ An independent install must be a complete directory bundle containing `SKILL.md`, `manifest.json`, and every manifest-declared file. The runtime checks SemVer 2.0.0 `skillVersion`, an exact supported `runtimeContract`, complete-file SHA-256 integrity, and same-version content conflicts. Prompt governance and routing role contracts are selected atomically for one agent turn; project sources are scoped by that session's cwd, and changes are reconsidered on the next user turn. Explicit deployment `skillPath` or `ODAI_SKILL_PATH` remains highest priority and requires a DSH restart.
65
+
56
66
  ## Status and uninstall
57
67
 
58
68
  ```sh
@@ -71,7 +81,7 @@ Use only the surface that matches the desired scope:
71
81
  - `odai-dsh-agent`: selectable Odai governance for only sessions using this preset.
72
82
  - both: supported only for a deliberate combination of profile-wide and Agent-scoped behavior; normally redundant, so it is not the default recommendation.
73
83
 
74
- When both are deliberate, the shared compatibility-safe evidence store deduplicates tool and route records and denials remain monotonic. Neither package installs or changes the provider-neutral `odai-cli`.
84
+ When both are deliberate, a process-shared per-agent/per-turn skill snapshot keeps prompt governance and role contracts identical, the compatibility-safe evidence store deduplicates tool and route records, and denials remain monotonic. Neither package installs or changes the provider-neutral `odai-cli`.
75
85
 
76
86
  ## Development
77
87
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { readDshVersion } from "../src/dsh-version.mjs";
4
4
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odai-dsh-agent",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "完整继承 DSH 标准模式 全部能力的 Odai Agent preset。主要用于治理、规划、测试、验证 等,可当成默认总控使用。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { existsSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import {
7
7
  decideRoute,
8
- extractLatestUserText,
8
+ extractRoutingText,
9
9
  renderDelegationPrompt,
10
10
  renderMissingRouteConfigNotice,
11
11
  renderRouteFailureNotice,
@@ -27,9 +27,26 @@ import {
27
27
  resolveRoutingConfigPath,
28
28
  } from "./routing-config.mjs";
29
29
  import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
30
+ import {
31
+ SKILL_SOURCE_CONFIG_PROMPT,
32
+ createSkillSourceConfigTool,
33
+ effectiveSkillSource,
34
+ resolveSkillSourceConfigPath,
35
+ } from "./skill-source-config.mjs";
36
+ import {
37
+ ODAI_RUNTIME_CONTRACT,
38
+ SKILL_SOURCE_MODES,
39
+ loadSkillBundle,
40
+ } from "./skill-bundle.mjs";
41
+ import { resolveSkillSelection } from "./skill-selector.mjs";
42
+ import {
43
+ currentAgentTurn,
44
+ selectSharedSkillForTurn,
45
+ sharedSkillSelection,
46
+ } from "./skill-selection-state.mjs";
30
47
 
31
48
  export const name = "odai-dsh-runtime";
32
- export const inject = ["systemPrompt", "tools", "subagents"];
49
+ export const inject = ["systemPrompt", "tools", "subagents", "sessions"];
33
50
 
34
51
  const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
35
52
  const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
@@ -54,6 +71,8 @@ export function resolveConfig(rawConfig = {}) {
54
71
  const maxInputChars = routing.maxInputChars ?? 12_000;
55
72
  const configPath = resolveRoutingConfigPath(routing.configPath);
56
73
  const additionalDeniedTools = governance.additionalDeniedTools ?? [];
74
+ const skillSource = governance.skillSource ?? "bundled";
75
+ const skillConfigPath = resolveSkillSourceConfigPath(governance.skillConfigPath);
57
76
  const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
58
77
 
59
78
  if (!ROUTING_MODES.has(mode)) {
@@ -72,6 +91,9 @@ export function resolveConfig(rawConfig = {}) {
72
91
  || additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
73
92
  throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
74
93
  }
94
+ if (!SKILL_SOURCE_MODES.includes(skillSource)) {
95
+ throw new TypeError("config.governance.skillSource must be bundled, auto, or user");
96
+ }
75
97
  if (raw.skillPath !== undefined && (typeof raw.skillPath !== "string" || raw.skillPath.trim() === "")) {
76
98
  throw new TypeError("config.skillPath must be a non-empty string");
77
99
  }
@@ -90,32 +112,51 @@ export function resolveConfig(rawConfig = {}) {
90
112
  }),
91
113
  governance: Object.freeze({
92
114
  additionalDeniedTools: Object.freeze(additionalDeniedTools.map((tool) => tool.trim())),
115
+ skillSource,
116
+ skillConfigPath,
93
117
  }),
94
118
  });
95
119
  }
96
120
 
97
- export function resolveSkillPath(configuredPath) {
121
+ export function inheritCompactionReasoning(options, sessions) {
122
+ if (options?.purpose !== "compaction"
123
+ || options.reasoningEffort !== undefined
124
+ || options.sessionId === undefined
125
+ || !Object.isExtensible(options)) {
126
+ return false;
127
+ }
128
+
129
+ const route = sessions?.get?.(options.sessionId)?.requestHeader?.()?.config;
130
+ if (route?.provider !== options.provider
131
+ || route?.model !== options.model
132
+ || typeof route.reasoningEffort !== "string"
133
+ || route.reasoningEffort.length === 0) {
134
+ return false;
135
+ }
136
+
137
+ options.reasoningEffort = route.reasoningEffort;
138
+ return true;
139
+ }
140
+
141
+ export function resolveSkillPath(configuredPath, env = process.env) {
142
+ const explicit = configuredPath ?? env.ODAI_SKILL_PATH;
143
+ if (explicit !== undefined) {
144
+ const path = resolve(explicit);
145
+ if (!existsSync(path)) throw new Error(`explicit Odai canonical skill not found: ${path}`);
146
+ return path;
147
+ }
148
+
98
149
  const candidates = [
99
- configuredPath,
100
- process.env.ODAI_SKILL_PATH,
101
150
  resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
102
151
  resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
103
- ].filter(Boolean).map((candidate) => resolve(candidate));
104
-
152
+ ];
105
153
  const found = candidates.find((candidate) => existsSync(candidate));
106
154
  if (found) return found;
107
-
108
- throw new Error(`odai canonical skill not found; checked: ${candidates.join(", ")}`);
155
+ throw new Error(`Odai bundled canonical skill not found; checked: ${candidates.join(", ")}`);
109
156
  }
110
157
 
111
158
  export function loadRoleContracts(skillPath) {
112
- const roleRoot = resolve(dirname(skillPath), "assets/routing-roles");
113
- return Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => {
114
- const path = resolve(roleRoot, `${role}.md`);
115
- const text = existsSync(path) ? readFileSync(path, "utf8").trim() : "";
116
- if (!text) throw new Error(`odai canonical ${role} role is unavailable: ${path}`);
117
- return [role, text];
118
- })));
159
+ return loadSkillBundle(skillPath).roleContracts;
119
160
  }
120
161
 
121
162
  function deepFreeze(value) {
@@ -263,13 +304,20 @@ export async function runRoutedRole({ subagents, provider, decision, taskText, r
263
304
  return outcome;
264
305
  }
265
306
 
266
- function canonicalPrompt(skillPath, skillText) {
307
+ function canonicalPrompt(selection) {
308
+ const { bundle } = selection;
309
+ const fallback = selection.status === "fallback"
310
+ ? `Selection fallback: ${selection.reasonCode}${selection.detail ? ` (${selection.detail})` : ""}.`
311
+ : undefined;
267
312
  return [
268
313
  "## odai canonical governance",
269
- `Canonical source: ${skillPath}`,
314
+ `Canonical source: ${bundle.source} (${bundle.provider})`,
315
+ `Canonical skill: ${bundle.manifest.skillVersion}; runtime contract: ${bundle.manifest.runtimeContract}; digest: ${bundle.digest}.`,
316
+ ...(fallback ? [fallback] : []),
270
317
  "Apply this governance to every request. Keep the controller as the final delivery owner; use another role only for a real independent gap with observable net benefit.",
318
+ "Odai governance is already loaded by this runtime; do not call the skill tool to load odai again.",
271
319
  "",
272
- skillText,
320
+ bundle.skillText,
273
321
  ].join("\n");
274
322
  }
275
323
 
@@ -302,21 +350,108 @@ export function apply(ctx, rawConfig) {
302
350
  }
303
351
  };
304
352
  const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
353
+ ctx.on("llm/stream", (options, next) => {
354
+ inheritCompactionReasoning(options, ctx.sessions);
355
+ return next();
356
+ });
305
357
  const skillPath = resolveSkillPath(config.skillPath);
306
- const skillText = readFileSync(skillPath, "utf8").trim();
307
- if (!skillText) throw new Error(`odai canonical skill is empty: ${skillPath}`);
308
- const roleContracts = loadRoleContracts(skillPath);
358
+ const explicitSkillPath = config.skillPath !== undefined
359
+ || (typeof process.env.ODAI_SKILL_PATH === "string" && process.env.ODAI_SKILL_PATH.trim() !== "");
360
+ const bundled = loadSkillBundle(skillPath, {
361
+ source: explicitSkillPath ? "path" : "bundled",
362
+ provider: explicitSkillPath ? "odai-explicit-path" : "odai-dsh-runtime",
363
+ });
364
+ if (bundled.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
365
+ throw new Error(`Odai canonical runtimeContract ${bundled.manifest.runtimeContract} is incompatible with this runtime`);
366
+ }
367
+ const baseSelection = Object.freeze({
368
+ mode: explicitSkillPath ? "path" : "bundled",
369
+ status: "selected",
370
+ reasonCode: explicitSkillPath ? "explicit-path" : "bundled-configured",
371
+ bundle: bundled,
372
+ rejections: Object.freeze([]),
373
+ });
309
374
 
310
375
  ctx.systemPrompt.section({
311
376
  name: "odai:canonical-governance",
312
377
  order: -20,
313
- text: canonicalPrompt(skillPath, skillText),
378
+ text: canonicalPrompt(baseSelection),
314
379
  });
315
380
  ctx.systemPrompt.section({
316
381
  name: "odai:routing-configuration",
317
382
  order: -19,
318
383
  text: ROUTING_CONFIG_PROMPT,
319
384
  });
385
+ ctx.systemPrompt.section({
386
+ name: "odai:skill-source-configuration",
387
+ order: -18,
388
+ text: SKILL_SOURCE_CONFIG_PROMPT,
389
+ });
390
+
391
+ const optionalSkillRegistry = () => {
392
+ try {
393
+ if (ctx.skills && typeof ctx.skills.get === "function") return ctx.skills;
394
+ } catch {}
395
+ try {
396
+ const service = typeof ctx.get === "function" ? ctx.get("skills") : undefined;
397
+ return service && typeof service.get === "function" ? service : undefined;
398
+ } catch {
399
+ return undefined;
400
+ }
401
+ };
402
+ const selectForAgent = async (agent, context) => {
403
+ if (explicitSkillPath) return baseSelection;
404
+ let mode;
405
+ try {
406
+ mode = effectiveSkillSource(config.governance.skillConfigPath, config.governance.skillSource);
407
+ } catch (error) {
408
+ return Object.freeze({
409
+ ...baseSelection,
410
+ mode: "bundled",
411
+ status: "fallback",
412
+ reasonCode: "source-config-invalid",
413
+ detail: error instanceof Error ? error.message : String(error),
414
+ });
415
+ }
416
+ return resolveSkillSelection({
417
+ mode,
418
+ bundled,
419
+ cwd: agent.session?.header?.cwd,
420
+ scope: context.scope,
421
+ signal: context.signal,
422
+ skills: optionalSkillRegistry(),
423
+ });
424
+ };
425
+ ctx.on("system-prompt/assemble", async (assembly, context, next) => {
426
+ const downstream = await next();
427
+ const agent = context.agent;
428
+ if (!agent) return downstream;
429
+ const selection = await selectSharedSkillForTurn(agent, () => selectForAgent(agent, context));
430
+ const turn = currentAgentTurn(agent);
431
+ const selectionEvidence = {
432
+ ...(turn === undefined ? {} : { turn }),
433
+ requestedMode: selection.mode,
434
+ status: selection.status,
435
+ reasonCode: selection.reasonCode,
436
+ effectiveSource: selection.bundle.source,
437
+ skillVersion: selection.bundle.manifest.skillVersion,
438
+ runtimeContract: selection.bundle.manifest.runtimeContract,
439
+ digest: selection.bundle.digest,
440
+ rejections: selection.rejections.map(({ source, reasonCode }) => ({ source, reasonCode })),
441
+ };
442
+ if (!hasSessionEvent(agent, "odai/skill-selected", (data) => data?.turn === turn && data?.digest === selection.bundle.digest)) {
443
+ appendEvent(agent, "odai/skill-selected", selectionEvidence);
444
+ }
445
+ if (selection.status === "fallback") {
446
+ logger.warn(`Odai skill source ${selection.mode} fell back to bundled governance (${selection.reasonCode})`);
447
+ }
448
+ return {
449
+ ...downstream,
450
+ sections: downstream.sections.map((section) => section.name === "odai:canonical-governance"
451
+ ? { ...section, text: canonicalPrompt(selection) }
452
+ : section),
453
+ };
454
+ });
320
455
 
321
456
  const routeProtections = new WeakMap();
322
457
  const controllerUpgrades = new WeakMap();
@@ -354,6 +489,16 @@ export function apply(ctx, rawConfig) {
354
489
  appendEvent(agent, "odai/routing-configured", data);
355
490
  },
356
491
  }));
492
+ ctx.tools.register(createSkillSourceConfigTool(
493
+ config.governance.skillConfigPath,
494
+ config.governance.skillSource,
495
+ {
496
+ explicitPath: explicitSkillPath,
497
+ onConfigured(agent, data) {
498
+ appendEvent(agent, "odai/skill-source-configured", data);
499
+ },
500
+ },
501
+ ));
357
502
  ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
358
503
 
359
504
  ctx.on("tools/result", (execution, result) => {
@@ -423,7 +568,7 @@ export function apply(ctx, rawConfig) {
423
568
  if (routed.has(routeKey)) return downstream;
424
569
  routed.add(routeKey);
425
570
 
426
- const taskText = extractLatestUserText(downstream.messages).slice(0, config.routing.maxInputChars);
571
+ const taskText = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
427
572
  const decision = decideRoute({ text: taskText });
428
573
  const routeRole = decision.targetRole ?? decision.role;
429
574
  appendEvent(agent, "odai/route-decided", {
@@ -520,7 +665,7 @@ export function apply(ctx, rawConfig) {
520
665
  provider: config.routing.provider,
521
666
  decision: delegationDecision,
522
667
  taskText,
523
- roleContract: roleContracts[routeRole],
668
+ roleContract: (sharedSkillSelection(agent, turn)?.bundle ?? bundled).roleContracts[routeRole],
524
669
  agent,
525
670
  signal,
526
671
  roleRoute,
@@ -577,7 +722,7 @@ export function apply(ctx, rawConfig) {
577
722
  }, { prepend: true });
578
723
  }
579
724
 
580
- logger.info(`loaded canonical governance from ${skillPath}; routing=${config.routing.mode}`);
725
+ logger.info(`loaded canonical governance ${bundled.manifest.skillVersion} from ${skillPath}; skillSource=${explicitSkillPath ? "path" : config.governance.skillSource}; routing=${config.routing.mode}`);
581
726
  }
582
727
 
583
728
  function isSubagentSession(agent) {
@@ -60,6 +60,16 @@ const IRREVERSIBLE_ACTION_PATTERNS = [
60
60
  /\b(?:delete|drop|clear|migrate|deploy|release|disable)\b/iu,
61
61
  ];
62
62
 
63
+ const CONTINUATION_PATTERNS = [
64
+ /(?:继续|接着|进一步|深入|再(?:判断|分析|评估|检查|处理)|按(?:照)?(?:刚才|上面|上述|前面|这个|该)|那就|就按)/iu,
65
+ /\b(?:continue|proceed|go ahead|follow up|dig deeper|based on (?:that|the previous|the above))\b/iu,
66
+ ];
67
+
68
+ const LOW_RISK_TRANSFORM_PATTERNS = [
69
+ /(?:重述|总结|概括|压缩|翻译|改写|润色|格式化|解释(?:一下)?)/iu,
70
+ /\b(?:restate|summari[sz]e|translate|shorten|rewrite|format|explain)\b/iu,
71
+ ];
72
+
63
73
  function matchesAny(text, patterns) {
64
74
  return patterns.some((pattern) => pattern.test(text));
65
75
  }
@@ -72,6 +82,48 @@ function stripQuotedMaterial(text) {
72
82
  .replace(/“[^”\n]*”|‘[^’\n]*’|「[^」\n]*」|『[^』\n]*』|《[^》\n]*》|"[^"\n]*"/gu, " ");
73
83
  }
74
84
 
85
+ function hasContextualPlannerGap(text) {
86
+ return matchesAny(text, RISK_PATTERNS)
87
+ && matchesAny(text, UNVERIFIED_CAUSAL_PATTERNS)
88
+ && matchesAny(text, CONCRETE_CHANGE_PATTERNS)
89
+ && (matchesAny(text, SPECIFIC_PARAMETER_PATTERNS)
90
+ || matchesAny(text, URGENCY_PATTERNS)
91
+ || matchesAny(text, IRREVERSIBLE_ACTION_PATTERNS));
92
+ }
93
+
94
+ function isLowRiskTransform(text) {
95
+ return matchesAny(stripQuotedMaterial(text), LOW_RISK_TRANSFORM_PATTERNS);
96
+ }
97
+
98
+ function genuineUserText(message) {
99
+ if (message?.role !== "user" || message?.source?.kind !== "user" || !Array.isArray(message.content)) return "";
100
+ return message.content
101
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
102
+ .map((block) => block.text)
103
+ .join("\n")
104
+ .trim();
105
+ }
106
+
107
+ function genuineUserTexts(messages, sessionEvents = []) {
108
+ const candidates = [
109
+ ...(Array.isArray(messages) ? [...messages].reverse() : []),
110
+ ...(Array.isArray(sessionEvents)
111
+ ? [...sessionEvents].reverse()
112
+ .filter((event) => event?.type === "user/message")
113
+ .map((event) => event.data)
114
+ : []),
115
+ ];
116
+ const seen = new Set();
117
+ const texts = [];
118
+ for (const message of candidates) {
119
+ const text = genuineUserText(message);
120
+ if (!text || seen.has(text)) continue;
121
+ seen.add(text);
122
+ texts.push(text);
123
+ }
124
+ return texts;
125
+ }
126
+
75
127
  function route(role, reasonCode, reason, signals, action = role === "controller" ? "direct" : "delegate", targetRole) {
76
128
  return Object.freeze({
77
129
  role,
@@ -137,11 +189,11 @@ export function decideRoute(input = {}) {
137
189
  );
138
190
  }
139
191
 
140
- const hasContextualPlannerGap = riskPresent
192
+ const contextualPlannerGap = riskPresent
141
193
  && unverifiedCausalClaim
142
194
  && concreteChangeRequest
143
195
  && (specificOperationalParameter || urgencyPressure || irreversibleAction);
144
- if (hasContextualPlannerGap) {
196
+ if (contextualPlannerGap) {
145
197
  return route(
146
198
  "controller",
147
199
  HIGH_IMPACT_PLANNER_REASON,
@@ -162,23 +214,29 @@ export function decideRoute(input = {}) {
162
214
  }
163
215
 
164
216
  export function extractLatestUserText(messages) {
165
- if (!Array.isArray(messages)) return "";
166
-
167
- for (let index = messages.length - 1; index >= 0; index -= 1) {
168
- const message = messages[index];
169
- if (message?.role !== "user") continue;
170
- if (message?.source?.kind !== "user") continue;
171
- if (!Array.isArray(message.content)) continue;
172
-
173
- const text = message.content
174
- .filter((block) => block?.type === "text" && typeof block.text === "string")
175
- .map((block) => block.text)
176
- .join("\n")
177
- .trim();
178
- if (text) return text;
217
+ return genuineUserTexts(messages)[0] ?? "";
218
+ }
219
+
220
+ export function extractRoutingText(messages, sessionEvents) {
221
+ const texts = genuineUserTexts(messages, sessionEvents);
222
+ const latest = texts[0] ?? "";
223
+ if (!latest
224
+ || hasContextualPlannerGap(latest)
225
+ || !matchesAny(stripQuotedMaterial(latest), CONTINUATION_PATTERNS)
226
+ || isLowRiskTransform(latest)) {
227
+ return latest;
179
228
  }
180
229
 
181
- return "";
230
+ let referencedHighImpact;
231
+ for (const text of texts.slice(1)) {
232
+ if (hasContextualPlannerGap(text)) {
233
+ referencedHighImpact = text;
234
+ break;
235
+ }
236
+ if (!isLowRiskTransform(text)) break;
237
+ }
238
+ if (!referencedHighImpact) return latest;
239
+ return `${latest}\n\nReferenced earlier high-impact user context:\n${referencedHighImpact}`;
182
240
  }
183
241
 
184
242
  export function requiresFailClosedProtection(decision) {
@@ -283,6 +341,7 @@ export function renderDelegationPrompt(decision, taskText, roleContract) {
283
341
  `You are the odai ${decision.role}.`,
284
342
  roleContract.trim(),
285
343
  "Runtime boundary: do not edit files, run shell commands, ask the user, or delegate further. The controller owns all final decisions and delivery.",
344
+ "Context boundary: this is a bounded task and evidence packet, not an inherited controller transcript. Use only the supplied contract, task, evidence, and source pointers; do not request or reconstruct the controller's full history.",
286
345
  "",
287
346
  "Task:",
288
347
  taskText,