@sema-agent/server 7.49.0 → 7.51.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 (63) hide show
  1. package/USAGE.md +39 -0
  2. package/dist/boot/device-lane.d.ts +79 -0
  3. package/dist/boot/device-lane.js +63 -0
  4. package/dist/boot/engine-lease.d.ts +95 -0
  5. package/dist/boot/engine-lease.js +56 -0
  6. package/dist/boot/execution-env.d.ts +48 -2
  7. package/dist/boot/execution-env.js +57 -5
  8. package/dist/boot/resolve-spec.d.ts +3 -0
  9. package/dist/boot/resolve-spec.js +12 -9
  10. package/dist/boot/session-faces.d.ts +5 -0
  11. package/dist/boot/session-faces.js +4 -1
  12. package/dist/boot/shutdown.d.ts +10 -0
  13. package/dist/boot/shutdown.js +19 -1
  14. package/dist/boot/stores.js +9 -0
  15. package/dist/config-center/types.d.ts +10 -3
  16. package/dist/config-invariants.d.ts +2 -2
  17. package/dist/config-invariants.js +18 -0
  18. package/dist/config-types.d.ts +32 -1
  19. package/dist/config.js +69 -10
  20. package/dist/device-enrollment.d.ts +125 -0
  21. package/dist/device-enrollment.js +156 -0
  22. package/dist/device-store.d.ts +385 -0
  23. package/dist/device-store.js +407 -0
  24. package/dist/device-ws-hub.d.ts +182 -0
  25. package/dist/device-ws-hub.js +1012 -0
  26. package/dist/device-ws-protocol.d.ts +429 -0
  27. package/dist/device-ws-protocol.js +464 -0
  28. package/dist/env-facts.d.ts +4 -1
  29. package/dist/env-facts.js +1 -0
  30. package/dist/execution-lane-caps.d.ts +152 -0
  31. package/dist/execution-lane-caps.js +166 -0
  32. package/dist/fleet/subagent-tail-bus.d.ts +1 -1
  33. package/dist/fleet/subagent-tail-bus.js +10 -0
  34. package/dist/http/routes/approvals-assistant.js +1 -1
  35. package/dist/http/routes/capabilities.js +7 -2
  36. package/dist/http/routes/sessions.js +1 -1
  37. package/dist/http/routes/tasks.js +12 -6
  38. package/dist/http/send.d.ts +14 -0
  39. package/dist/http/send.js +5 -0
  40. package/dist/http/server.d.ts +19 -0
  41. package/dist/http/server.js +21 -2
  42. package/dist/http/wire-types.d.ts +1 -1
  43. package/dist/leader/wire.js +4 -2
  44. package/dist/main.js +10 -3
  45. package/dist/orchestration/hardened-vm-runner.d.ts +7 -0
  46. package/dist/orchestration/hardened-vm-runner.js +11 -1
  47. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  48. package/dist/plugins/device-store-sql.d.ts +130 -0
  49. package/dist/plugins/device-store-sql.js +574 -0
  50. package/dist/plugins/remote-env-device.d.ts +271 -0
  51. package/dist/plugins/remote-env-device.js +727 -0
  52. package/dist/plugins/remote-scratchpad.js +1 -1
  53. package/dist/plugins/store-backend.d.ts +9 -0
  54. package/dist/plugins/store-backend.js +3 -0
  55. package/dist/plugins/usage-window-store-sql.d.ts +2 -2
  56. package/dist/plugins/usage-window-store-sql.js +17 -6
  57. package/dist/task-cwd.d.ts +25 -0
  58. package/dist/task-cwd.js +3 -0
  59. package/dist/task-settings.js +3 -1
  60. package/dist/trace/core-keyset-guard.d.ts +3 -3
  61. package/dist/trace/engine-notice-wire.d.ts +1 -1
  62. package/dist/trace/engine-notice-wire.js +1 -0
  63. package/package.json +3 -3
