@sema-agent/client-core 0.6.0 → 0.8.0

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 (68) hide show
  1. package/README.md +31 -11
  2. package/dist/adapt.d.ts +4 -2
  3. package/dist/adapt.js +225 -25
  4. package/dist/compensations.d.ts +63 -0
  5. package/dist/compensations.js +138 -0
  6. package/dist/detachWire.d.ts +141 -0
  7. package/dist/detachWire.js +182 -0
  8. package/dist/diff/patch.d.ts +29 -0
  9. package/dist/diff/patch.js +45 -0
  10. package/dist/engineSessionParam.d.ts +11 -0
  11. package/dist/engineSessionParam.js +41 -0
  12. package/dist/engineWireTarget.d.ts +14 -0
  13. package/dist/engineWireTarget.js +53 -0
  14. package/dist/finalVerifyWire.d.ts +74 -0
  15. package/dist/finalVerifyWire.js +63 -0
  16. package/dist/fleet/fleetLedger.d.ts +132 -0
  17. package/dist/fleet/fleetLedger.js +393 -0
  18. package/dist/fleet/fleetProjection.d.ts +181 -0
  19. package/dist/fleet/fleetProjection.js +258 -0
  20. package/dist/headlessPermissionModeWire.d.ts +55 -0
  21. package/dist/headlessPermissionModeWire.js +111 -0
  22. package/dist/headlessReconnectWire.d.ts +96 -0
  23. package/dist/headlessReconnectWire.js +141 -0
  24. package/dist/hooksWireCaps.d.ts +91 -0
  25. package/dist/hooksWireCaps.js +359 -0
  26. package/dist/host.d.ts +34 -2
  27. package/dist/host.js +6 -0
  28. package/dist/index.d.ts +74 -1
  29. package/dist/index.js +104 -1
  30. package/dist/interactiveToolsWire.d.ts +48 -0
  31. package/dist/interactiveToolsWire.js +86 -0
  32. package/dist/limitsWire.d.ts +89 -0
  33. package/dist/limitsWire.js +225 -0
  34. package/dist/liveInitToolFace.d.ts +53 -0
  35. package/dist/liveInitToolFace.js +251 -0
  36. package/dist/model/modelFamilies.json +63 -0
  37. package/dist/model/providerPresets.d.ts +46 -0
  38. package/dist/model/providerPresets.js +160 -0
  39. package/dist/model/providerPresets.json +1179 -0
  40. package/dist/notifications.d.ts +7 -0
  41. package/dist/notifications.js +32 -0
  42. package/dist/sandboxWire.d.ts +75 -0
  43. package/dist/sandboxWire.js +138 -0
  44. package/dist/scenarioWire.d.ts +62 -0
  45. package/dist/scenarioWire.js +115 -0
  46. package/dist/seam.d.ts +52 -5
  47. package/dist/seam.js +15 -2
  48. package/dist/subagent/engineCompactWire.d.ts +12 -0
  49. package/dist/subagent/engineCompactWire.js +176 -0
  50. package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
  51. package/dist/subagent/engineDelegatedPrompt.js +205 -0
  52. package/dist/subagent/engineRowStopGate.d.ts +18 -0
  53. package/dist/subagent/engineRowStopGate.js +54 -0
  54. package/dist/subagent/engineSubagentOutput.d.ts +15 -0
  55. package/dist/subagent/engineSubagentOutput.js +98 -0
  56. package/dist/subagent/engineSubagentSteer.d.ts +10 -0
  57. package/dist/subagent/engineSubagentSteer.js +68 -0
  58. package/dist/subagent/engineSubagentTail.d.ts +40 -0
  59. package/dist/subagent/engineSubagentTail.js +241 -0
  60. package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
  61. package/dist/subagent/engineTaskHandleWire.js +212 -0
  62. package/dist/toolResult.d.ts +118 -0
  63. package/dist/toolResult.js +774 -0
  64. package/dist/workflowClient.d.ts +42 -0
  65. package/dist/workflowClient.js +467 -0
  66. package/dist/workflowMonitor.d.ts +94 -0
  67. package/dist/workflowMonitor.js +43 -0
  68. package/package.json +5 -3
@@ -90,6 +90,13 @@ export declare function dropQueuedNotificationsForRun(taskId: string): number;
90
90
  * steer-inject 的 task_notification 帧)即预标记,Channel A 对同 runId 的补发直接丢弃。
91
91
  */
92
92
  export declare function markEngineWorkflowNotified(runId: string): void;
93
+ /** 跨通道去重的**读**口(适配器 workflow_complete 臂的发射门;cli 同域)。 */
94
+ export declare function isEngineWorkflowNotified(runId: string): boolean;
95
+ /** 「这个 run 的完成卡已入队」的**写**口(cli 侧等价物 = 两个 enqueue 函数内的 `cardEnqueuedRunIds.add`)。 */
96
+ export declare function noteWorkflowCompletionCardEnqueued(runId: string): void;
97
+ /** 台账序列化面(`exportLedger`)—— 只读快照,顺序 = 插入序。 */
98
+ export declare function listNotifiedRuns(): string[];
99
+ export declare function listWorkflowCompletionCardsEnqueued(): string[];
93
100
  export declare function isWorkflowCompletionCardEnqueued(runId: string): boolean;
