@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
@@ -0,0 +1,258 @@
1
+ import { collapseLabel, taskDescFromName } from '../fleetTaskDesc.js';
2
+ import { rowIdTail } from '../workflow.js';
3
+ /**
4
+ * SDK `FleetTaskStatus` 的终态词汇 —— 非终态即在飞(未知未来值按在飞算,保守)。
5
+ * cli 侧 `engineBgGate.TERMINAL_STATUSES` 是**刻意的第二份**(零模块图污染),drift 由壳的
6
+ * `terminalStatuses.test.ts` 锁定 —— 本批不动那份。
7
+ */
8
+ export const TERMINAL_FLEET_TASK_STATUSES = new Set([
9
+ 'completed',
10
+ 'failed',
11
+ 'killed',
12
+ ]);
13
+ /**
14
+ * 未来/未知 wire 状态 → 渲染词汇(闭集但读侧开集)。
15
+ *
16
+ * ⚠️ **B6 未删 `parked`,与任务书的 G11 指令相反 —— 理由与直证写在这里**:
17
+ * · 直证(server 1.291.0 dist,`fleet/fleet-bus.d.ts` + `fleet-bus.js` 12 处 publishTask):
18
+ * `FleetTaskStatus` 闭集**不含** `parked`,且每处 publishTask 的 status 都出自
19
+ * `completed|failed|killed|running` 四值三元链 ⇒ 今天 `'parked'` 在 fleet 行 wire 上**不可达**,
20
+ * 这一支确实是死分支。
21
+ * · 但**后果方向不对称**([anchor-on-the-deciding-quantity]「先定这个量门控动作的正负号」):
22
+ * 本支是**保护型**映射(把非活非终态的 parked 映到 'awaiting approval');删掉之后
23
+ * 它落 `default` ⇒ 渲成 `running` = **谎报活跃**。core 1.382 已有该状态([1561],
24
+ * design/153 §7.1),server 只是尚未投影 ⇒ 这不是「猜测层」,是**预挂**。
25
+ * 删的收益 = 一行;删的代价 = server 补上透传那天静默说谎。
26
+ * ⇒ 保留 + 把「候 server 预挂」的注释换成上面这条可复核的直证。定谳权交回 PM。
27
+ */
28
+ export function coerceTaskStatus(s) {
29
+ switch (s) {
30
+ case 'queued':
31
+ case 'running':
32
+ case 'waiting':
33
+ case 'stopping':
34
+ case 'awaiting approval':
35
+ case 'idle':
36
+ case 'completed':
37
+ case 'failed':
38
+ case 'killed':
39
+ return s;
40
+ case 'parked':
41
+ return 'awaiting approval';
42
+ default:
43
+ return 'running'; // unknown future status → render as a generic live row (open-set fallback)
44
+ }
45
+ }
46
+ /** 中性 workflow run 状态(running|completed|failed)→ 渲染词汇。 */
47
+ export function coerceWorkflowStatus(s) {
48
+ switch (s) {
49
+ case 'completed':
50
+ return 'completed';
51
+ case 'failed':
52
+ return 'failed';
53
+ case 'killed':
54
+ return 'killed';
55
+ default:
56
+ return 'running';
57
+ }
58
+ }
59
+ /**
60
+ * 187 给 fleet 行命名用的是 agent 的**身份**,从不是它的 objective:
61
+ * `tjl(e) = e.type==="in_process_teammate" ? e.identity.agentName : e.agentType`
62
+ * (ui-modules/0616_053_uiux_ejl.js:20-21)。SDK `FleetTaskRow` 带 `agentType`/`agentName`
63
+ * (§K-7),所以这里**重建** tjl:`agentName`(in-process teammate 实例名)优先,否则 `agentType`
64
+ * (subagent_type/teammate 类型名)。只有引擎还没发身份时才退到 objective 派生的 `name` ——
65
+ * 折成单行、绝不整段多行 prompt、绝不空(187 的显示名恒是非空短身份)。全空 → 短 id 尾。
66
+ */
67
+ export function deriveAgentLabel(agentType, agentName, name, id) {
68
+ const identity = collapseLabel(agentName ?? '') || collapseLabel(agentType ?? '');
69
+ if (identity)
70
+ return identity;
71
+ const collapsed = collapseLabel(name ?? '');
72
+ if (collapsed)
73
+ return collapsed;
74
+ const tail = rowIdTail(id).slice(-8);
75
+ return tail ? `agent ${tail}` : 'agent';
76
+ }
77
+ /**
78
+ * 引擎把 fleet 行的 `description` 设成 agent 的**最后一个工具名**(service fleet-bus
79
+ * `description = ev.toolName`)。187 的行正文是进度摘要、从不是工具动词,所以控制面/合成动词
80
+ * 泄漏进行(clay 见过的 `report_blocked`)是纯分歧。这里压掉**已知**的控制面动词(保守集合),
81
+ * 真实工作工具(Read/Bash/Edit…)原样放行。
82
+ *
83
+ * 集合 = core `RESERVED_AGENT_NAMES`(subagent.ts)的 CC-parity PascalCase 形 + goal 模式的
84
+ * `DeclareDone`。引擎 ≥ core 1.176 经 `canonicalToolName` 发 PascalCase。
85
+ */
86
+ export const CONTROL_TOOL_VERBS = new Set([
87
+ 'Task',
88
+ 'Fork',
89
+ 'StructuredOutput',
90
+ 'ReportBlocked',
91
+ 'ReadToolResult',
92
+ 'ToolSearch',
93
+ 'DeclareDone',
94
+ ]);
95
+ /** 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则原样。 */
96
+ export function projectDescription(description) {
97
+ const d = description ?? '';
98
+ return CONTROL_TOOL_VERBS.has(d.trim()) ? '' : d;
99
+ }
100
+ // ── wire 值域过滤器(B6:类型面已到货,这里只剩「脏值不当真」)──────────────────────────────────
101
+ /**
102
+ * `toolUses` —— 子代**累计**工具调用数。语义两条(接错会显示一个「看起来很合理」的错数字):
103
+ * ① **累计不是增量** —— bg 子代 sink 的 usage 是累计口径,与 `turn_end` 的每轮增量相反。直取,绝不累加。
104
+ * ② **缺席 ≠ 0** —— 老 server 不发这个键。有键 ⇒ 真值(含真 0);无键 ⇒ undefined(不知道)。
105
+ */
106
+ export function wireToolUses(r) {
107
+ const v = r.toolUses;
108
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
109
+ }
110
+ /** `transcriptId` —— 子代转录锚(core 铸的 childSessionId);委派 prompt 凭它去转录读面自取。 */
111
+ export function wireTranscriptId(r) {
112
+ const v = r.transcriptId;
113
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
114
+ }
115
+ /** `currentTool: {toolName, target?}` —— `currentAction` 的结构化同源体(core 1.429 → server 1.288)。 */
116
+ export function wireCurrentTool(r) {
117
+ const v = r.currentTool;
118
+ if (typeof v !== 'object' || v === null)
119
+ return undefined;
120
+ const toolName = v.toolName;
121
+ if (typeof toolName !== 'string' || toolName.length === 0)
122
+ return undefined;
123
+ const target = v.target;
124
+ return {
125
+ toolName,
126
+ ...(typeof target === 'string' && target.length > 0 ? { target } : {}),
127
+ };
128
+ }
129
+ /** `startedCount` —— 「已起过的 agent 数」。🔴 缺席 ≠ 0(见 FleetWorkflowView 键注)。 */
130
+ export function wireStartedCount(r) {
131
+ const v = r.startedCount;
132
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
133
+ }
134
+ /** `parentToolCallId`(P1-2)—— 归属直读键。空串按缺席处理(引擎不该发,发了也别当真)。 */
135
+ export function wireParentToolCallId(r) {
136
+ const v = r.parentToolCallId;
137
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
138
+ }
139
+ /** 终态四键之 `usage`(数值 verbatim,只挑数字位;全脏 ⇒ 整键缺席,不造空对象)。 */
140
+ export function wireRowUsage(r) {
141
+ const u = r.usage;
142
+ if (typeof u !== 'object' || u === null)
143
+ return undefined;
144
+ const out = {};
145
+ for (const k of ['totalTokens', 'toolUses', 'durationMs', 'tokens', 'turns', 'costMicroUsd']) {
146
+ const v = u[k];
147
+ if (typeof v === 'number' && Number.isFinite(v))
148
+ out[k] = v;
149
+ }
150
+ return Object.keys(out).length > 0 ? out : undefined;
151
+ }
152
+ /** 终态四键之 `editedFiles`(bg 子代专属;脏项逐条丢,不整键丢)。 */
153
+ export function wireEditedFiles(r) {
154
+ const v = r.editedFiles;
155
+ if (!Array.isArray(v))
156
+ return undefined;
157
+ const out = v
158
+ .filter((f) => typeof f === 'object' &&
159
+ f !== null &&
160
+ typeof f.path === 'string' &&
161
+ f.path.length > 0 &&
162
+ typeof f.edits === 'number' &&
163
+ Number.isFinite(f.edits))
164
+ .map(f => ({ path: f.path, edits: f.edits }));
165
+ return out.length > 0 ? out : undefined;
166
+ }
167
+ /** 终态四键之 `stoppedBy`(开放枚举 verbatim:"user"/"parent"/"system"/…)。 */
168
+ export function wireStoppedBy(r) {
169
+ const v = r.stoppedBy;
170
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
171
+ }
172
+ /** 终态四键之 `resumable`(仅 bg 子代有源;非 boolean ⇒ 缺席,**不当 false**)。 */
173
+ export function wireResumable(r) {
174
+ return typeof r.resumable === 'boolean' ? r.resumable : undefined;
175
+ }
176
+ /**
177
+ * wire task 行(Map values)→ 渲染行。树 depth/parentId 来自 wire:无 parentId = 顶层(depth 0),
178
+ * 子行挂在父下面,depth 沿 parent 链算(自引用/环由 `seen` 断)。
179
+ */
180
+ export function projectTasks(allRows, opts) {
181
+ const rows = opts.rowFilter(allRows);
182
+ const viewingTaskId = opts.viewingTaskId;
183
+ const byId = new Map(rows.map(r => [r.id, r]));
184
+ const depthOf = (r) => {
185
+ let d = 0;
186
+ let cur = r;
187
+ const seen = new Set();
188
+ while (cur?.parentId && byId.has(cur.parentId) && !seen.has(cur.id)) {
189
+ seen.add(cur.id);
190
+ d += 1;
191
+ cur = byId.get(cur.parentId);
192
+ }
193
+ return d;
194
+ };
195
+ // descendantCount:有多少行认这一行当爹(行尾的 " (+N)")。
196
+ const childCount = new Map();
197
+ for (const r of rows) {
198
+ if (r.parentId)
199
+ childCount.set(r.parentId, (childCount.get(r.parentId) ?? 0) + 1);
200
+ }
201
+ return rows.map(r => {
202
+ const toolUses = wireToolUses(r);
203
+ const transcriptId = wireTranscriptId(r);
204
+ const currentTool = wireCurrentTool(r);
205
+ const parentToolCallId = wireParentToolCallId(r);
206
+ const stoppedBy = wireStoppedBy(r);
207
+ const usage = wireRowUsage(r);
208
+ const resumable = wireResumable(r);
209
+ const editedFiles = wireEditedFiles(r);
210
+ const task = {
211
+ id: r.id,
212
+ name: deriveAgentLabel(r.agentType, r.agentName, r.name, r.id),
213
+ // 子行帧无 description → 从 name 复原任务描述(CC 锚:行正文恒显 description,不留空列)。
214
+ description: projectDescription(r.description) || taskDescFromName(r.name, r.agentType, r.agentName),
215
+ depth: depthOf(r),
216
+ ...(r.parentId !== undefined ? { parentId: r.parentId } : {}),
217
+ status: coerceTaskStatus(r.status),
218
+ elapsedMs: r.elapsedMs ?? 0,
219
+ tokens: r.tokens ?? 0,
220
+ // 🔴 wire 从无 tokenDir(#50 门禁盘出的真类型错:读点恒 undefined 曾靠 ?? 掩住)——恒壳侧缺省。
221
+ tokenDir: 'down',
222
+ queuedCount: r.queuedCount ?? 0,
223
+ awaitingPlanApproval: r.awaitingPlanApproval ?? false,
224
+ descendantCount: childCount.get(r.id) ?? 0,
225
+ // 198 isViewed:这一行就是被查看的那个 agent(行 id 尾段 = 引擎 taskId)。
226
+ viewed: viewingTaskId !== undefined && rowIdTail(r.id) === viewingTaskId,
227
+ // 🔴 「键缺席」与「值为 0/''」是两件事 —— 只有真给了才落键(下游据「键在不在」判三态)。
228
+ ...(toolUses !== undefined ? { toolUses } : {}),
229
+ ...(transcriptId !== undefined ? { transcriptId } : {}),
230
+ ...(currentTool !== undefined ? { currentTool } : {}),
231
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
232
+ ...(stoppedBy !== undefined ? { stoppedBy } : {}),
233
+ ...(usage !== undefined ? { usage } : {}),
234
+ ...(resumable !== undefined ? { resumable } : {}),
235
+ ...(editedFiles !== undefined ? { editedFiles } : {}),
236
+ };
237
+ return task;
238
+ });
239
+ }
240
+ /** wire workflow 行 → 渲染行。 */
241
+ export function projectWorkflows(rows) {
242
+ return rows.map(r => {
243
+ const startedCount = wireStartedCount(r);
244
+ return {
245
+ id: r.id,
246
+ // 187 workflow label = `e.summary ?? e.description`(短 label);折成单行,空 → 187 占位符。
247
+ name: collapseLabel(r.name ?? '') || 'Dynamic workflow',
248
+ description: projectDescription(r.description),
249
+ status: coerceWorkflowStatus(r.status),
250
+ doneCount: r.doneCount ?? 0,
251
+ totalCount: r.totalCount ?? 0,
252
+ elapsedMs: r.elapsedMs ?? 0,
253
+ tokens: r.tokens ?? 0,
254
+ failedCount: r.failedCount ?? 0,
255
+ ...(startedCount !== undefined ? { startedCount } : {}),
256
+ };
257
+ });
258
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * src/sema/headlessPermissionModeWire.ts — SEMA_HEADLESS_PERMISSION_MODE 部署旋钮
3
+ * (clay 拍板 2026-07-16:TB/bench 场景 headless `-p` 统一垫 bypassPermissions,对齐 CC bench 实跑的
4
+ * --dangerously-skip-permissions 姿势;落地形态 = 环境变量部署旋钮,产品缺省语义不动)。
5
+ *
6
+ * gap-check 现状(2026-07-16,db3c72f):
7
+ * · `--permission-mode <mode>` CLI flag 存在(main.tsx:1042,choices = PERMISSION_MODES + 'manual'),
8
+ * `--dangerously-skip-permissions` 亦在;但两者在 headless `-p` 车道【都不进 TaskRequest】——
9
+ * seamQueryEngine.ask 自建请求从不 stamp permissionMode(交互 REPL 走 seamQuery →
10
+ * permissionWireCaps.permissionModeField,print/headless 从不 stamp = server absent-mode)。
11
+ * · 即:今天 `-p` 的权限面全靠 server 对 absent-mode 的缺省解释([830]① server 1.191 五模式解释层,
12
+ * absent 不挂门)。core [885] 残余建议:显式垫,防未来缺省变化让 TB 静默变盘。
13
+ *
14
+ * 语义(件2 拍板逐字):
15
+ * 优先级:CLI flag 显式值 > SEMA_HEADLESS_PERMISSION_MODE env > 现状 absent-mode(env 是缺省态的
16
+ * 垫层,flag 恒赢——flag 在场时 env 完全不看,即使 flag 值不在 wire 词表内)。
17
+ * 值域 = 壳既有 wire permissionMode 词表(permissionWireCaps.PermissionMode:default / plan /
18
+ * acceptEdits / bypassPermissions / auto——分类器批3 [907] server 五模式表,server ≥1.209 认
19
+ * "auto")。非法 env 值 ⇒ 一次性 warn + 回落现状,绝不 fail 启动。
20
+ * stamp 规则:
21
+ * · flag 来源:非 default 的 wire 词直接 stamp(auto 自分类器批3 起入表);`default` 不 stamp
22
+ * (= 今天 flag 的实际效果,零行为变化);wire 词表外的 flag 值(dontAsk/manual…是壳内部
23
+ * 模式,server 不认)不 stamp。
24
+ * · env 来源:五词全部显式 stamp(含 `default`——env 是"显式垫层"旋钮,设了就是要显式语义,
25
+ * 不再依赖 server absent-mode 缺省解释;server 1.191 对显式 "default" 走 manual ask 门解释)。
26
+ * 车道纪律:本模块只被 seamQueryEngine.ask(headless `-p`)import;交互 REPL(seamQuery)一根毛
27
+ * 不动——单测有 source-level lane proof(detachWire 同款)。
28
+ */
29
+ import { type PermissionMode, type PermissionModeField } from './permissionWireCaps.js';
30
+ import { type EnvLike } from './hostEnv.js';
31
+ /** 部署旋钮 env 键名(settings.json env 块 → 1a-envseed → process.env 同车道)。 */
32
+ export declare const HEADLESS_PERMISSION_MODE_ENV = "SEMA_HEADLESS_PERMISSION_MODE";
33
+ /** wire 词表(permissionWireCaps.PermissionMode 的运行时镜像;大小写敏感——与 CC flag choices 一致)。
34
+ * 'auto'(分类器批3,[907]):server ≥1.209 五模式表认;老 server coerce 未知词 → "default" 只紧不松。 */
35
+ export declare const HEADLESS_PERMISSION_WIRE_MODES: readonly PermissionMode[];
36
+ export type HeadlessPermissionResolution = {
37
+ /** 直接展开进 TaskRequest 的字段({} = 不 stamp,现状 absent-mode)。 */
38
+ fields: PermissionModeField;
39
+ source: 'flag' | 'env' | 'absent';
40
+ /** 非法 env 值的告警文案(调用层一次性打 stderr)。 */
41
+ warning?: string;
42
+ };
43
+ /**
44
+ * argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
45
+ * `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
46
+ * 返回 undefined = flag 车道未显式表态。
47
+ */
48
+ export declare function permissionModeFromArgv(argv: readonly string[]): string | undefined;
49
+ /**
50
+ * 解析 headless `-p` 的 permissionMode stamp。纯函数除告警 latch(同值只警一次)。
51
+ * 调用方(seamQueryEngine.ask)live-gated:mock/pty 车道永不经过这里。
52
+ */
53
+ export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike): HeadlessPermissionResolution;
54
+ /** 测试钩子:重置一次性告警 latch。 */
55
+ export declare function _resetHeadlessPermissionModeWireForTest(): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * src/sema/headlessPermissionModeWire.ts — SEMA_HEADLESS_PERMISSION_MODE 部署旋钮
3
+ * (clay 拍板 2026-07-16:TB/bench 场景 headless `-p` 统一垫 bypassPermissions,对齐 CC bench 实跑的
4
+ * --dangerously-skip-permissions 姿势;落地形态 = 环境变量部署旋钮,产品缺省语义不动)。
5
+ *
6
+ * gap-check 现状(2026-07-16,db3c72f):
7
+ * · `--permission-mode <mode>` CLI flag 存在(main.tsx:1042,choices = PERMISSION_MODES + 'manual'),
8
+ * `--dangerously-skip-permissions` 亦在;但两者在 headless `-p` 车道【都不进 TaskRequest】——
9
+ * seamQueryEngine.ask 自建请求从不 stamp permissionMode(交互 REPL 走 seamQuery →
10
+ * permissionWireCaps.permissionModeField,print/headless 从不 stamp = server absent-mode)。
11
+ * · 即:今天 `-p` 的权限面全靠 server 对 absent-mode 的缺省解释([830]① server 1.191 五模式解释层,
12
+ * absent 不挂门)。core [885] 残余建议:显式垫,防未来缺省变化让 TB 静默变盘。
13
+ *
14
+ * 语义(件2 拍板逐字):
15
+ * 优先级:CLI flag 显式值 > SEMA_HEADLESS_PERMISSION_MODE env > 现状 absent-mode(env 是缺省态的
16
+ * 垫层,flag 恒赢——flag 在场时 env 完全不看,即使 flag 值不在 wire 词表内)。
17
+ * 值域 = 壳既有 wire permissionMode 词表(permissionWireCaps.PermissionMode:default / plan /
18
+ * acceptEdits / bypassPermissions / auto——分类器批3 [907] server 五模式表,server ≥1.209 认
19
+ * "auto")。非法 env 值 ⇒ 一次性 warn + 回落现状,绝不 fail 启动。
20
+ * stamp 规则:
21
+ * · flag 来源:非 default 的 wire 词直接 stamp(auto 自分类器批3 起入表);`default` 不 stamp
22
+ * (= 今天 flag 的实际效果,零行为变化);wire 词表外的 flag 值(dontAsk/manual…是壳内部
23
+ * 模式,server 不认)不 stamp。
24
+ * · env 来源:五词全部显式 stamp(含 `default`——env 是"显式垫层"旋钮,设了就是要显式语义,
25
+ * 不再依赖 server absent-mode 缺省解释;server 1.191 对显式 "default" 走 manual ask 门解释)。
26
+ * 车道纪律:本模块只被 seamQueryEngine.ask(headless `-p`)import;交互 REPL(seamQuery)一根毛
27
+ * 不动——单测有 source-level lane proof(detachWire 同款)。
28
+ */
29
+ import { permissionModeField } from './permissionWireCaps.js';
30
+ import { hostEnv } from './hostEnv.js';
31
+ /** 部署旋钮 env 键名(settings.json env 块 → 1a-envseed → process.env 同车道)。 */
32
+ export const HEADLESS_PERMISSION_MODE_ENV = 'SEMA_HEADLESS_PERMISSION_MODE';
33
+ /** wire 词表(permissionWireCaps.PermissionMode 的运行时镜像;大小写敏感——与 CC flag choices 一致)。
34
+ * 'auto'(分类器批3,[907]):server ≥1.209 五模式表认;老 server coerce 未知词 → "default" 只紧不松。 */
35
+ export const HEADLESS_PERMISSION_WIRE_MODES = [
36
+ 'default',
37
+ 'plan',
38
+ 'acceptEdits',
39
+ 'bypassPermissions',
40
+ 'auto',
41
+ ];
42
+ /**
43
+ * argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
44
+ * `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
45
+ * 返回 undefined = flag 车道未显式表态。
46
+ */
47
+ export function permissionModeFromArgv(argv) {
48
+ let mode;
49
+ let dsp = false;
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === '--permission-mode') {
53
+ const v = argv[i + 1];
54
+ if (typeof v === 'string' && !v.startsWith('-')) {
55
+ mode = v;
56
+ i++;
57
+ }
58
+ }
59
+ else if (a?.startsWith('--permission-mode=')) {
60
+ mode = a.slice('--permission-mode='.length);
61
+ }
62
+ else if (a === '--dangerously-skip-permissions') {
63
+ dsp = true;
64
+ }
65
+ else if (a === '--') {
66
+ break; // POSIX 约定:-- 之后不是 flag
67
+ }
68
+ }
69
+ if (mode !== undefined)
70
+ return mode;
71
+ if (dsp)
72
+ return 'bypassPermissions';
73
+ return undefined;
74
+ }
75
+ // 一次性告警 latch(-p 是 one-shot 进程;测试钩子可重置)
76
+ let warnedInvalidEnv = false;
77
+ /**
78
+ * 解析 headless `-p` 的 permissionMode stamp。纯函数除告警 latch(同值只警一次)。
79
+ * 调用方(seamQueryEngine.ask)live-gated:mock/pty 车道永不经过这里。
80
+ */
81
+ export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
82
+ // 1) CLI flag 恒赢:flag 在场(含 wire 词表外的壳内部模式)⇒ env 完全不看。
83
+ const flagMode = permissionModeFromArgv(argv);
84
+ if (flagMode !== undefined) {
85
+ // permissionModeField 只放行 wire 已知的非 default 词;default/壳内部词 ⇒ {}(现状,零变化)。
86
+ return { fields: permissionModeField(flagMode), source: 'flag' };
87
+ }
88
+ // 2) env 垫层。
89
+ const raw = env[HEADLESS_PERMISSION_MODE_ENV]?.trim();
90
+ if (raw === undefined || raw === '') {
91
+ return { fields: {}, source: 'absent' };
92
+ }
93
+ if (HEADLESS_PERMISSION_WIRE_MODES.includes(raw)) {
94
+ // env 是显式垫层:五词全 stamp(含 default——stampDefault 显式垫,不依赖 absent-mode 缺省解释)。
95
+ return {
96
+ fields: permissionModeField(raw, { stampDefault: true }),
97
+ source: 'env',
98
+ };
99
+ }
100
+ // 3) 非法 env 值:一次性 warn + 回落现状(绝不 fail)。
101
+ const resolution = { fields: {}, source: 'absent' };
102
+ if (!warnedInvalidEnv) {
103
+ warnedInvalidEnv = true;
104
+ resolution.warning = `sema: ${HEADLESS_PERMISSION_MODE_ENV}="${raw}" is not a recognized permission mode (expected one of: ${HEADLESS_PERMISSION_WIRE_MODES.join(', ')}) — ignoring it, this run keeps the default permission behavior`;
105
+ }
106
+ return resolution;
107
+ }
108
+ /** 测试钩子:重置一次性告警 latch。 */
109
+ export function _resetHeadlessPermissionModeWireForTest() {
110
+ warnedInvalidEnv = false;
111
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * src/sema/headlessReconnectWire.ts — 重连腿 M2 · R1 换车道续收(design/HEADLESS-RECONNECT-DESIGN.md
3
+ * §3 R1 + §6 M2,2026-07-16)。M1(detachWire 头 + 信号 cancel 兜底,2545900)已发运;本模块把
4
+ * engine-alive-idle 判死升级为「先重连、耗尽才判死」——但只在 headless `-p` detach 车道。
5
+ *
6
+ * 触发点(design §3,四条同时成立才进重连分支):
7
+ * ① tasks/stream 以 SseIdleError 收场(流静默超 grace)——其它失败形态(engine-down/restarted/
8
+ * 4xx/断连 reset)原样穿透,现行路径一字不动;
9
+ * ② headless `-p` ∧ detach 头已随首次 POST 发出——本 wrapper 只在 liveClient.tasks.stream 的
10
+ * opts.headers 携带 x-detach-on-disconnect: 'true' 时套上(唯一 set 头的调用方 =
11
+ * seamQueryEngine.ask;交互 REPL(seamQuery)与 headless-bg worker 都不传 headers ⇒ 源码级
12
+ * 证明这两条车道一根毛不动,断连=取消语义保持,铁律 5);
13
+ * ③ durableTaskId 已捕获(X-Task-Id 早捕获,liveClient onResponse)——没有 id 无从换道,原样抛;
14
+ * ④ 每次重连前置 probeEngineAlive(批 D /health 探针复用)——引擎死了绝不空等,原样抛走
15
+ * 现行 engine-down 判死。
16
+ *
17
+ * 铁律对表:
18
+ * · signal.aborted 恒先行(铁律 3,seamQuery.ts:1073 契约):重试环每一轮入口先查 aborted,
19
+ * 用户中止绝不进重连分支(连探活都不做);续收中途 aborted → 干净 return(镜像 SDK parseSse
20
+ * 的 abort 语义),上游 print.ts 的 SIGINT 处置(M1 cancel 兜底)全权接管。
21
+ * · 有界(铁律 5/任务 §4):每 run 最多 HEADLESS_RECONNECT_MAX=2 次壳级重连;耗尽 → 重抛
22
+ * 【原始】SseIdleError(对象恒原件,后续 diagnoseSseIdleTear 打 engine-alive-idle 标 →
23
+ * seamQueryEngine 现行诚实判死文案,字节不变)。
24
+ * · 15min STREAM_MAX_DURATION 硬顶(任务 §3):runs.events 车道被 cap 断开时,SDK sse.js
25
+ * parseSse 自带 Last-Event-ID 立即重连(frame.id 锁存 → 重开带 lastEventId;无进展 5 连败
26
+ * 才放弃)——cap 重连发生在【同一次】openEvents 调用内部,计入同一次重连预算(不额外扣),
27
+ * 铁律 4「不动 SDK」同时满足:runs.events 消费面已有,本模块零 SDK 改动。
28
+ * · 与 headless-bg 有界重发腿(bgTearResends)互斥:重发腿在 seamQuery.query(REPL 形 worker),
29
+ * 其触发词表本就只含 restarted/engine-down(seamQuery.ts:1143),engine-alive-idle 从不重发;
30
+ * 本重连腿只在 -p detach 车道。两腿车道 + 词表双重不相交,结构性互斥。
31
+ *
32
+ * ── R1 去重锚选型(任务书要求写明选型与理由)────────────────────────────────────────────────
33
+ * runs.events 车道:server streamRunEvents 把账本 seq 写成 SSE `id:` 行(formatEvent id: ev.seq),
34
+ * SDK sse.js 把 frame.id 回贴到 ev.id ⇒ 每个续收帧带单调递增数字 seq。锚 = lastSeenSeq:
35
+ * a) 跨壳级 attempt 续传:第二次 openEvents 传 Last-Event-ID=lastSeenSeq(server 从其后回放);
36
+ * b) 防双投:丢弃 seq ≤ lastSeenSeq 的重放帧(server 从头回放/416 回卷都兜住)。
37
+ * 首次换道(tasks/stream → runs.events):tasks/stream 帧无 id(SDK tasks.js 不回贴 frame.id,
38
+ * server sync 腿也不写 id: 行)⇒ 无从定位,只能从起点(不带 Last-Event-ID)拉。防双投依据 =
39
+ * server 实证(1.205/1.209 dist 逐行核对):sync /v1/tasks/stream 腿【不】逐事件落账(runs.js 的
40
+ * appendEvent 只在 async runs 腿与 resume 腿;sync 腿只 createRun + setTerminal(finalResult))⇒
41
+ * sync 腿 run 的账本在终局前恒空,从起点拉不存在「重放已消费帧」面;将来 server 若开始 sync 腿
42
+ * 落账,守卫 = ①终帧唯一(本 wrapper 首终帧即 return + adapter runStream 首终帧 return + 批 B
43
+ * 双帧守卫「恰一条 result」)②seq 锚(首帧起锁存,attempt 间绝不回放)③gap-check 记账:换道前
44
+ * 已消费的 delta 与账本聚合 text 事件无逐字节对齐关系,真正的精确锚需要 server 给 tasks/stream
45
+ * 帧发 id(引擎侧诉求,走黑板,非壳侧可解)。
46
+ *
47
+ * ── 账本空(现役 server sync 腿)时的续收降级(R1b)────────────────────────────────────────
48
+ * 同一实证的另一面:detach 后 run 后台跑完,账本无事件行,runs.events 只会心跳保活直到 run row
49
+ * 落终局,然后无终帧地收流(SDK 无进展 5 连败抛错)。此时 run 的终局【已在】run row 里
50
+ * (GET /v1/runs/:id → RunRecord.result = TaskResult,含完整最终文本)——续收环的 catch 里查
51
+ * run row:已终局 → 合成终帧(done{result} / failed)喂回同一消费管线,turn 以 rc=0 正常收尾,
52
+ * 最终答案全量保真(-p 的 result 面 = done.result.result 字符串,非增量拼接);未终局(还在跑/
53
+ * suspended)→ 计预算继续重试,耗尽才判死。中途 delta 的全帧保真(R1a 环)在账本有事件行时
54
+ * (resume 腿 run / 将来 sync 腿落账的 server)自动生效——两形态由同一环处理,帧结构差异
55
+ * (text/reasoning 聚合事件 vs *_delta)由 adapter eventToSdkMessage 既有 case 'text'/'reasoning'
56
+ * 消费面兜住。
57
+ */
58
+ import type { AgentEvent } from '@sema-agent/sdk';
59
+ /** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。 */
60
+ export declare const HEADLESS_RECONNECT_MAX = 2;
61
+ /** GET /v1/runs/:id 的 run row 终局子集(SDK RunRecord;字段防御性读)。 */
62
+ export interface RunRowLike {
63
+ status?: string;
64
+ result?: unknown;
65
+ errorCode?: string;
66
+ error?: string;
67
+ }
68
+ export interface HeadlessReconnectDeps {
69
+ /** 引擎 baseUrl(探活用;凭证不经本模块——openEvents/getRun 闭包自带)。 */
70
+ baseUrl: string;
71
+ /** X-Task-Id 早捕获读数(liveClient onResponse 闭包);null = 未捕获 ⇒ 不换道。 */
72
+ getTaskId: () => string | null;
73
+ /** runs.events 换道开流(liveClient 闭包 → client.runs.events;lastEventId = 去重锚续传)。 */
74
+ openEvents: (taskId: string, lastEventId?: string) => AsyncGenerator<AgentEvent>;
75
+ /** run row 终局读数(R1b 降级捞终局;失败返回 null,永不抛)。 */
76
+ getRun: (taskId: string) => Promise<RunRowLike | null>;
77
+ signal?: AbortSignal;
78
+ /** 测试注入位;缺省 = 批 D probeEngineAlive(/health,2s 预算)。 */
79
+ probeAlive?: (baseUrl: string) => Promise<boolean>;
80
+ }
81
+ /** run row → 合成终帧(R1b)。带 result 对象的引擎终态(全词表)合成 done{result}——与
82
+ * tasks/stream 终帧字节同构,adapter terminalToSdkResult 全枚举分流([909]B1)原样生效
83
+ * (timeout→error_max_turns、blocked→error_during_execution、park→[884]A1 臂);裸 failed
84
+ * (无 result)合成 failed 事件。running / 无 result 的 park(可能还会被 approvals 解锁)null
85
+ * —— 计预算继续重试,耗尽才判死。 */
86
+ export declare function terminalEventFromRunRow(row: RunRowLike | null): AgentEvent | null;
87
+ /**
88
+ * withHeadlessR1Reconnect(live, deps) — the M2 R1 lane-switch wrapper.
89
+ *
90
+ * 位置:liveClient.tasks.stream 管线【最内层】(demux/HITL 桥/captureSessionId/normalizeToolNames/
91
+ * diagnoseSseIdleTear 之内)——续收帧与合成终帧流经与 live 帧完全相同的下游包装(工具名规整、
92
+ * sessionId 捕获、HITL 桥、批 B 守卫),零特殊路径。重连成功 → 生成器以终帧正常 return ⇒
93
+ * diagnoseSseIdleTear/tearVerdict 永不介入,turn 正常收尾;耗尽/不可换道 → 重抛原始 SseIdleError,
94
+ * 外层打标 + 现行判死文案,字节不变。
95
+ */
96
+ export declare function withHeadlessR1Reconnect(live: AsyncGenerator<AgentEvent>, deps: HeadlessReconnectDeps): AsyncGenerator<AgentEvent>;