@@ -0,0 +1,166 @@
1
+ const NO = "unsupported";
2
+ const YES = "supported";
3
+ const SU = "single-user-only";
4
+ export function executionLaneCaps(lane) {
5
+ switch (lane) {
6
+ case "in-process":
7
+ return {
8
+ callerCwd: NO,
9
+ backgroundShell: NO,
10
+ scheduler: NO,
11
+ lsp: NO,
12
+ sandboxSendUserFile: NO,
13
+ isolation: NO,
14
+ suspendable: NO,
15
+ remoteScratchpad: NO,
16
+ sandboxPathAdjudication: NO,
17
+ worktreeIsolation: NO,
18
+ memoryPersistenceCapable: NO,
19
+ hostFilePlane: YES,
20
+ coreRemoteExecutionEnv: NO,
21
+ };
22
+ case "host":
23
+ return {
24
+ callerCwd: SU,
25
+ backgroundShell: SU,
26
+ scheduler: SU,
27
+ lsp: SU,
28
+ sandboxSendUserFile: NO,
29
+ isolation: NO,
30
+ suspendable: NO,
31
+ remoteScratchpad: NO,
32
+ sandboxPathAdjudication: NO,
33
+ worktreeIsolation: YES,
34
+ memoryPersistenceCapable: YES,
35
+ hostFilePlane: YES,
36
+ coreRemoteExecutionEnv: YES,
37
+ };
38
+ case "e2b":
39
+ return {
40
+ callerCwd: NO,
41
+ backgroundShell: YES,
42
+ scheduler: NO,
43
+ lsp: YES,
44
+ sandboxSendUserFile: YES,
45
+ isolation: YES,
46
+ suspendable: YES,
47
+ remoteScratchpad: YES,
48
+ sandboxPathAdjudication: YES,
49
+ worktreeIsolation: NO,
50
+ memoryPersistenceCapable: NO,
51
+ hostFilePlane: NO,
52
+ coreRemoteExecutionEnv: YES,
53
+ };
54
+ case "k8s":
55
+ return {
56
+ callerCwd: NO,
57
+ backgroundShell: YES,
58
+ scheduler: NO,
59
+ lsp: YES,
60
+ sandboxSendUserFile: YES,
61
+ isolation: YES,
62
+ suspendable: YES,
63
+ remoteScratchpad: YES,
64
+ sandboxPathAdjudication: YES,
65
+ worktreeIsolation: NO,
66
+ memoryPersistenceCapable: NO,
67
+ hostFilePlane: NO,
68
+ coreRemoteExecutionEnv: YES,
69
+ };
70
+ case "ssh":
71
+ return {
72
+ callerCwd: NO,
73
+ backgroundShell: NO,
74
+ scheduler: NO,
75
+ lsp: NO,
76
+ sandboxSendUserFile: SU,
77
+ isolation: NO,
78
+ suspendable: NO,
79
+ remoteScratchpad: YES,
80
+ sandboxPathAdjudication: YES,
81
+ worktreeIsolation: NO,
82
+ memoryPersistenceCapable: NO,
83
+ hostFilePlane: NO,
84
+ coreRemoteExecutionEnv: YES,
85
+ };
86
+ case "adb":
87
+ return {
88
+ callerCwd: NO,
89
+ backgroundShell: NO,
90
+ scheduler: NO,
91
+ lsp: NO,
92
+ sandboxSendUserFile: NO,
93
+ isolation: NO,
94
+ suspendable: NO,
95
+ remoteScratchpad: NO,
96
+ sandboxPathAdjudication: YES,
97
+ worktreeIsolation: NO,
98
+ memoryPersistenceCapable: NO,
99
+ hostFilePlane: NO,
100
+ coreRemoteExecutionEnv: YES,
101
+ };
102
+ case "local-docker":
103
+ return {
104
+ callerCwd: NO,
105
+ backgroundShell: NO,
106
+ scheduler: NO,
107
+ lsp: NO,
108
+ sandboxSendUserFile: NO,
109
+ isolation: YES,
110
+ suspendable: NO,
111
+ remoteScratchpad: YES,
112
+ sandboxPathAdjudication: YES,
113
+ worktreeIsolation: NO,
114
+ memoryPersistenceCapable: NO,
115
+ hostFilePlane: NO,
116
+ coreRemoteExecutionEnv: YES,
117
+ };
118
+ case "device":
119
+ return {
120
+ callerCwd: YES,
121
+ backgroundShell: NO,
122
+ scheduler: NO,
123
+ lsp: NO,
124
+ sandboxSendUserFile: NO,
125
+ isolation: NO,
126
+ suspendable: NO,
127
+ remoteScratchpad: YES,
128
+ sandboxPathAdjudication: YES,
129
+ worktreeIsolation: NO,
130
+ memoryPersistenceCapable: NO,
131
+ hostFilePlane: NO,
132
+ coreRemoteExecutionEnv: YES,
133
+ };
134
+ }
135
+ }
136
+ const EXECUTION_LANE_WORDS = {
137
+ "in-process": true,
138
+ host: true,
139
+ e2b: true,
140
+ k8s: true,
141
+ ssh: true,
142
+ adb: true,
143
+ "local-docker": true,
144
+ device: true,
145
+ };
146
+ export function isExecutionLane(word) {
147
+ return Object.hasOwn(EXECUTION_LANE_WORDS, word);
148
+ }
149
+ export const ALL_EXECUTION_LANES = Object.freeze(Object.keys(EXECUTION_LANE_WORDS).filter(isExecutionLane));
150
+ export function executionLaneOf(provider) {
151
+ if (provider === undefined)
152
+ return "in-process";
153
+ return isExecutionLane(provider) ? provider : undefined;
154
+ }
155
+ export function laneCapabilityHolds(cap, deployment) {
156
+ switch (cap) {
157
+ case "supported":
158
+ return true;
159
+ case "unsupported":
160
+ return false;
161
+ case "single-user-only":
162
+ return deployment.requirePrincipal !== true;
163
+ }
164
+ }
165
+ export const PLANNED_LANE_DIVERGENCES = Object.freeze([]);
166
+ //# sourceMappingURL=execution-lane-caps.js.map
@@ -7,7 +7,7 @@ export type SubagentTailFrame = Record<string, unknown> & {
7
7
  type: string;
8
8
  };