94
101
  type WorkflowStatusProbe = (runId: string) => Promise<{
95
102
  terminal: boolean;
@@ -215,6 +215,38 @@ export function markEngineWorkflowNotified(runId) {
215
215
  if (outstandingRuns.delete(runId))
216
216
  notifyOutstanding();
217
217
  }
218
+ // ══════════════════════════════════════════════════════════════════════════════════════════════
219
+ // B5 ①(0.7.0)—— **配对台账折回**([1857];记忆 paired-mechanisms-must-share-premise)。
220
+ // 0.6.0 把 `notifiedRuns` / `cardEnqueuedRuns` 放在**适配器实例**上,而与它们配对的另一半全在
221
+ // 本模块的 module 台账上:清账的 `dropQueuedNotificationsForRun`、记账的
222
+ // `enqueueBgChildNotification` / `enqueueEngineWorkflowNotification`、以及 idle watcher 那条
223
+ // (壳根本拦不到的调用点)。两半前提不共享 = 四个方向同时错:
224
+ // ① module 记过账 ⇒ 实例不知道 ⇒ 推送帧渲第二张卡(双卡)
225
+ // ② 宿主回钩写实例、drop 清 module ⇒ 实例只增不减 ⇒ 那一行永不上屏(a0110e88 复发)
226
+ // ③④ 跨通道去重两向失效 ⇒ 同一完成二次喂模型
227
+ // 修法 = 台账**整体**回到 module(与 cli 的 module 级 Set 逐字同域),适配器只经下面这四个口读写;
228
+ // 实例上只留 `renderedNotifications`(渲染去重,天然 per-session)与 `firedSubagentStartHookTaskIds`
229
+ // (DIVERGENCE-6 有意的实例级)。
230
+ // 🔴 多 session 宿主(web 一个页面两个会话)上本对台账是**进程级共享**的 —— 这与 cli 一致,
231
+ // 且必须如此:队列 port、idle watcher、outstanding 药丸也都是进程级的,台账单独 per-session
232
+ // 就又造出一对「前提不共享」的机制。
233
+ // ══════════════════════════════════════════════════════════════════════════════════════════════
234
+ /** 跨通道去重的**读**口(适配器 workflow_complete 臂的发射门;cli 同域)。 */
235
+ export function isEngineWorkflowNotified(runId) {
236
+ return notifiedRunIds.has(runId);
237
+ }
238
+ /** 「这个 run 的完成卡已入队」的**写**口(cli 侧等价物 = 两个 enqueue 函数内的 `cardEnqueuedRunIds.add`)。 */
239
+ export function noteWorkflowCompletionCardEnqueued(runId) {
240
+ if (runId.length > 0)
241
+ cardEnqueuedRunIds.add(runId);
242
+ }
243
+ /** 台账序列化面(`exportLedger`)—— 只读快照,顺序 = 插入序。 */
244
+ export function listNotifiedRuns() {
245
+ return [...notifiedRunIds];
246
+ }
247
+ export function listWorkflowCompletionCardsEnqueued() {
248
+ return [...cardEnqueuedRunIds];
249
+ }
218
250
  /** 双通道「完成卡」去重(clay dogfood 2026-07-19 双通知案):与 notifiedRunIds(模型通知去重,
219
251
  * external mark 也写它)**分键**——本集只由 probe 合成 enqueue 写入(workflow+bg agent 两族,
220
252
  * 对抗复审§1:bg 合成不写此集时 probe 先到→后到推送帧仍渲第二张卡)、只由推送帧渲染前读,
@@ -0,0 +1,75 @@
1
+ /**
2
+ * src/sema/sandboxWire.ts — [686]② `--sandbox` TRI-STATE flag (clay 拍, 2026-07-13): the CLI face of the
3
+ * per-task SANDBOX IMAGE PROFILE the server already ships end-to-end (`TaskRequest.sandboxImageProfile`
4
+ * per-task binding + the model-facing `SelectEnvironment` tool + the `GET /v1/images` catalog).
5
+ *
6
+ * TRI-STATE (clay 口径):
7
+ * a) DEFAULT (no flag) → NO `sandboxImageProfile` on the POST body — the worker's default image runs.
8
+ * b) `--sandbox <profile>` → EXPLICIT: stamp `sandboxImageProfile` (SDK types.ts:115 → service
9
+ * main.ts:2316 resolves profile→digest FAIL-CLOSED with the caller's principal — we only ever send the
10
+ * PROFILE, never a digest). Vocabulary = the live catalog (`sema cloud images list` / GET /v1/images) —
11
+ * the shell does NOT pre-validate against it (fail-closed admission is the server's job; a typo comes
12
+ * back as an honest 404, mapped to a hint by sandboxDegradeHint below).
13
+ * c) `--sandbox auto` → ADVISORY: the submit carries NO profile; instead the FIRST live turn's
14
+ * objective is prefixed with an advisory block asking the model to drive the `SelectEnvironment` tool
15
+ * itself (list → pick → report `Sandbox: <profile> — <reason>` in its reply, so the choice + reason are
16
+ * visible on the run card and correctable by the user). NOT a hard route — server keeps zero new
17
+ * endpoints; the tool's session binding takes effect from the NEXT task boundary ([440] F3 contract).
18
+ *
19
+ * LANE HONESTY (诚实不撒谎 UI, [686] constraint): the profile only works on the k8s sandbox lane — an
20
+ * e2b/host-lane worker (including the local TOC self-spawned engine) 400s the submit honestly
21
+ * (service main.ts:2323 "sandboxImageProfile is only supported on the k8s sandbox backend"). The shell does
22
+ * NOT pre-judge the lane (no caps sniffing before submit — per [686]: "不预判 lane,收到 400 再提示");
23
+ * it maps the server's honest reject into a degradation hint (sandboxDegradeHint) rendered on the error row.
24
+ * So on the local self-spawned engine `--sandbox <profile>` = a LOUD per-turn failure with an actionable
25
+ * hint — never a silent ignore (silently dropping the user's explicit environment choice would be lying).
26
+ */
27
+ export type SandboxSelection = {
28
+ mode: 'default';
29
+ } | {
30
+ mode: 'auto';
31
+ } | {
32
+ mode: 'explicit';
33
+ profile: string;
34
+ };
35
+ export type SandboxParseResult = {
36
+ ok: true;
37
+ sel: SandboxSelection;
38
+ } | {
39
+ ok: false;
40
+ error: string;
41
+ };
42
+ /**
43
+ * Parse `--sandbox` out of an argv slice (everything before a bare `--`; both `--sandbox <v>` and
44
+ * `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
45
+ * Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
46
+ * a mis-typed environment choice must not quietly run on the wrong image).
47
+ */
48
+ export declare function parseSandboxArgv(argv: string[]): SandboxParseResult;
49
+ /** The request-body stamp for a selection: EXPLICIT ⇒ `{sandboxImageProfile}`, default/auto ⇒ nothing
50
+ * (auto is advisory-by-prompt — the submit itself never carries a profile, per clay's c) 口径). */
51
+ export declare function sandboxRequestFields(sel: SandboxSelection | undefined): {
52
+ sandboxImageProfile?: string;
53
+ };
54
+ /**
55
+ * The `--sandbox auto` advisory block. Design choices (each deliberate):
56
+ * - Conditional on the tool being mounted: `SelectEnvironment` reaches the model via the deployment's
57
+ * `spec.tools` injection (service select-environment-tool.ts RFC A2) — a worker without it must degrade
58
+ * HONESTLY (model says so and continues on the default), not hallucinate a selection.
59
+ * - The `Sandbox: <profile> — <reason>` reply line IS the run-card echo (可观测可纠正): the tool call
60
+ * renders as a normal tool card and the choice+reason line lands in the assistant reply — zero new
61
+ * renderer machinery, zero new server endpoints.
62
+ * - Boundary honesty: the server applies a session's selection from the NEXT task boundary ([440] F3),
63
+ * so the advisory says so — the model must not promise the current turn already runs on the new image.
64
+ */
65
+ export declare const AUTO_SANDBOX_ADVISORY: string;
66
+ /** Prefix an objective with the auto-select advisory (one-shot — the caller owns the latch). */
67
+ export declare function wrapObjectiveWithAutoSandboxAdvisory(objective: string): string;
68
+ /**
69
+ * Map a submit failure → the sandbox DEGRADATION HINT, or undefined when the error is not sandbox-shaped.
70
+ * Keys on the server's honest reject texts (ai-agent-service main.ts:2323-2330, per-task-image.ts:52,
71
+ * server.ts:4651/4670) — status+substring, the same recognize-by-wire-shape idiom the quota/scenario
72
+ * copy modules use. The caller renders it under the raw "API Error: …" row (original message preserved —
73
+ * the hint supplements, never masks, the server's own words).
74
+ */
75
+ export declare function sandboxDegradeHint(err: unknown): string | undefined;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * src/sema/sandboxWire.ts — [686]② `--sandbox` TRI-STATE flag (clay 拍, 2026-07-13): the CLI face of the
3
+ * per-task SANDBOX IMAGE PROFILE the server already ships end-to-end (`TaskRequest.sandboxImageProfile`
4
+ * per-task binding + the model-facing `SelectEnvironment` tool + the `GET /v1/images` catalog).
5
+ *
6
+ * TRI-STATE (clay 口径):
7
+ * a) DEFAULT (no flag) → NO `sandboxImageProfile` on the POST body — the worker's default image runs.
8
+ * b) `--sandbox <profile>` → EXPLICIT: stamp `sandboxImageProfile` (SDK types.ts:115 → service
9
+ * main.ts:2316 resolves profile→digest FAIL-CLOSED with the caller's principal — we only ever send the
10
+ * PROFILE, never a digest). Vocabulary = the live catalog (`sema cloud images list` / GET /v1/images) —
11
+ * the shell does NOT pre-validate against it (fail-closed admission is the server's job; a typo comes
12
+ * back as an honest 404, mapped to a hint by sandboxDegradeHint below).
13
+ * c) `--sandbox auto` → ADVISORY: the submit carries NO profile; instead the FIRST live turn's
14
+ * objective is prefixed with an advisory block asking the model to drive the `SelectEnvironment` tool
15
+ * itself (list → pick → report `Sandbox: <profile> — <reason>` in its reply, so the choice + reason are
16
+ * visible on the run card and correctable by the user). NOT a hard route — server keeps zero new
17
+ * endpoints; the tool's session binding takes effect from the NEXT task boundary ([440] F3 contract).
18
+ *
19
+ * LANE HONESTY (诚实不撒谎 UI, [686] constraint): the profile only works on the k8s sandbox lane — an
20
+ * e2b/host-lane worker (including the local TOC self-spawned engine) 400s the submit honestly
21
+ * (service main.ts:2323 "sandboxImageProfile is only supported on the k8s sandbox backend"). The shell does
22
+ * NOT pre-judge the lane (no caps sniffing before submit — per [686]: "不预判 lane,收到 400 再提示");
23
+ * it maps the server's honest reject into a degradation hint (sandboxDegradeHint) rendered on the error row.
24
+ * So on the local self-spawned engine `--sandbox <profile>` = a LOUD per-turn failure with an actionable
25
+ * hint — never a silent ignore (silently dropping the user's explicit environment choice would be lying).
26
+ */
27
+ /** Mirror of the server's own body validation (server.ts:4651): non-empty, ≤128 chars. Validating locally
28
+ * just converts a guaranteed 400 into an immediate CLI error — same rule, earlier and cheaper. */
29
+ const MAX_PROFILE_LEN = 128;
30
+ const USAGE_HINT = "sema: --sandbox expects a profile name or 'auto'.\n" +
31
+ ' --sandbox <profile> run on that sandbox image profile (k8s-lane workers; list: sema cloud images list)\n' +
32
+ ' --sandbox auto let the model pick via its SelectEnvironment tool (advisory — choice + reason shown in the reply)\n' +
33
+ ' (no flag) the worker default image';
34
+ /**
35
+ * Parse `--sandbox` out of an argv slice (everything before a bare `--`; both `--sandbox <v>` and
36
+ * `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
37
+ * Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
38
+ * a mis-typed environment choice must not quietly run on the wrong image).
39
+ */
40
+ export function parseSandboxArgv(argv) {
41
+ let sel = { mode: 'default' };
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const a = argv[i];
44
+ if (a === '--')
45
+ break; // positionals — never flags
46
+ let raw;
47
+ if (a === '--sandbox') {
48
+ const nxt = argv[i + 1];
49
+ if (nxt === undefined || nxt.startsWith('-')) {
50
+ return { ok: false, error: USAGE_HINT };
51
+ }
52
+ raw = nxt;
53
+ i++; // consume the value token
54
+ }
55
+ else if (a.startsWith('--sandbox=')) {
56
+ raw = a.slice('--sandbox='.length);
57
+ }
58
+ else {
59
+ continue;
60
+ }
61
+ const v = raw.trim();
62
+ if (v.length === 0)
63
+ return { ok: false, error: USAGE_HINT };
64
+ // audit #17: a bare `--sandbox` EATS the following positional — `-p --sandbox 'say hi'`
65
+ // consumed the prompt as the profile, then died on an unrelated "Input must be provided…"
66
+ // error. Profile names never contain whitespace; a value that does is almost certainly the
67
+ // user's prompt — say exactly that instead of silently mis-parsing.
68
+ if (/\s/.test(v)) {
69
+ return {
70
+ ok: false,
71
+ error: `Invalid sandbox profile "${v}". Profile names cannot contain spaces — if you meant it as the prompt, ` +
72
+ `pass --sandbox a profile name (or 'auto') first, or omit --sandbox to use the worker default image.\n` +
73
+ USAGE_HINT,
74
+ };
75
+ }
76
+ if (v.length > MAX_PROFILE_LEN) {
77
+ return {
78
+ ok: false,
79
+ error: `sema: --sandbox profile must be at most ${MAX_PROFILE_LEN} characters (same rule the server enforces).`,
80
+ };
81
+ }
82
+ sel = v === 'auto' ? { mode: 'auto' } : { mode: 'explicit', profile: v };
83
+ }
84
+ return { ok: true, sel };
85
+ }
86
+ /** The request-body stamp for a selection: EXPLICIT ⇒ `{sandboxImageProfile}`, default/auto ⇒ nothing
87
+ * (auto is advisory-by-prompt — the submit itself never carries a profile, per clay's c) 口径). */
88
+ export function sandboxRequestFields(sel) {
89
+ return sel?.mode === 'explicit' ? { sandboxImageProfile: sel.profile } : {};
90
+ }
91
+ /**
92
+ * The `--sandbox auto` advisory block. Design choices (each deliberate):
93
+ * - Conditional on the tool being mounted: `SelectEnvironment` reaches the model via the deployment's
94
+ * `spec.tools` injection (service select-environment-tool.ts RFC A2) — a worker without it must degrade
95
+ * HONESTLY (model says so and continues on the default), not hallucinate a selection.
96
+ * - The `Sandbox: <profile> — <reason>` reply line IS the run-card echo (可观测可纠正): the tool call
97
+ * renders as a normal tool card and the choice+reason line lands in the assistant reply — zero new
98
+ * renderer machinery, zero new server endpoints.
99
+ * - Boundary honesty: the server applies a session's selection from the NEXT task boundary ([440] F3),
100
+ * so the advisory says so — the model must not promise the current turn already runs on the new image.
101
+ */
102
+ export const AUTO_SANDBOX_ADVISORY = '<sandbox-auto-select>\n' +
103
+ 'The user launched this session with `--sandbox auto`: choose the best sandbox environment (image profile) for the work in this session yourself.\n' +
104
+ 'If a SelectEnvironment tool is available: call it with no arguments to list the available profiles and their capabilities, then select the one that best fits the task (keep the default if nothing fits better). After deciding, report your choice on its own line, exactly in this form: `Sandbox: <profile> — <one-line reason>` (use `Sandbox: default — <reason>` if you kept the default). Note the selection takes effect from the next task boundary — the current turn keeps the environment it started with.\n' +
105
+ 'If no SelectEnvironment tool is available, briefly say so and continue in the default environment.\n' +
106
+ '</sandbox-auto-select>';
107
+ /** Prefix an objective with the auto-select advisory (one-shot — the caller owns the latch). */
108
+ export function wrapObjectiveWithAutoSandboxAdvisory(objective) {
109
+ return `${AUTO_SANDBOX_ADVISORY}\n\n${objective}`;
110
+ }
111
+ /**
112
+ * Map a submit failure → the sandbox DEGRADATION HINT, or undefined when the error is not sandbox-shaped.
113
+ * Keys on the server's honest reject texts (ai-agent-service main.ts:2323-2330, per-task-image.ts:52,
114
+ * server.ts:4651/4670) — status+substring, the same recognize-by-wire-shape idiom the quota/scenario
115
+ * copy modules use. The caller renders it under the raw "API Error: …" row (original message preserved —
116
+ * the hint supplements, never masks, the server's own words).
117
+ */
118
+ export function sandboxDegradeHint(err) {
119
+ const e = err;
120
+ if (typeof e?.status !== 'number' || typeof e?.message !== 'string')
121
+ return undefined;
122
+ const msg = e.message;
123
+ if (e.status === 400 && msg.includes('only supported on the k8s sandbox backend')) {
124
+ return ('--sandbox <profile> only applies on k8s-lane workers; this engine runs a host/e2b sandbox lane, so the request was honestly rejected.\n' +
125
+ 'Re-run without --sandbox (worker default image), or point the shell at a k8s-lane worker (sema cloud engine connect <url>).');
126
+ }
127
+ if (e.status === 400 && msg.includes('requires the image index')) {
128
+ return ('This worker has no sandbox image index configured (sema-registry backend absent), so per-task profiles cannot resolve.\n' +
129
+ 'Re-run without --sandbox, or use a worker with the image index enabled.');
130
+ }
131
+ if (e.status === 404 && msg.includes('no published sandbox image for profile')) {
132
+ return 'No published image matches that profile for your principal. List what you can use: sema cloud images list';
133
+ }
134
+ if (e.status === 400 && msg.includes('not supported together with cascade/verify')) {
135
+ return '--sandbox cannot combine with cascade/verify submits in v1 (the per-task image would not bind to the sub-runs). Drop one of the two.';
136
+ }
137
+ return undefined;
138
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * src/sema/scenarioWire.ts — `--scenario <name>` wire (clay 拍, 2026-07-15): expose the engine's
3
+ * REQUEST-LEVEL scenario routing axis on the headless `-p` path. The server has shipped scenario
4
+ * routing since day one (ai-agent-service capabilities/scenarios.ts — `TaskRequest.scenario` picks the
5
+ * tool roster + prompt provider per request: default / code-review / scan / oa / team + center-declared
6
+ * overlays); the shell simply never exposed the field to users. This module is the CLI face.
7
+ *
8
+ * PRECEDENCE (flag > settings > default):
9
+ * a) `--scenario <name>` (explicit flag) → stamp `TaskRequest.scenario`.
10
+ * b) `SEMA_HEADLESS_SCENARIO` env — the settings lane: → stamp when no flag. Set it in the user
11
+ * settings.json `env` block (seeded into process.env by the SAME 1a-envseed both the REPL boot and
12
+ * printModeEngine run), the lane every other wire config rides (SEMA_SELF_ORCHESTRATION,
13
+ * SEMA_WEBSEARCH_*, SEMA_ENABLE_FORK). One mechanism serves both "env var" and "settings file".
14
+ * c) neither → NO stamp = today's behaviour, verbatim.
15
+ *
16
+ * VOCABULARY = the server's scenario registry (builtin + center-declared). The shell does NOT
17
+ * pre-validate against it (the registry is deployment-side truth): an UNKNOWN name falls back to the
18
+ * `default` scenario server-side (scenarios.ts selectScenario — safe), and a center allowlist violation
19
+ * comes back as an honest 400 `scenario_not_allowed` (already rendered by scenarioNotAllowedCopy.ts).
20
+ * We only mirror the server's NAME-SHAPE rule (scenarios.ts:318 SCENARIO_NAME_RE) locally — same rule,
21
+ * earlier and cheaper, and it keeps prompt-looking values (whitespace etc.) from being eaten as a name.
22
+ *
23
+ * NOTE (mock-key shadow): the seam also uses `scenario` as the MOCK transcript selector
24
+ * (seamQuery.selectScenario / SEMA_SCENARIO). liveClient strips those mock keys before a live forward;
25
+ * a `--scenario` value that collides with a mock key (e.g. `bash`) is therefore equivalent to `default`
26
+ * on the live engine — which is exactly what the server would resolve for it anyway (no such scenario).
27
+ *
28
+ * SEMA_SCENARIO (the mock harness opt-in — "never spawn a real engine", printModeEngine.ts) is
29
+ * deliberately NOT reused here: it selects offline mock transcripts, not live engine routing.
30
+ */
31
+ import { type EnvLike } from './hostEnv.js';
32
+ /** Mirror of the server's scenario NAME shape (ai-agent-service scenarios.ts:318). */
33
+ export declare const SCENARIO_NAME_RE: RegExp;
34
+ /** Settings-lane env knob (settings.json `env` block → 1a-envseed → process.env). */
35
+ export declare const HEADLESS_SCENARIO_ENV = "SEMA_HEADLESS_SCENARIO";
36
+ export type ScenarioParseResult = {
37
+ ok: true;
38
+ scenario?: string;
39
+ } | {
40
+ ok: false;
41
+ error: string;
42
+ };
43
+ /**
44
+ * Parse `--scenario` out of an argv slice (everything before a bare `--`; both `--scenario <v>` and
45
+ * `--scenario=<v>` forms — the sandboxWire idiom). No flag ⇒ `{ok:true}` with no scenario. Repeated
46
+ * flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
47
+ * fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
48
+ * the wrong tool surface; same stance as `--sandbox`).
49
+ */
50
+ export declare function parseScenarioArgv(argv: string[]): ScenarioParseResult;
51
+ /**
52
+ * The settings-lane default (`SEMA_HEADLESS_SCENARIO`). FAIL-SOFT on a bad value (stderr warning +
53
+ * no stamp): a settings-file typo must not hard-brick every headless run the way an explicit flag
54
+ * typo fails the one run it was typed on.
55
+ */
56
+ export declare function headlessScenarioFromEnv(env?: EnvLike): string | undefined;
57
+ /**
58
+ * Resolve the scenario for a headless `-p` submit: explicit `--scenario` flag > `SEMA_HEADLESS_SCENARIO`
59
+ * settings default > none (= the engine's default scenario, today's behaviour). Flag parse errors
60
+ * propagate fail-loud; the caller exits.
61
+ */
62
+ export declare function scenarioForPrint(argv: string[], env?: EnvLike): ScenarioParseResult;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * src/sema/scenarioWire.ts — `--scenario <name>` wire (clay 拍, 2026-07-15): expose the engine's
3
+ * REQUEST-LEVEL scenario routing axis on the headless `-p` path. The server has shipped scenario
4
+ * routing since day one (ai-agent-service capabilities/scenarios.ts — `TaskRequest.scenario` picks the
5
+ * tool roster + prompt provider per request: default / code-review / scan / oa / team + center-declared
6
+ * overlays); the shell simply never exposed the field to users. This module is the CLI face.
7
+ *
8
+ * PRECEDENCE (flag > settings > default):
9
+ * a) `--scenario <name>` (explicit flag) → stamp `TaskRequest.scenario`.
10
+ * b) `SEMA_HEADLESS_SCENARIO` env — the settings lane: → stamp when no flag. Set it in the user
11
+ * settings.json `env` block (seeded into process.env by the SAME 1a-envseed both the REPL boot and
12
+ * printModeEngine run), the lane every other wire config rides (SEMA_SELF_ORCHESTRATION,
13
+ * SEMA_WEBSEARCH_*, SEMA_ENABLE_FORK). One mechanism serves both "env var" and "settings file".
14
+ * c) neither → NO stamp = today's behaviour, verbatim.
15
+ *
16
+ * VOCABULARY = the server's scenario registry (builtin + center-declared). The shell does NOT
17
+ * pre-validate against it (the registry is deployment-side truth): an UNKNOWN name falls back to the
18
+ * `default` scenario server-side (scenarios.ts selectScenario — safe), and a center allowlist violation
19
+ * comes back as an honest 400 `scenario_not_allowed` (already rendered by scenarioNotAllowedCopy.ts).
20
+ * We only mirror the server's NAME-SHAPE rule (scenarios.ts:318 SCENARIO_NAME_RE) locally — same rule,
21
+ * earlier and cheaper, and it keeps prompt-looking values (whitespace etc.) from being eaten as a name.
22
+ *
23
+ * NOTE (mock-key shadow): the seam also uses `scenario` as the MOCK transcript selector
24
+ * (seamQuery.selectScenario / SEMA_SCENARIO). liveClient strips those mock keys before a live forward;
25
+ * a `--scenario` value that collides with a mock key (e.g. `bash`) is therefore equivalent to `default`
26
+ * on the live engine — which is exactly what the server would resolve for it anyway (no such scenario).
27
+ *
28
+ * SEMA_SCENARIO (the mock harness opt-in — "never spawn a real engine", printModeEngine.ts) is
29
+ * deliberately NOT reused here: it selects offline mock transcripts, not live engine routing.
30
+ */
31
+ import { hostEnv } from './hostEnv.js';
32
+ /** Mirror of the server's scenario NAME shape (ai-agent-service scenarios.ts:318). */
33
+ export const SCENARIO_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
34
+ /** Settings-lane env knob (settings.json `env` block → 1a-envseed → process.env). */
35
+ export const HEADLESS_SCENARIO_ENV = 'SEMA_HEADLESS_SCENARIO';
36
+ const USAGE_HINT = "sema: --scenario expects a scenario name (lowercase letters/digits/hyphens, starting with a letter, ≤32 chars).\n" +
37
+ ' --scenario <name> route this run through the named engine scenario (deployment-defined tool\n' +
38
+ ' roster + system prompt; unknown names fall back to the default scenario)\n' +
39
+ ' (no flag) the default scenario — today\'s behaviour';
40
+ /**
41
+ * Parse `--scenario` out of an argv slice (everything before a bare `--`; both `--scenario <v>` and
42
+ * `--scenario=<v>` forms — the sandboxWire idiom). No flag ⇒ `{ok:true}` with no scenario. Repeated
43
+ * flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
44
+ * fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
45
+ * the wrong tool surface; same stance as `--sandbox`).
46
+ */
47
+ export function parseScenarioArgv(argv) {
48
+ let scenario;
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const a = argv[i];
51
+ if (a === '--')
52
+ break; // positionals — never flags
53
+ let raw;
54
+ if (a === '--scenario') {
55
+ const nxt = argv[i + 1];
56
+ if (nxt === undefined || nxt.startsWith('-')) {
57
+ return { ok: false, error: USAGE_HINT };
58
+ }
59
+ raw = nxt;
60
+ i++; // consume the value token
61
+ }
62
+ else if (a.startsWith('--scenario=')) {
63
+ raw = a.slice('--scenario='.length);
64
+ }
65
+ else {
66
+ continue;
67
+ }
68
+ const v = raw.trim();
69
+ // sandboxWire audit #17 lesson: a bare `--scenario` EATS the following positional — a value with
70
+ // whitespace is almost certainly the user's prompt; say exactly that instead of mis-parsing.
71
+ if (/\s/.test(v)) {
72
+ return {
73
+ ok: false,
74
+ error: `sema: --scenario got "${v.length > 48 ? `${v.slice(0, 48)}…` : v}" — that looks like your prompt, not a scenario name.\n` +
75
+ 'Put the flag AFTER the value form (--scenario=<name>) or keep the name and prompt as separate tokens.\n' +
76
+ USAGE_HINT,
77
+ };
78
+ }
79
+ if (!SCENARIO_NAME_RE.test(v)) {
80
+ return { ok: false, error: USAGE_HINT };
81
+ }
82
+ scenario = v;
83
+ }
84
+ return { ok: true, ...(scenario ? { scenario } : {}) };
85
+ }
86
+ /**
87
+ * The settings-lane default (`SEMA_HEADLESS_SCENARIO`). FAIL-SOFT on a bad value (stderr warning +
88
+ * no stamp): a settings-file typo must not hard-brick every headless run the way an explicit flag
89
+ * typo fails the one run it was typed on.
90
+ */
91
+ export function headlessScenarioFromEnv(env = hostEnv()) {
92
+ const v = env[HEADLESS_SCENARIO_ENV]?.trim();
93
+ if (!v)
94
+ return undefined;
95
+ if (!SCENARIO_NAME_RE.test(v)) {
96
+ // eslint-disable-next-line no-console
97
+ console.error(`[sema] ${HEADLESS_SCENARIO_ENV}="${v}" is not a valid scenario name (want ${String(SCENARIO_NAME_RE)}) — ignoring the settings default`);
98
+ return undefined;
99
+ }
100
+ return v;
101
+ }
102
+ /**
103
+ * Resolve the scenario for a headless `-p` submit: explicit `--scenario` flag > `SEMA_HEADLESS_SCENARIO`
104
+ * settings default > none (= the engine's default scenario, today's behaviour). Flag parse errors
105
+ * propagate fail-loud; the caller exits.
106
+ */
107
+ export function scenarioForPrint(argv, env = hostEnv()) {
108
+ const parsed = parseScenarioArgv(argv);
109
+ if (!parsed.ok)
110
+ return parsed;
111
+ if (parsed.scenario)
112
+ return parsed;
113
+ const fromEnv = headlessScenarioFromEnv(env);
114
+ return { ok: true, ...(fromEnv ? { scenario: fromEnv } : {}) };
115
+ }
package/dist/seam.d.ts CHANGED
@@ -98,6 +98,20 @@ export type ChromeEvent = {
98
98
  taskId: string;
99
99
  status: string;
100
100
  outputPath?: string;
101
+ }
102
+ /**
103
+ * B5 新臂(#117a,E 层 Bash 分臂的**执行半场**)—— 模型发起的后台 Bash 回执被认出来了。
104
+ * 判定在库(`detectEngineBgShellReceipt`,纯文案锚定 + `run_in_background` 门);执行留宿主:
105
+ * 宿主消费义务 = ① 把 registration 发给自己的后台 shell 面板 store(壳 =
106
+ * `publishEngineBgShellPanelEvent`)② 登记「句柄 → 宿主 run」映射(壳 = `recordBgParentRun(
107
+ * taskId, getActiveEngineTaskId())`,[1501] A/B 寻址的硬证据)。
108
+ * 缺席 = ctrl+B 面板看不到这条后台命令(不是报错,是这一面在该宿主上哑掉)。
109
+ */
110
+ | {
111
+ kind: 'bgshell_register';
112
+ laneProof: LaneProof;
113
+ taskId: string;
114
+ registration: unknown;
101
115
  } | {
102
116
  kind: 'tasks_expand';
103
117
  laneProof: LaneProof;
@@ -165,8 +179,11 @@ export type ChromeEvent = {
165
179
  * 🔴 配对纪律([paired-mechanisms-must-share-premise]):这两件从「宿主义务」改成「库自持」是
166
180
  * **整臂让位**,不是两边都做——宿主若仍照旧实现,两次 drop/clear 幂等无害,但**记号必须删**,
167
181
  * 否则下一棒会以为库没做。
168
- * 跨通道去重(「补发通道不得再注入模型一遍」)由**适配器自持台账**负责,不摊给宿主——
169
- * 见 adapt.ts 的 notifiedRuns / workflow_notification_enqueue 臂的发射门。
182
+ * 跨通道去重(「补发通道不得再注入模型一遍」)由**库自持**,不摊给宿主 —— B5 起台账回到
183
+ * `notifications.ts`**module 级** `notifiedRunIds` / `cardEnqueuedRunIds`(0.6.0 曾放在适配器
184
+ * 实例上,与清账/记账的另一半不共享前提 = [1857] 那个缺口)。
185
+ * 🔴 宿主**不要**再在本臂上调 `markEngineWorkflowNotified` —— 库已在同一位置调过(幂等,
186
+ * 宿主照调无害,但记号该删:整臂让位,不是两边都做)。
170
187
  * 注:面板行 end 事件本身走 panel_task 臂、SubagentStop 走 subagent_lifecycle 臂,不重复。
171
188
  */
172
189
  | {
@@ -235,9 +252,25 @@ export type ChromeEvent = {
235
252
  }
236
253
  /**
237
254
  * 带外后台 workflow 完成推送(service 1.75 Path B:引擎**没有** server-side 注入,壳负责喂模型)。
238
- * 宿主消费义务:把 `message`(模型面 `<task-notification>` 正文,适配器已按 core 同形铸好)
239
- * 入「完成通知队列」,idle 时 auto-submit 给模型(CC RemoteAgentTask 先例,默认 'later' 优先级)。
240
- * 跨通道去重(同一 run 已由推送帧记账 ⇒ 本臂根本不会发)由适配器台账保证,宿主无需再判。
255
+ * 宿主消费义务 = **只有投递一件**:把 `message`(模型面 `<task-notification>` 正文,适配器已按 core
256
+ * 同形铸好)入「完成通知队列」,idle 时 auto-submit 给模型(CC RemoteAgentTask 先例,默认 'later'
257
+ * 优先级)
258
+ *
259
+ * 🔴 **切缝 = 「门归包、投递归宿主」**([1857] Path B 回归的定谳形,B5 壳侧收批实撞;
260
+ * [paired-mechanisms-must-share-premise]):`workflow_complete` 臂在**发本臂之前**已经在库内
261
+ * 一次做完三件 —— ① 跨通道去重门 `isEngineWorkflowNotified(runId)` 早退;
262
+ * ② `markEngineWorkflowNotified(runId)`;③ `noteWorkflowCompletionCardEnqueued(runId)`
263
+ * (adapt.ts `case 'workflow_complete'` 三行连坐)。⇒ 宿主这三件**一件都不要再做**:
264
+ * · 绝不要借任何「自带同一道门」的宿主老函数投递(cli 的 `enqueueEngineWorkflowNotification()`
265
+ * 首行就是 `if (notifiedRunIds.has(runId)) return`,读的正是库刚写的记账 ⇒ **恒早退,
266
+ * 一条都进不了队列**,两半各自都对、组合零结果);投递要用**裸入队**口。
267
+ * · 也不要再补 `noteWorkflowCompletionCardEnqueued` —— 库已落(幂等 Set.add,宿主照落无害,
268
+ * 但记号该删:整臂让位,不是两边都做)。
269
+ * 🔴 **这条建议在 B6 之前是不安全的,别照抄旧结论**:库里那一行原本写在 `yield` **之后**,
270
+ * 消费者早退时永不执行 ⇒ 那时壳 `upstreamBridge.ts:488` 的显式落记号是**真的在补一个洞**。
271
+ * B6 已把它移到 `yield` 之前(adapt.ts `case 'workflow_complete'`,pure 门「早退 ⇒
272
+ * cardEnqueued 仍落」先红后绿钉住)⇒ 库侧现在**无条件**落,壳那一行才成为可删的冗余。
273
+ * ⇒ 判据只能锚在「队列里到底有没有多出这一条」,不能锚在「谁调了什么」。
241
274
  */
242
275
  | {
243
276
  kind: 'workflow_notification_enqueue';
@@ -261,6 +294,20 @@ export type ChromeEvent = {
261
294
  toolUseId: string;
262
295
  input: unknown;
263
296
  }
297
+ /**
298
+ * B5 —— 同一台账的**第二个来源**:core 1.217([379])的 `task` / `task-list` structured 权威全量态。
299
+ * `source:'tool_use'` 那条是旧引擎兜底(从 input 重建),本条是引擎真实 id 的精确同步。
300
+ * 宿主消费义务:壳 = `handleEngineTaskStructured(structured)`(两路经 engineToShellTaskId 收敛)。
301
+ * 🔴 两条**不是**二选一 —— 旧引擎只有前者、新引擎两者都来且后者更准,宿主按幂等处理即可。
302
+ */
303
+ | {
304
+ kind: 'task_ledger_sync';
305
+ laneProof: LaneProof;
306
+ source: 'structured';
307
+ /** `'task' | 'task-list'`(白名单已在库内判过)。 */
308
+ structuredType: string;
309
+ structured: unknown;
310
+ }
264
311
  /**
265
312
  * B3 新臂 ①(T38 三清的第三清 + T57 五处断闸的第五处)—— E12「下一步可问什么」建议**批**。
266
313
  * `suggestions: null` = 作废(turn 开场必发一次:上一轮的建议对新上下文是噪声,挂着不掉会让人
package/dist/seam.js CHANGED
@@ -13,6 +13,7 @@ export const CHROME_ARMS = [
13
13
  { kind: 'retry_status', required: false, duty: '渲/清 spinner 的重试覆盖层' },
14
14
  { kind: 'panel_task', required: true, duty: 'publishEngineAgentPanelEvent(event) 原样转发' },
15
15
  { kind: 'bgshell_settle', required: true, duty: '后台 shell 面板行落终态(含 outputPath)' },
16
+ { kind: 'bgshell_register', required: true, duty: '后台 shell 面板行建行 + 登记「句柄→宿主 run」' },
16
17
  { kind: 'tasks_expand', required: false, duty: '展开 ctrl+t 任务面板' },
17
18
  { kind: 'thinking_activity', required: false, duty: '开/收活体 "∴ Thinking…" 行' },
18
19
  { kind: 'request_start', required: false, duty: 'spinner 置 requesting 态' },
@@ -21,8 +22,20 @@ export const CHROME_ARMS = [
21
22
  { kind: 'attachment', required: false, duty: '渲 attachment 行(必须带 default 臂)' },
22
23
  { kind: 'notification_terminal', required: false, duty: '可选 UI 提示(记账已由库自持)' },
23
24
  { kind: 'subagent_lifecycle', required: true, duty: 'fire SubagentStart/Stop hooks(去重已由库解)' },
24
- { kind: 'workflow_notification_enqueue', required: true, duty: 'message 入完成通知队列,idle 时喂模型' },
25
- { kind: 'task_ledger_sync', required: true, duty: '按 toolUseId 幂等落 TaskCreate/TaskUpdate' },
25
+ {
26
+ kind: 'workflow_notification_enqueue',
27
+ required: true,
28
+ // 门/notified 记账/cardEnqueued 记号三件已由库在发臂前做完 ⇒ 宿主只投递,且必须用**裸入队**口。
29
+ duty: 'message 裸入完成通知队列(不要用自带去重门的老函数),idle 时喂模型;门与两条记号归库',
30
+ },
31
+ {
32
+ kind: 'task_ledger_sync',
33
+ required: true,
34
+ // 🔴 **两个 source 都要接**(B6 核实:同一张任务卡实测出两条)——`tool_use` 是卡刚开时的
35
+ // 乐观建行(旧引擎只有它),`structured` 是引擎回来的权威全量态(带真实 id/状态)。
36
+ // 只接前者 ⇒ 引擎真实 id 永不回填;只接后者 ⇒ 面板要等工具跑完才出现。幂等键 = toolUseId。
37
+ duty: '按 toolUseId 幂等落 TaskCreate/TaskUpdate —— source 的 tool_use(乐观建行)与 structured(权威全量态)两条都要接',
38
+ },
26
39
  { kind: 'inline_task_stats', required: true, duty: 'tick/settle/settle_all 转发给 engineInlineTaskStats' },
27
40
  { kind: 'prompt_suggestions', required: false, duty: '推 composer 上方 chips(绝不回喂模型)' },
28
41
  { kind: 'last_turn_usage', required: true, duty: '落最近一次 turn 真 usage(statusline 回落源)' },
@@ -0,0 +1,12 @@
1
+ /** Test/introspection hook. */
2
+ export declare function isEngineCompactPending(): boolean;
3
+ /** Test hook: reset the cached capability (fresh engine / respawn). */
4
+ export declare function resetManualCompactCapability(): void;
5
+ /**
6
+ * The /compact command's live half. Mid-turn: fire now. Idle: arm for the next run bind.
7
+ * Returns what it did (for the command's display line / tests). No-op ('offline') without a live
8
+ * wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
9
+ * the client-side projection compaction still runs, but the ENGINE session context does not shrink.
10
+ */
11
+ export declare function requestEngineCompact(): 'fired' | 'armed' | 'offline' | 'unsupported';
12
+ export declare function onEngineTaskBound(taskId: string): void;