9
9
  /** core forward 事件 → wire 投影(sync 主流五分支同源;不认识的帧类型=null 不发——tail 面只承诺
10
- * content 五类,新 core 帧类型先经这里显式收编再出 wire)。 */
10
+ * content 六类,新 core 帧类型先经这里显式收编再出 wire)。 */
11
11
  export declare function projectTailFrame(e: Record<string, unknown> & {
12
12
  type?: string;
13
13
  }): SubagentTailFrame | null;
@@ -18,6 +18,16 @@ export function projectTailFrame(e) {
18
18
  const td = e;
19
19
  return { type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) };
20
20
  }
21
+ if (t === "text_end") {
22
+ if (typeof e.content !== "string" || e.content.length === 0)
23
+ return null;
24
+ return {
25
+ type: "text_end",
26
+ content: e.content,
27
+ ...(typeof e.eventId === "string" ? { eventId: e.eventId } : {}),
28
+ ...(typeof e.parentToolCallId === "string" ? { parentToolCallId: e.parentToolCallId } : {}),
29
+ };
30
+ }
21
31
  if (t === "reasoning_delta") {
22
32
  const rd = e;
23
33
  return { type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) };
@@ -60,7 +60,7 @@ async function decideIdempotentReplay(cs, args) {
60
60
  return undefined;
61
61
  }
62
62
  }
63
- if (row.boundInputHash !== (binding.boundInputHash ?? null))
63
+ if (binding.boundInputHash !== undefined && row.boundInputHash !== binding.boundInputHash)
64
64
  return undefined;
65
65
  if (row.decision !== decision)
66
66
  return undefined;
@@ -2,7 +2,8 @@ import { DEFAULT_EFFORT_LEVELS, expandTiers, routePairingStatus } from "@sema-ag
2
2
  import { isModelAllowlisted } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { isSealedKeyPoison } from "../../sealed-key.js";
5
- import { cwdHonored } from "../../task-cwd.js";
5
+ import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
6
+ import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
6
7
  import { a2aInjectionHonored } from "../../task-a2a.js";
7
8
  import { mcpInjectionHonored } from "../../task-mcp.js";
8
9
  import { sendJson, sendError } from "../send.js";
@@ -47,6 +48,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
47
48
  artifacts: Boolean(deps.runStore),
48
49
  approvals: Boolean(deps.checkpointStore),
49
50
  sessions: Boolean(deps.sessionAudit),
51
+ deviceExecutor: deps.deviceHub
52
+ ? { enabled: true, protocolVersion: DEVICE_PROTOCOL_VERSION, maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4, wsPath: DEVICE_WS_PATH }
53
+ : false,
50
54
  sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
51
55
  fleet: deps.fleetBus
52
56
  ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory", hookFailureNotice: true }
@@ -86,7 +90,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
86
90
  scratchpadDir: true,
87
91
  subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
88
92
  subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
89
- taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
93
+ taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
90
94
  modeShellGateTranslation: true,
91
95
  permissionModeAuto: {
92
96
  accepted: true,
@@ -103,6 +107,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
103
107
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
104
108
  interactiveTools: true,
105
109
  projectContext: cwdHonored(deps.config),
110
+ callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
106
111
  mcpInjection: mcpInjectionHonored(deps.config),
107
112
  a2a: true,
108
113
  a2aInjection: a2aInjectionHonored(deps.config),
@@ -366,7 +366,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
366
366
  sendError(res, 503, "state.sse_probe_cap", "session-events probe cap reached — fall back to /head polling");
367
367
  return;
368
368
  }
369
- res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" });
369
+ sseHeaders(res, { "cache-control": "no-cache, no-transform", "x-accel-buffering": "no" });
370
370
  writeRaw(": connected\n\n");
371
371
  started = true;
372
372
  if (preVal !== undefined)
@@ -331,6 +331,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
331
331
  const td = e;
332
332
  sseData(res, { type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) });
333
333
  }
334
+ else if (t === "text_end") {
335
+ const tend = e;
336
+ sseData(res, { type: "text_end", content: tend.content, ...(tend.eventId ? { eventId: tend.eventId } : {}), ...(tend.parentToolCallId ? { parentToolCallId: tend.parentToolCallId } : {}) });
337
+ }
334
338
  else if (t === "reasoning_delta") {
335
339
  const rd = e;
336
340
  sseData(res, { type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) });
@@ -459,7 +463,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
459
463
  else if (ev.type === "human_input") {
460
464
  sseData(res, { type: "human_input", ...humanInputEventData(ev) });
461
465
  }
462
- else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
466
+ else if (ev.type === "text_delta" || ev.type === "text_end" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
463
467
  const e = ev;
464
468
  const ident = {
465
469
  ...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
@@ -469,11 +473,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
469
473
  };
470
474
  const arm = ev.type === "text_delta"
471
475
  ? { type: "text_delta", delta: e.delta, ...ident }
472
- : ev.type === "turn_end"
473
- ? { type: "turn_end", ...(e.usage !== undefined ? { usage: e.usage } : {}), ...(e.usageMissing !== undefined ? { usageMissing: e.usageMissing } : {}), ...(e.stopReason !== undefined ? { stopReason: e.stopReason } : {}), ...ident }
474
- : ev.type === "message_committed"
475
- ? { type: "message_committed", entryId: e.entryId, role: e.role, ...(e.toolCallId !== undefined ? { toolCallId: e.toolCallId } : {}), ...ident }
476
- : { type: "context_usage", ...contextUsageEventData(ev), ...ident };
476
+ : ev.type === "text_end"
477
+ ? { type: "text_end", content: e.content, ...ident }
478
+ : ev.type === "turn_end"
479
+ ? { type: "turn_end", ...(e.usage !== undefined ? { usage: e.usage } : {}), ...(e.usageMissing !== undefined ? { usageMissing: e.usageMissing } : {}), ...(e.stopReason !== undefined ? { stopReason: e.stopReason } : {}), ...ident }
480
+ : ev.type === "message_committed"
481
+ ? { type: "message_committed", entryId: e.entryId, role: e.role, ...(e.toolCallId !== undefined ? { toolCallId: e.toolCallId } : {}), ...ident }
482
+ : { type: "context_usage", ...contextUsageEventData(ev), ...ident };
477
483
  sseData(res, arm);
478
484
  }
479
485
  else {
@@ -8,6 +8,20 @@
8
8
  * now imports them from here, so all ~500 existing `sendJson(...)` call sites are unchanged text.
9
9
  */
10
10
  import type { ServerResponse } from "node:http";
11
+ /**
12
+ * #118(黑板 [5371]②)—— 一条响应「是 SSE 流」的**在场标记**。
13
+ *
14
+ * 引擎附着租约把**打开着的 SSE 流**当作壳的附着租约来数,于是 `handle()` 需要判「这条响应是不是流」。
15
+ * 🔴 **不能靠读回 content-type**(本机 node v24.2.0 实测):`res.writeHead(status, headersObject)` 直接
16
+ * 把头写进网络缓冲,`res.getHeader("content-type")` / `res.hasHeader(...)` 在那之后**仍然**回
17
+ * `undefined` / `false`。照那条路写会得到一个恒为 0 的附着计数 —— 而恒 0 的方向恰好是「以为没人附着」
18
+ * ⇒ 在一只有人用的引擎底下触发自退。所以判据必须是我们自己在开流那一刻盖的这个章。
19
+ *
20
+ * symbol 键:不上 wire、不进 `JSON.stringify`、不与任何应用属性撞名。
21
+ */
22
+ export declare const SSE_RESPONSE_MARK: unique symbol;
23
+ /** 这条响应是否已开成 SSE 流(= 走过 {@link sseHeaders})。 */
24
+ export declare function isSseResponse(res: ServerResponse): boolean;
11
25
  export declare function sseHeaders(res: ServerResponse, extra?: Record<string, string>): void;
12
26
  /**
13
27
  * 一条 SSE 连接的墙钟上限。到点发一帧 `error`/`STREAM_MAX_DURATION`(**不是**静默关闭 —— 客户端分不出
package/dist/http/send.js CHANGED
@@ -1,4 +1,9 @@
1
+ export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
2
+ export function isSseResponse(res) {
3
+ return res[SSE_RESPONSE_MARK] === true;
4
+ }
1
5
  export function sseHeaders(res, extra) {
6
+ res[SSE_RESPONSE_MARK] = true;
2
7
  res.writeHead(200, {
3
8
  "content-type": "text/event-stream",
4
9
  "cache-control": "no-cache",
@@ -242,6 +242,20 @@ export interface ServiceCoordinatorDeps {
242
242
  * 重组零件——见 backend 的同款论证)。parked-revive 三件是同族的构件缝(boot 期从生产装配取的裸
243
243
  * ToolSpec + 成员集 + 父约束重建器),与 backgroundAgentStore 齐备才构成赎回腿。 */
244
244
  export interface ServiceSeamDeps {
245
+ /**
246
+ * device lane 的 WS 汇聚端(design/device-executor-lane-v2 §5,车A-3)—— 本仓**首个** WS 服务端。
247
+ * 给了就把 `GET /v1/device/ws` 的 upgrade 处理器挂到这台 `http.Server` 上;缺席 = 本部署没有 device
248
+ * 车道 ⇒ 该路径连 upgrade 都不认(与 `REMOTE_EXEC=device` 的拒启门同一条诚实线)。
249
+ *
250
+ * ⚠️ 排空联动由**装配层**驱动:`drainState.draining` 翻真时,main.ts 须同时调 `deviceHub.beginDrain()`
251
+ * ——两者刻意不在这里自动耦合,因为 §8-R7 的顺序(新提交 503 → 新 upgrade 拒 → 停发新指令 →
252
+ * 在途结果照收 → 才断连)是**部署编排**的语义,不是 HTTP 层能自作主张的。
253
+ *
254
+ * 🔴 **当前生产装配恒缺席,这是有意的**(`parkedKnownAgentTypes` 同族形):车A-3 只交汇聚端与这条
255
+ * 挂载缝;`main.ts` 侧的真装配(建店 → 建 hub → 传这个键 → 与 `drainState` 联动 → 与
256
+ * `REMOTE_EXEC=device` 拒启门对齐)属**车A-4/端点车**。在那之前本键只有测试口消费,不是死枝。
257
+ */
258
+ deviceHub?: import("../device-ws-hub.js").DeviceWsHub;
245
259
  /** Audit回溯: current context (+ summary) for a session, plus its owner. Enables GET /v1/sessions/:id. */
246
260
  sessionAudit?: (sessionId: string) => Promise<({
247
261
  owner: string | null;
@@ -513,6 +527,9 @@ export interface ServiceDeploymentDeps {
513
527
  * • `inflight` is ASSIGNED BY createServer (a live union of this replica's in-flight legs: durable bg/resume
514
528
  * `inflightRuns` + live sync/resume streams `steerableRuns`) — main.ts polls it to know when drain is done.
515
529
  * • /health carries `draining:true` (k8s readiness 摘流信号) + `version` (build self-description).
530
+ * • #118:`attachedStreams` 同样 ASSIGNED BY createServer —— 本副本当前打开着的 SSE 流条数
531
+ * (= 引擎附着租约集的大小)。消费方是 `boot/engine-lease.ts` 的自退判据;与 `inflight` 同族,
532
+ * 同一个共享盒子,不另开第二条 main↔server 的通道。
516
533
  */
517
534
  drainState?: {
518
535
  draining: boolean;
@@ -520,6 +537,8 @@ export interface ServiceDeploymentDeps {
520
537
  reason?: string;
521
538
  inflight?: () => number;
522
539
  lastActivityAt?: () => number;
540
+ attachedStreams?: () => number;
541
+ lastAttachEndedAt?: () => number;
523
542
  };
524
543
  /** boot ready 门(b):false = registry 部署无显式 env 模型且首次 effective pull 尚未落 roster
525
544
  * (worker 只有占位模型)。计费提交 503 + /health 加性 `ready:false`。absent = 恒 ready(env 模型在/非
@@ -70,7 +70,7 @@ import { cascadeConfig } from "./run-meta.js";
70
70
  export { cascadeConfig };
71
71
  import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit } from "./routes/images.js";
72
72
  export { coarseStatusForState, errorCodeForExit };
73
- import { sendJson, sendError, httpErrorCode, msg } from "./send.js";
73
+ import { sendJson, sendError, httpErrorCode, msg, isSseResponse } from "./send.js";
74
74
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
75
75
  import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
76
76
  export { explicitOperatorOk, isOperator };
@@ -179,6 +179,18 @@ export function createHttpServer(rawDeps) {
179
179
  };
180
180
  if (deps.drainState)
181
181
  deps.drainState.lastActivityAt = () => lastBillableActivityAt;
182
+ const openResponses = new Set();
183
+ let lastAttachEndedAt = 0;
184
+ if (deps.drainState) {
185
+ deps.drainState.attachedStreams = () => {
186
+ let n = 0;
187
+ for (const r of openResponses)
188
+ if (isSseResponse(r))
189
+ n++;
190
+ return n;
191
+ };
192
+ deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
193
+ }
182
194
  if (deps.hookWakeBus) {
183
195
  deps.hookWakeBus.deliver = async (sessionId, text) => {
184
196
  try {
@@ -232,6 +244,7 @@ export function createHttpServer(rawDeps) {
232
244
  res.end();
233
245
  });
234
246
  });
247
+ deps.deviceHub?.attach(server);
235
248
  const routeCtxBase = {
236
249
  deps,
237
250
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
@@ -247,6 +260,8 @@ export function createHttpServer(rawDeps) {
247
260
  const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
248
261
  if (admitted)
249
262
  counters.admittedInflight++;
263
+ if (admitted)
264
+ openResponses.add(res);
250
265
  let logged = false;
251
266
  const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
252
267
  const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
@@ -256,6 +271,8 @@ export function createHttpServer(rawDeps) {
256
271
  logged = true;
257
272
  if (admitted)
258
273
  counters.admittedInflight--;
274
+ if (openResponses.delete(res) && isSseResponse(res))
275
+ lastAttachEndedAt = performance.now();
259
276
  const route = routeLabel(method, url);
260
277
  const seconds = (Date.now() - startedAt) / 1000;
261
278
  const aborted = viaClose && !res.writableEnded;
@@ -1588,6 +1605,8 @@ export function createHttpServer(rawDeps) {
1588
1605
  e.code === "resume.constraint_unprojectable" ||
1589
1606
  e.code === "resume.usage_window_exhausted" ||
1590
1607
  e.code === "resume.principal_mismatch" ||
1608
+ e.code === "resume.placement_mismatch" ||
1609
+ e.code === "resume.preflight_rejected" ||
1591
1610
  e.code.startsWith("wake.") ||
1592
1611
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
1593
1612
  if (claimedRow && taskId && deps.runStore) {
@@ -1617,7 +1636,7 @@ export function createHttpServer(rawDeps) {
1617
1636
  ? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
1618
1637
  : e.code,
1619
1638
  ...(e.detail?.field ? { field: e.detail.field } : {}),
1620
- ...(e.code === "resume.usage_window_exhausted" && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
1639
+ ...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
1621
1640
  ? { retryAfterSec: Math.max(1, Math.ceil(e.detail.retryAfterMs / 1000)) }
1622
1641
  : {}),
1623
1642
  ...(retriable ? { retriable: true } : {}),
@@ -216,7 +216,7 @@ export interface TaskRequestBody {
216
216
  skillsListing?: boolean;
217
217
  };
218
218
  /** C1 (core 1.219, subagent viewing pane): opt-in — forward a delegated child's live CONTENT events
219
- * (text_delta/reasoning_delta/tool_start/tool_end, each stamped `parentToolCallId`) onto this run's stream/durable
219
+ * (text_delta/text_end/reasoning_delta/tool_start/tool_end, each stamped `parentToolCallId`) onto this run's stream/durable
220
220
  * log, so a shell can render the child's transcript live ("enter 看详情"). Default OFF = progress-only (prior
221
221
  * behavior). Purely a render channel — core never merges the child stream into the parent's model context. */
222
222
  forwardSubagentEvents?: boolean;
@@ -366,7 +366,8 @@ export function createLeaderRunner(cfg) {
366
366
  };
367
367
  const provisionIntegrationSandbox = async () => {
368
368
  if (cfg.envFactory) {
369
- const env = cfg.envFactory({ sessionId: `leader-integ-${Date.now()}` });
369
+ const integSessionId = `leader-integ-${Date.now()}`;
370
+ const env = cfg.envFactory({ sessionId: integSessionId, placementRootSessionId: integSessionId });
370
371
  try {
371
372
  await sh(env)(body.seedCmd);
372
373
  await injectOracles(env);
@@ -412,7 +413,8 @@ export function createLeaderRunner(cfg) {
412
413
  const resolveRepair = async (solo, _sub) => {
413
414
  let env;
414
415
  if (cfg.envFactory) {
415
- env = cfg.envFactory({ sessionId: `leader-grader-${Date.now()}` });
416
+ const graderSessionId = `leader-grader-${Date.now()}`;
417
+ env = cfg.envFactory({ sessionId: graderSessionId, placementRootSessionId: graderSessionId });
416
418
  }
417
419
  else {
418
420
  if (!cfg.e2bApiKey)
package/dist/main.js CHANGED
@@ -50,6 +50,7 @@ import { assertRetentionLaneWirable, startRetentionLane, RETENTION_LEASE_TTL_FAC
50
50
  import { assertConsolidationDriverWirable, buildConsolidationValveAudit, createMemoryConsolidationFaces, resolveConsolidationDriverSeat } from "./boot/memory-consolidation.js";
51
51
  import { startFleetReconciler } from "./fleet/fleet-reconciler.js";
52
52
  import { openStores } from "./boot/stores.js";
53
+ import { assertDeviceLaneWired, createDeviceLane } from "./boot/device-lane.js";
53
54
  import { runAdoptionBootScan } from "./boot/adoption.js";
54
55
  import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
55
56
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
@@ -117,6 +118,7 @@ async function main() {
117
118
  const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
118
119
  await configCenter.applyLocalRemoteExec();
119
120
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
121
+ const instanceId = uuidv7();
120
122
  const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine) : undefined;
121
123
  const memoryComplianceFaces = memoryEngine
122
124
  ? createMemoryComplianceFaces(memoryEngine, {
@@ -144,7 +146,9 @@ async function main() {
144
146
  });
145
147
  }
146
148
  await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
147
- const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
149
+ const deviceLane = config.remoteExec?.provider === "device" ? createDeviceLane({ config, logger, metrics, backend, replicaId: instanceId, rateGate: () => rateLimiter }) : undefined;
150
+ const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, dropSessionScoped, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore, ...(deviceLane ? { deviceLane } : {}) });
151
+ assertDeviceLaneWired(config.remoteExec?.provider === "device", deviceLane, executionEnvFactory !== undefined);
148
152
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
149
153
  if (toolTracer)
150
154
  logger.info("tool_trace_enabled", {});
@@ -328,7 +332,6 @@ async function main() {
328
332
  const runStore = backend ? backend.run() : undefined;
329
333
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
330
334
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
331
- const instanceId = uuidv7();
332
335
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
333
336
  const capEnv = (name) => {
334
337
  const r = parseCapEnv(name, process.env[name]);
@@ -515,6 +518,8 @@ async function main() {
515
518
  configCenter.startRefreshLoop({ runnerTierFrozen, pricing, limitSync, swapRunnerModels });
516
519
  configCenter.initKeyResolver();
517
520
  const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
521
+ deviceStore: deviceLane?.store,
522
+ dropSessionScoped,
518
523
  config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
519
524
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
520
525
  workflowCompletionInbox, taskListLane,
@@ -585,6 +590,7 @@ async function main() {
585
590
  sessionTitler: sessionTitler ? sessionTitler : undefined,
586
591
  };
587
592
  const seams = {
593
+ deviceHub: deviceLane?.hub,
588
594
  sessionAudit,
589
595
  routeJudgeBrain: brain,
590
596
  purgeSession: purgeSession ? purgeSession : undefined,
@@ -665,7 +671,7 @@ async function main() {
665
671
  config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
666
672
  getCenterPrompts: () => configCenter.getCenterPrompts(),
667
673
  getKeyResolver: () => configCenter.getKeyResolver(),
668
- taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv,
674
+ taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner,
669
675
  hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
670
676
  selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore,
671
677
  singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
@@ -785,6 +791,7 @@ async function main() {
785
791
  version: serviceVersion(),
786
792
  runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
787
793
  storeLiveProbe, configCenter,
794
+ deviceHub: deviceLane?.hub,
788
795
  retentionLane: retentionLaneStarted,
789
796
  releaseRetentionLease: retentionLaneStore !== undefined ? () => retentionLaneStore.release(instanceId) : undefined,
790
797
  });
@@ -62,6 +62,13 @@ export interface HardenedVmLimits {
62
62
  * A script exceeding it OOMs the WORKER (killed), never the shared main process. Default 128. */
63
63
  maxHeapMb?: number;
64
64
  }
65
+ /** [5434]①(clay 亲机四 run 三次恰在 600s 整点全灭):workflow 总超时默认与旋钮的**单点**。
66
+ * 旧默认 600_000(10 分钟)对真实 workflow(验证 agent 逐条重跑清单/任何带审批等待的车)结构性不够——
67
+ * 默认抬到 **1 小时**;部署经 `WORKFLOW_TOTAL_TIMEOUT_MS` 显式配,域 [60_000, 86_400_000](1 分钟..24h),
68
+ * 坏值**响亮拒**(#210 立律:旋钮坏值禁静默回默认)。0/负数不是「禁用」:无界 runaway 脚本会吞掉共享
69
+ * 副本的事件循环/堆,禁用臂刻意不提供——要更久就把值配大。模块加载时读一次(坏 env = boot fail-loud)。
70
+ * 超时终态分型(partial vs failed,[5434]① 后半)涉 wire status 语义,与 core workflow 终态形另批对表。 */
71
+ export declare function workflowTotalTimeoutMsDefault(): number;
65
72
  /**
66
73
  * The realm test itself, exported so there is exactly ONE ruler for "did this rejection come from an untrusted
67
74
  * script?" in the process. A promise minted INSIDE a vm context fails `instanceof` against the HOST `Promise`
@@ -1,7 +1,17 @@
1
1
  import vm from "node:vm";
2
2
  import { splitWorkflowMeta, WorkflowScriptError, } from "@sema-agent/core";
3
+ export function workflowTotalTimeoutMsDefault() {
4
+ const raw = process.env.WORKFLOW_TOTAL_TIMEOUT_MS;
5
+ if (raw === undefined || raw === "")
6
+ return 3_600_000;
7
+ const n = Number(raw);
8
+ if (!Number.isSafeInteger(n) || n < 60_000 || n > 86_400_000) {
9
+ throw new Error(`env WORKFLOW_TOTAL_TIMEOUT_MS must be an integer in [60000, 86400000] ms (got ${JSON.stringify(raw)}) — unset it for the 3600000 default`);
10
+ }
11
+ return n;
12
+ }
3
13
  const DEFAULTS = {
4
- totalTimeoutMs: 600_000,
14
+ totalTimeoutMs: workflowTotalTimeoutMsDefault(),
5
15
  syncTimeoutMs: 5_000,
6
16
  maxAgents: 1000,
7
17
  concurrency: 12,
@@ -1,7 +1,7 @@
1
1
  import { Worker } from "node:worker_threads";
2
2
  import { WorkflowScriptError } from "@sema-agent/core";
3
- import { scopedPhaseGate } from "./hardened-vm-runner.js";
4
- const DEFAULTS = { totalTimeoutMs: 600_000, syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
3
+ import { scopedPhaseGate, workflowTotalTimeoutMsDefault } from "./hardened-vm-runner.js";
4
+ const DEFAULTS = { totalTimeoutMs: workflowTotalTimeoutMsDefault(), syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
5
5
  const IS_TS = import.meta.url.endsWith(".ts");
6
6
  const WORKER_URL = new URL(IS_TS ? "./hardened-vm-worker.ts" : "./hardened-vm-worker.js", import.meta.url);
7
7
  const WORKER_EXEC_ARGV = IS_TS ? ["--import", "tsx/esm"] : undefined;