@sema-agent/server 7.119.0 → 7.121.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 (127) hide show
  1. package/MIGRATION.md +129 -0
  2. package/USAGE.md +72 -23
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +37 -13
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/execution-env.js +3 -2
  10. package/dist/boot/fatal-exit.d.ts +7 -1
  11. package/dist/boot/fatal-exit.js +2 -1
  12. package/dist/boot/resolve-spec.js +1 -0
  13. package/dist/boot/runner-deps.d.ts +4 -3
  14. package/dist/boot/runner-deps.js +7 -3
  15. package/dist/boot/shutdown.d.ts +9 -10
  16. package/dist/boot/shutdown.js +54 -16
  17. package/dist/boot/stage-01-config.js +2 -0
  18. package/dist/boot/stage-09-leader.d.ts +2 -9
  19. package/dist/boot/stage-10-http-server.d.ts +1 -9
  20. package/dist/boot/stage-10-http-server.js +31 -8
  21. package/dist/capabilities/builtin-tools.d.ts +31 -8
  22. package/dist/capabilities/builtin-tools.js +22 -1
  23. package/dist/capabilities/composite-members.d.ts +1 -1
  24. package/dist/capabilities/composite-members.js +1 -1
  25. package/dist/capabilities/scenarios.d.ts +8 -6
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  28. package/dist/capabilities/send-user-file-tool.js +1 -1
  29. package/dist/config-catalog.js +13 -7
  30. package/dist/config-center/center-request.d.ts +16 -1
  31. package/dist/config-center/center-request.js +36 -5
  32. package/dist/config-lkg.d.ts +33 -5
  33. package/dist/config-lkg.js +55 -11
  34. package/dist/config-types.d.ts +46 -3
  35. package/dist/config-types.js +6 -0
  36. package/dist/config.d.ts +33 -4
  37. package/dist/config.js +107 -15
  38. package/dist/deployment-governance.d.ts +3 -1
  39. package/dist/deployment-governance.js +3 -1
  40. package/dist/device-store.d.ts +7 -0
  41. package/dist/device-store.js +4 -0
  42. package/dist/device-ws-hub.js +17 -9
  43. package/dist/device-ws-protocol.d.ts +15 -0
  44. package/dist/fleet/drain-entry.d.ts +87 -0
  45. package/dist/fleet/drain-entry.js +35 -0
  46. package/dist/fleet/engine-signal.d.ts +144 -0
  47. package/dist/fleet/engine-signal.js +60 -0
  48. package/dist/fleet/serving-lease.d.ts +47 -0
  49. package/dist/fleet/serving-lease.js +235 -0
  50. package/dist/fleet-client.d.ts +35 -19
  51. package/dist/fleet-client.js +49 -25
  52. package/dist/http/approval-sweeps.js +3 -5
  53. package/dist/http/dispatch.js +18 -13
  54. package/dist/http/health-self-report.d.ts +60 -0
  55. package/dist/http/health-self-report.js +53 -0
  56. package/dist/http/principal-gate.d.ts +8 -1
  57. package/dist/http/principal-gate.js +1 -0
  58. package/dist/http/readiness.d.ts +25 -5
  59. package/dist/http/readiness.js +31 -6
  60. package/dist/http/resume-legs.js +5 -1
  61. package/dist/http/route-ctx.d.ts +6 -4
  62. package/dist/http/route-table.d.ts +3 -0
  63. package/dist/http/route-table.js +4 -1
  64. package/dist/http/routes/a2a-serve.d.ts +1 -1
  65. package/dist/http/routes/a2a-serve.js +5 -5
  66. package/dist/http/routes/admin-drain.d.ts +12 -31
  67. package/dist/http/routes/admin-drain.js +15 -9
  68. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  69. package/dist/http/routes/approvals-assistant.js +3 -3
  70. package/dist/http/routes/capabilities.js +3 -1
  71. package/dist/http/routes/devices.js +6 -5
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +1 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +14 -6
  79. package/dist/http/routes/runs.d.ts +2 -2
  80. package/dist/http/routes/runs.js +2 -2
  81. package/dist/http/routes/side-query.d.ts +1 -1
  82. package/dist/http/routes/side-query.js +1 -1
  83. package/dist/http/routes/tasks.d.ts +2 -2
  84. package/dist/http/routes/tasks.js +3 -3
  85. package/dist/http/server.d.ts +7 -21
  86. package/dist/http/server.js +20 -1
  87. package/dist/http/wire-types.d.ts +7 -1
  88. package/dist/list-entry-display.d.ts +14 -0
  89. package/dist/list-entry-display.js +14 -0
  90. package/dist/model-reach-scope.d.ts +15 -3
  91. package/dist/model-reach-scope.js +3 -2
  92. package/dist/model-unreachable-notice.d.ts +2 -1
  93. package/dist/model-unreachable-notice.js +1 -0
  94. package/dist/namespaced-rule-name.js +2 -6
  95. package/dist/observability/fail-open.d.ts +20 -0
  96. package/dist/observability/fail-open.js +20 -0
  97. package/dist/operator-ask.d.ts +1 -1
  98. package/dist/plugins/one-time-migrations.d.ts +8 -0
  99. package/dist/plugins/one-time-migrations.js +72 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -6
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/request-key-closure.js +1 -1
  108. package/dist/rule-entry-list.d.ts +0 -1
  109. package/dist/rule-entry-list.js +4 -9
  110. package/dist/rules-consent.d.ts +16 -3
  111. package/dist/rules-consent.js +8 -4
  112. package/dist/run-local.js +2 -0
  113. package/dist/runs.js +1 -1
  114. package/dist/sema-config-dir.d.ts +19 -0
  115. package/dist/sema-config-dir.js +33 -0
  116. package/dist/session-policy-wire.js +5 -4
  117. package/dist/spec-fields.d.ts +5 -4
  118. package/dist/task-settings.d.ts +3 -3
  119. package/dist/tool-approval.d.ts +7 -0
  120. package/dist/tool-approval.js +4 -1
  121. package/dist/trace/core-keyset-guard.d.ts +2 -2
  122. package/dist/trace/engine-notice-wire.d.ts +6 -4
  123. package/dist/trace/engine-notice-wire.js +16 -9
  124. package/dist/trace/self-minted-wire.d.ts +78 -0
  125. package/dist/trace/self-minted-wire.js +32 -0
  126. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  127. package/package.json +3 -3
@@ -0,0 +1,33 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ export function declaredSemaConfigDir(env = process.env) {
5
+ const raw = env.SEMA_CONFIG_DIR;
6
+ if (raw === undefined)
7
+ return undefined;
8
+ if (raw === "") {
9
+ throw new Error('env SEMA_CONFIG_DIR="" is empty — refusing to start rather than moving the configuration files: up to 7.119.x an empty value meant the process ' +
10
+ "working directory (the last-known-good config, the skill cache and the scheduler store were kept there), and reading it as unset would move them " +
11
+ "to ~/.sema without a word. Unset the variable to use ~/.sema, or set it to the directory you mean (to keep the old place, the working directory's path).");
12
+ }
13
+ return resolve(raw);
14
+ }
15
+ export function configRootOf(config) {
16
+ return config.agentConfigDir ?? join(homedir(), ".sema");
17
+ }
18
+ export function provisionSemaConfigDir(config, logger) {
19
+ const dir = config.agentConfigDir;
20
+ if (dir === undefined)
21
+ return;
22
+ try {
23
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ }
25
+ catch (e) {
26
+ logger.warn("sema_config_dir_unprovisioned", {
27
+ dir,
28
+ err: e instanceof Error ? e.message : String(e),
29
+ note: "SEMA_CONFIG_DIR names a directory this server could not create — the engine refuses a configuration root that is not an existing directory when the first Runner is built, so the boot stops there with the engine's refusal: create the directory, point the variable at one, or unset it (the default is ~/.sema)",
30
+ });
31
+ }
32
+ }
33
+ //# sourceMappingURL=sema-config-dir.js.map
@@ -1,4 +1,5 @@
1
1
  import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent } from "@sema-agent/core";
2
+ import { displayListEntry } from "./list-entry-display.js";
2
3
  import { describeEmptyToolSegment, emptyToolSegmentOf } from "./namespaced-rule-name.js";
3
4
  const RULE_FIELDS = {
4
5
  toolAllow: true,
@@ -99,17 +100,17 @@ export function describeSessionPermissionRulesProblem(p) {
99
100
  }
100
101
  }
101
102
  function describeUnreachableSessionRuleName(u) {
102
- return `${u.field} names "${u.name}", which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
+ return `${u.field} names ${displayListEntry(u.name)}, which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
104
  }
104
105
  function describeLegacyToolName(r) {
105
106
  const why = r.guidance !== undefined ? ` (${r.guidance})` : "";
106
107
  switch (r.verdict) {
107
108
  case "retired":
108
- return `${r.field} names "${r.name}", a retired tool name${why}; with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
109
+ return `${r.field} names ${displayListEntry(r.name)}, a retired tool name${why}; with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` (${displayListEntry(r.currentName)})` : ""}`;
109
110
  case "unprefixed_namespace":
110
- return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
111
+ return `${r.field} names ${displayListEntry(r.name)}, which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
111
112
  case "unmintable_segment":
112
- return `${r.field} names "${r.name}", which can never match a tool${why}, so nothing was written`;
113
+ return `${r.field} names ${displayListEntry(r.name)}, which can never match a tool${why}, so nothing was written`;
113
114
  default: {
114
115
  const unhandled = r.verdict;
115
116
  throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
@@ -67,10 +67,11 @@ export declare function skillToolListRefusal(skill: Record<string, unknown>): st
67
67
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
68
68
  * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。`allowTools` / `denyTools` 本门只判形与条数(`toolList`),
69
69
  * **名字归引擎一只判**(core 7.45.0 #1377 / #1385,开跑前,三来源同判):没有工具能是的拼法(带空白 / 括号形坏 / 不可见字符)、非字符串条目 ⇒ 整 run 拒
70
- * `config.agent.unknown_tool`;无人挂的名 ⇒ 不拒、经 `onError`(phase `"config"`)告一次(本仓汇点 `runner_config_advisory`);大小写孪生 / 退役名(`bash`)
71
- * **在本仓部署形上也只告不拒** —— 主车道的委派工具带 extraTools 工厂(`capabilities/builtin-tools.ts`),引擎开跑前列不全子代工具池(`agentToolPool` 缺席)就不把
72
- * 像 `Bash` 的名字判成拼错;引擎给宿主「声明可注入名册」的座后本仓声明名册、这两形回到开跑前拒;
73
- * `denyTools` 的 `Tool(content)` ⇒ 整只工具对该子代藏起并告。本仓不在受理口另做名字对照(那会是同一问的第二个判官)。
70
+ * `config.agent.unknown_tool`;无人挂的名 ⇒ 不拒、经 `onError`(phase `"config"`)告一次(本仓汇点 `runner_config_advisory`);大小写孪生 / 退役名(`bash` / `BASH`)
71
+ * ⇒ 开跑前拒 `config.agent.unknown_tool` —— 主车道的委派工具带 extraTools 工厂,本仓在同一处声明它的名册(core 7.46.0 `extraToolNames`;工厂与名册读同一批工具对象,
72
+ * `capabilities/builtin-tools.ts` `createSubagentExtraTools`),引擎因此列得出子代工具池。两张表管子代手里的**全部**工具:委派池与 hands 内建(`Read` / `Edit` / `Write` /
73
+ * `Bash` …,core #1430)—— `denyTools:["Write"]` ⇒ 子代没有 `Write`;`allowTools` 只点了调用方工具 ⇒ 子代一只 hands 工具都没有。hands 行之外的引擎工具
74
+ * (`Monitor` 等)仍不受这两张表管(引擎已知限制)。`denyTools` 的 `Tool(content)` ⇒ 整只工具对该子代藏起并告。本仓不在受理口另做名字对照(那会是同一问的第二个判官)。
74
75
  * ⚠️ 键判断必须 Object.hasOwn:`in` 走原型链,JSON 里的 "toString"/"constructor"/"__proto__" 键会被
75
76
  * 误判为白名单字段,随后 SHAPES[k].ok 不是函数 ⇒ TypeError(fresh 500 / resume 砖;cross-review 复现)。 */
76
77
  export declare function taskAgentIssue(item: unknown): string | null;
@@ -373,9 +373,9 @@ export interface FsWriteGateWiring {
373
373
  * 存不存在 ⇒ 把哨兵递给它,`deny Edit(src/**)` 会编译成功、指向一个谁也不会写的目录,对真实工作区
374
374
  * 的 `<工作区>/src/secret.ts` 这类目标判 allow —— 一条运维亲手写下的收紧规则**静默失效**,方向与写门那条正相反。
375
375
  *
376
- * ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 规则编译器拿不到 `pathBases.root` ⇒ 需要该基的拼法
377
- * (`Edit(src/**)` / `Edit(/etc/**)`)被 core 以 `unsupported.path_base` **整表拒**(→ 422,fail-loud),
378
- * 而不是编成一条够不着任何东西的规则。绝对形(`//abs`)与 `~/` 形不需要基,两种部署形都照常编得出。
376
+ * ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 不声明根 ⇒ 需要该基的拼法(`Edit(src/**)` / `Edit(/etc/**)`)锚在**装入这只策略的那条腿**
377
+ * 的真根(core 7.46.0 #1383 车 B:锚装一次、在被装入处;`unsupported.path_base` 只剩「声明了一个用不了的基」)—— 哨兵从不被声明成基。
378
+ * 修前(core ≤7.45)缺席 ⇒ 整表拒(→ 422):沙箱车道上相对 / `/x` 形规则恒 422。绝对形(`//abs`)不需要基;`~/` 形见下一字段。
379
379
  */
380
380
  taskRoot?: string;
381
381
  /**
@@ -1341,6 +1341,13 @@ export declare class ToolApprovalCoordinator {
1341
1341
  * 流没了,值相等即目标集;极端复用形下多转的那条也只是从流内卡变 durable 卡,方向保守。
1342
1342
  * 返回触发条数(0 = 无流内未决 ask,调用方照旧 abort)。幂等:已结算条目跳过,重复调用无害。 */
1343
1343
  parkOpenAsksForTask(wireTaskId: string): number;
1344
+ /**
1345
+ * 编排器片 2 件 2 —— 下面 `drainForShutdown` 的第 ① 步,提前到**排空进入**那一拍(`boot/shutdown.ts` 的唯一排空入口调):锁存
1346
+ * {@link shutdownDraining},本进程握着的每一张未决卡按断连同路改路耐久停驻(core `onAsk` 答 "unavailable" 的合同 —— 有 park 设施则停驻、
1347
+ * 没有则无头自动拒,fail-closed);之后**登记**的卡在登记那一拍同路(`askBroadcast` 读同一位)。幂等(再调只是再扫一遍未决表)。
1348
+ * 让出放在进入那一拍而不是硬停那一拍:活卡在排空窗里没有人能答到一只正在下线的副本上,早一拍停驻 = 早一拍能在替补上赎回。
1349
+ */
1350
+ beginDrain(): void;
1344
1351
  /**
1345
1352
  * 🔴 **S-929 —— 有序关停的审批面收尾(一只;`boot/shutdown.ts` 的 `hardShutdown` 起点调,SIGTERM / SIGINT / SIGHUP 三条停止路同一处)。**
1346
1353
  *
@@ -707,11 +707,14 @@ export class ToolApprovalCoordinator {
707
707
  }
708
708
  return n;
709
709
  }
710
- async drainForShutdown(deadlineMs) {
710
+ beginDrain() {
711
711
  this.shutdownDraining = true;
712
712
  for (const e of [...this.pending.values()])
713
713
  if (e.settledOutcome === undefined)
714
714
  e.forceParkNow();
715
+ }
716
+ async drainForShutdown(deadlineMs) {
717
+ this.beginDrain();
715
718
  while ((this.asksInFlight > 0 || this.pending.size > 0 || this.streams.size > 0 || this.parksAwaitingCommit.size > 0) && Date.now() < deadlineMs) {
716
719
  await new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_POLL_MS));
717
720
  }
@@ -148,8 +148,8 @@ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCal
148
148
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
149
149
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
150
150
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
151
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "allowedTools";
152
- type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "outputStyle";
151
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools";
152
+ type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence";
153
153
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
154
154
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
155
155
  type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles" | "responseModel" | "compaction";
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -63,10 +63,12 @@ export interface EngineNoticeWireRow {
63
63
  */
64
64
  export declare function sanitizeNoticeDetail(detail: Record<string, unknown> | undefined): Record<string, unknown>;
65
65
  /**
66
- * 通告 `detail` 里的**规则条目原串**过呈现口(`presentRuleEntry`:剥 C0 / 双向控制符、超长截断)—— 会话流行(经 {@link engineNoticeEventData})
67
- * 与运维日志行(`boot/runner-deps.ts` 的转发席)同调这一只。其余字段、其余码原样(多行文本不受影响)。
66
+ * 通告 `detail` 里的**规则文本**换成条目的脱敏原值({@link redactListEntry}:凭据换记号,不加引号、不转义 —— 机读位不是显示面,显示形只属于人读句)。
67
+ * 会话流行(经 {@link engineNoticeEventData},之后照常过 {@link sanitizeNoticeDetail})与运维日志行(`boot/runner-deps.ts` 的转发席,其余字段逐字)
68
+ * 同调这一只 ⇒ 两条出路上同一条规则文本逐字节同(会话流行另有 detail 串的 {@link NOTICE_TEXT_MAX} 字上限,超长的那一截只在日志行上),两只码里同一条
69
+ * 规则文本也同(引擎交来的两形本身一致时 —— `rule_row_dropped` 交的是引擎中和过的形,空白 / 超长的条目两只码今天不同,上游缝)。其余字段、其余码原样。
68
70
  */
69
- export declare function presentNoticeDetail(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
71
+ export declare function redactNoticeRuleText(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
70
72
  export declare function engineNoticeEventData(notice: {
71
73
  code: string;
72
74
  message: string;
@@ -1,5 +1,5 @@
1
1
  import { redactSecrets } from "./redact.js";
2
- import { presentRuleEntry } from "../rule-entry-list.js";
2
+ import { redactListEntry } from "../list-entry-display.js";
3
3
  import { recordFailOpen } from "../observability/fail-open.js";
4
4
  export const ENGINE_NOTICE_WIRE_CODES = [
5
5
  "memory.session_polluted",
@@ -34,6 +34,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
34
34
  "permission.ask_released_by_mode",
35
35
  "permission.read_deny_unapplied",
36
36
  "permission.rule_store_unavailable",
37
+ "permission.rule_row_dropped",
37
38
  "permission.allowed_tool_not_granted",
38
39
  "task.tool_turn_incomplete",
39
40
  ];
@@ -92,22 +93,28 @@ function sanitizeValue(v, depth) {
92
93
  }
93
94
  return undefined;
94
95
  }
95
- const RULE_ENTRY_DETAIL_ARRAYS = { "permission.allowed_tool_not_granted": "entries" };
96
- export function presentNoticeDetail(code, detail) {
97
- const key = Object.hasOwn(RULE_ENTRY_DETAIL_ARRAYS, code) ? RULE_ENTRY_DETAIL_ARRAYS[code] : undefined;
98
- if (key === undefined || detail === undefined)
96
+ const RULE_TEXT_DETAIL_FIELDS = {
97
+ "permission.allowed_tool_not_granted": { array: "entries", field: "entry" },
98
+ "permission.rule_row_dropped": { array: "rows", field: "rule" },
99
+ };
100
+ export function redactNoticeRuleText(code, detail) {
101
+ const at = Object.hasOwn(RULE_TEXT_DETAIL_FIELDS, code) ? RULE_TEXT_DETAIL_FIELDS[code] : undefined;
102
+ if (at === undefined || detail === undefined)
99
103
  return detail;
100
- const list = detail[key];
104
+ const list = detail[at.array];
101
105
  if (!Array.isArray(list))
102
106
  return detail;
103
- const presented = list.map((item) => item !== null && typeof item === "object" && typeof item.entry === "string" ? { ...item, entry: presentRuleEntry(item.entry) } : item);
104
- return { ...detail, [key]: presented };
107
+ const redacted = list.map((item) => {
108
+ const text = item !== null && typeof item === "object" ? item[at.field] : undefined;
109
+ return typeof text === "string" ? { ...item, [at.field]: redactListEntry(text) } : item;
110
+ });
111
+ return { ...detail, [at.array]: redacted };
105
112
  }
106
113
  export function engineNoticeEventData(notice, sessionId, nowMs) {
107
114
  return {
108
115
  code: notice.code,
109
116
  message: redactSecrets(String(notice.message)).slice(0, NOTICE_TEXT_MAX),
110
- detail: sanitizeNoticeDetail(presentNoticeDetail(notice.code, notice.detail)),
117
+ detail: sanitizeNoticeDetail(redactNoticeRuleText(notice.code, notice.detail)),
111
118
  sessionId,
112
119
  ts: nowMs,
113
120
  };
@@ -169,6 +169,70 @@ export declare const SELF_MINTED_WIRE: {
169
169
  };
170
170
  };
171
171
  };
172
+ /** 7.121.0:`GET /health` 自报位 `auth.kinds[]` 的成员(验过的身份来源闭集;这台部署的装配能回的那几个)。 */
173
+ readonly healthAuthKind: {
174
+ readonly at: "GET /health auth.kinds[]";
175
+ readonly members: {
176
+ service_token: {
177
+ attr: string;
178
+ since: string;
179
+ };
180
+ sso: {
181
+ attr: string;
182
+ since: string;
183
+ };
184
+ direct_door_jwt: {
185
+ attr: string;
186
+ since: string;
187
+ };
188
+ };
189
+ };
190
+ /** 7.121.0:`GET /health` 自报位 `auth.identityAttach`。 */
191
+ readonly healthIdentityAttach: {
192
+ readonly at: "GET /health auth.identityAttach";
193
+ readonly members: {
194
+ available: {
195
+ attr: string;
196
+ since: string;
197
+ };
198
+ unavailable: {
199
+ attr: string;
200
+ since: string;
201
+ };
202
+ };
203
+ };
204
+ /** 7.121.0:`GET /health` 自报位 `operator`。 */
205
+ readonly healthOperator: {
206
+ readonly at: "GET /health operator";
207
+ readonly members: {
208
+ present: {
209
+ attr: string;
210
+ since: string;
211
+ };
212
+ absent: {
213
+ attr: string;
214
+ since: string;
215
+ };
216
+ };
217
+ };
218
+ /** 7.121.0:`GET /health` 自报位 `center.sessionsVisible`(两个布尔 + 一个词;成员按 JSON 字面登记)。 */
219
+ readonly healthSessionsVisible: {
220
+ readonly at: "GET /health center.sessionsVisible";
221
+ readonly members: {
222
+ true: {
223
+ attr: string;
224
+ since: string;
225
+ };
226
+ false: {
227
+ attr: string;
228
+ since: string;
229
+ };
230
+ unknown: {
231
+ attr: string;
232
+ since: string;
233
+ };
234
+ };
235
+ };
172
236
  /** 本仓在出口对 core 值做的变换(值变、键不变)。 */
173
237
  readonly wireValueRule: {
174
238
  readonly at: "出口值变换(<端点>#<键> → 变换)";
@@ -183,6 +247,20 @@ export declare const SELF_MINTED_WIRE: {
183
247
  readonly attr: "redactErrorMessage";
184
248
  readonly since: "7.108.0";
185
249
  };
250
+ /**
251
+ * 7.120.0:通告 `detail` 里的规则文本 = 条目的脱敏原值(`list-entry-display.ts` `redactListEntry`,经 `engine-notice-wire.ts` 的
252
+ * `redactNoticeRuleText`;会话流行与运维日志行同一只;不加引号、不转义 —— 显示形只在人读句里)。修前(7.119.0):本仓手抄的呈现形
253
+ * (剥 C0 / 双向控制符、超过 160 字截断并标原长),运维日志行不脱敏。钉 test/notice-entry-presentation.test.ts。
254
+ */
255
+ readonly "engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": {
256
+ readonly attr: "redactListEntry";
257
+ readonly since: "7.120.0";
258
+ };
259
+ /** 7.120.0:同上一行同一只写法(码本版入册;引擎交来的是它中和过的规则文本,本仓再过脱敏读口)。钉同上「两只码」组。 */
260
+ readonly "engine_notice permission.rule_row_dropped#detail.rows[].rule": {
261
+ readonly attr: "redactListEntry";
262
+ readonly since: "7.120.0";
263
+ };
186
264
  };
187
265
  };
188
266
  };
@@ -60,11 +60,43 @@ export const SELF_MINTED_WIRE = {
60
60
  shutdown_before_park: { attr: "-", since: "7.116.0" },
61
61
  },
62
62
  },
63
+ healthAuthKind: {
64
+ at: "GET /health auth.kinds[]",
65
+ members: {
66
+ service_token: { attr: "-", since: "7.121.0" },
67
+ sso: { attr: "-", since: "7.121.0" },
68
+ direct_door_jwt: { attr: "-", since: "7.121.0" },
69
+ },
70
+ },
71
+ healthIdentityAttach: {
72
+ at: "GET /health auth.identityAttach",
73
+ members: {
74
+ available: { attr: "-", since: "7.121.0" },
75
+ unavailable: { attr: "-", since: "7.121.0" },
76
+ },
77
+ },
78
+ healthOperator: {
79
+ at: "GET /health operator",
80
+ members: {
81
+ present: { attr: "-", since: "7.121.0" },
82
+ absent: { attr: "-", since: "7.121.0" },
83
+ },
84
+ },
85
+ healthSessionsVisible: {
86
+ at: "GET /health center.sessionsVisible",
87
+ members: {
88
+ true: { attr: "-", since: "7.121.0" },
89
+ false: { attr: "-", since: "7.121.0" },
90
+ unknown: { attr: "-", since: "7.121.0" },
91
+ },
92
+ },
63
93
  wireValueRule: {
64
94
  at: "出口值变换(<端点>#<键> → 变换)",
65
95
  members: {
66
96
  "GET /v1/runs/:id#result.errorMessage": { attr: "redactErrorMessage", since: "7.57.0" },
67
97
  "POST /v1/side-query#errorMessage": { attr: "redactErrorMessage", since: "7.108.0" },
98
+ "engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": { attr: "redactListEntry", since: "7.120.0" },
99
+ "engine_notice permission.rule_row_dropped#detail.rows[].rule": { attr: "redactListEntry", since: "7.120.0" },
68
100
  },
69
101
  },
70
102
  };
@@ -46,7 +46,9 @@ which version the server speaks before you spend a handshake finding out.
46
46
  acquire the connection lease
47
47
  ◀── helloAck { gen, heartbeatIntervalMs,
48
48
  maxInflight, pendingInstructions,
49
- chunkWatermarks }
49
+ chunkWatermarks, reconnectGraceMs,
50
+ cancelGraceMs, chunkGapTimeoutMs,
51
+ executionCeilingGraceMs }
50
52
  … or helloReject { code }
51
53
  fence: kill/drain everything from older generations
52
54
  generationAck { gen } ───────────────────────▶ now (and only now) the
@@ -79,7 +81,7 @@ which version the server speaks before you spend a handshake finding out.
79
81
  | `platformOs` | yes | closed set `darwin` \| `linux` (anything else is a `400`; there is no third value) |
80
82
  | `platformArch` | no | ≤ 32 bytes |
81
83
  | `displayName` | no | ≤ 255 bytes |
82
- | `workspaceRoot` | yes | non-empty, ≤ 1024 bytes — the device-side root that path resolution falls back to |
84
+ | `workspaceRoot` | yes | ≤ 1024 bytes, **an absolute POSIX path** — the device-side root that path resolution and `exec` `cwd` fall back to. Registration refuses anything else (empty, relative, `~/…`) with `400 request.field_invalid`; the same check runs again when a session's execution environment is created, so a device registered with a non-absolute root by an older server is refused there |
83
85
 
84
86
  `deviceId` and the owner are **not** inputs: the server mints `dev_<26-char ULID>` and takes the owner
85
87
  from the verified credential. The response is `200 { deviceId }` and contains no secret of any kind.
@@ -251,6 +253,17 @@ backoff, exponential to a 60 s ceiling, jittered — except for `revoked`, which
251
253
  | `gen` | **canonical decimal string** (`"7"`, never `7`, never `"07"`) | the connection generation. Stamp it on every frame you send |
252
254
  | `pendingInstructions` | `string[]` | instruction ids the server still considers in flight for you — see §5 R2 |
253
255
  | `chunkWatermarks` | `{ [instructionId]: number }` | per instruction, the highest **contiguous** `streamSeq` the server has already accepted. `-1` means "nothing accepted yet" |
256
+ | `reconnectGraceMs` | integer ms | the reconnect grace **in force on this server** (R4): how long in-flight work survives a dropped socket. Deployment-tunable, default 120000 |
257
+ | `cancelGraceMs` | integer ms | the cancel grace in force (R11): how long after a `cancel` is delivered the server waits for your terminal `result`. Default 10000 |
258
+ | `chunkGapTimeoutMs` | integer ms | the chunk-gap timeout in force (R8): how long the server waits for a missing `streamSeq` before failing the instruction. Default 30000 |
259
+ | `executionCeilingGraceMs` | integer ms | the grace added to an instruction's `timeoutMs` before the server gives up and settles it as outcome-unknown (R12: the ceiling is `timeoutMs + executionCeilingGraceMs`). Default 30000 |
260
+
261
+ The four grace fields are the values the server's own timers are armed with — not documentation defaults. Size
262
+ your reconnect backoff ceiling, your `cancel` answer deadline, your retransmission cadence and your own
263
+ `timeoutMs` enforcement from them. A server that predates them omits all four; treat absence as "not told" and
264
+ fall back to the defaults in the table above, never as zero. For the same reason the published TypeScript type
265
+ (`DeviceHelloAckFrame` from `@sema-agent/server/device-protocol`) declares the four as optional; a server from
266
+ 7.120.0 on always sends all four.
254
267
 
255
268
  `gen` is a string because it is a 64-bit database value; parse it as an integer only if your language can
256
269
  hold it, and always **echo the string you were given** rather than re-formatting a number.
@@ -276,7 +289,7 @@ fenced — that half is yours, and it is the reason a device-level single-instan
276
289
 
277
290
  Send `goodbye { gen, reason, graceMs? }`. The server stops *starting* new dispatches for this connection
278
291
  and keeps collecting results for the ones already in flight, then closes the socket after `graceMs` (if you
279
- omit it, the deployment's reconnect grace — default 120000 ms — is used). Sending `goodbye` does not settle
292
+ omit it, the deployment's reconnect grace — `helloAck.reconnectGraceMs`, default 120000 ms — is used). Sending `goodbye` does not settle
280
293
  anything: finish or fail your in-flight instructions properly before you exit.
281
294
 
282
295
  ⚠️ **`goodbye` is not a hard barrier.** The check happens when a dispatch *enters* the pipeline, and a
@@ -341,9 +354,9 @@ zeros, a plus sign, whitespace or a float form are rejected as malformed.
341
354
  | `t` | Fields | Notes |
342
355
  |---|---|---|
343
356
  | `challenge` | `nonce` string, `protocolVersion` int | first frame the server sends, immediately on open |
344
- | `helloAck` | `ok`, `heartbeatIntervalMs`, `maxInflight`, `gen`, `pendingInstructions`, `chunkWatermarks` | §1.5 |
357
+ | `helloAck` | `ok`, `heartbeatIntervalMs`, `maxInflight`, `gen`, `pendingInstructions`, `chunkWatermarks`, `reconnectGraceMs`, `cancelGraceMs`, `chunkGapTimeoutMs`, `executionCeilingGraceMs` | §1.5 |
345
358
  | `helloReject` | `code`, `minProtocolVersion?` | §1.4; the socket closes right after |
346
- | `instruction` | `gen`, `instructionId`, `rootSessionId`, `issuedSeq` int, `kind`, `args`, `cwd?`, `shellEnv?`, `timeoutMs` int | one unit of work (§3) |
359
+ | `instruction` | `gen`, `instructionId`, `rootSessionId`, `issuedSeq` int, `kind`, `args`, `cwd?`, `shellEnv?`, `timeoutMs` int | one unit of work (§3). `cwd` appears on `exec` / `execStream` only, and always there (§3.1) |
347
360
  | `payload` | `gen`, `instructionId`, `part` (`begin` \| `chunk` \| `commit`), `offset?`, `dataB64?`, `totalLen?`, `sha256?` | the bytes for a write-family instruction (§3.4) |
348
361
  | `cancel` | `gen`, `instructionId` | idempotent; answer with a terminal `result` (§5 R11) |
349
362
  | `resultAck` | `gen`, `instructionId` | you may release that instruction's buffers |
@@ -416,8 +429,10 @@ silently forgiven: it becomes a gap or a missing terminal, and the instruction e
416
429
  ### 3.1 The closed set, its arguments, and the success outcome each one owes
417
430
 
418
431
  Every `path` in `args` arrives **already absolute and POSIX-normalized** — the server resolved it against
419
- the session's working directory before sending. `cwd` (when present) is likewise absolute; when it is
420
- absent, run in the instruction's implied working directory (the resolution base the server already used).
432
+ the session's working directory before sending. `exec` and `execStream` additionally carry `cwd`, which is
433
+ **always present and always absolute**: run the command in exactly that directory. There is no implicit
434
+ base to fall back to, so an executor needs no branch for a missing or relative `cwd`. Every other kind
435
+ carries no `cwd` — its paths are already absolute.
421
436
 
422
437
  <!-- closed-set: DEVICE_INSTRUCTION_KINDS -->
423
438
 
@@ -551,10 +566,37 @@ payload { part: "commit", instructionId, totalLen, sha256 }
551
566
  2. on `commit`, verify the length **and** the digest;
552
567
  3. only then rename into place — atomically, honouring the instruction's semantics (exclusive create,
553
568
  guarded expectation, append);
554
- 4. any failure at any step fails the **whole instruction** and leaves **no half-written file**.
569
+ 4. any failure at any step fails the **whole instruction** and leaves **no half-written file** — with one
570
+ exception, `appendFile`, which is stated next.
555
571
 
556
572
  `defaultPayloadPartBytes` is 262144, but do not assume it: use `offset` and `totalLen`.
557
573
 
574
+ **`appendFile` is the one write that cannot be all-or-nothing.** Steps 1–3 give `writeFile`,
575
+ `writeFileExclusive` and `writeFileGuarded` their guarantee by building the complete file beside the target
576
+ and swapping it in. An append has no such swap: it extends the existing target **in place**, so there is a
577
+ moment after which the target has changed and the instruction has not finished. What POSIX guarantees for an
578
+ `O_APPEND` write is that the position is taken at end-of-file atomically; it does not promise that the payload
579
+ lands in one piece. This protocol therefore asks you to treat `PIPE_BUF` bytes (the POSIX minimum is 512; Linux
580
+ uses 4096) as the largest append you may regard as indivisible — the conservative bound, borrowed from the
581
+ rule POSIX states for pipe writes — and to assume that anything larger can reach the file in several writes,
582
+ interleave with another writer's appends, or stop part-way. So what you answer depends on *when* you fail:
583
+
584
+ - **The target cannot have been touched yet** — the payload failed its length or digest check, the target
585
+ could not be opened, your own containment policy refused the path. Answer the ordinary code
586
+ (`permission_denied`, `not_found`, `is_directory`, `invalid`, …). Nothing changed, so the caller may safely
587
+ retry.
588
+ - **The target may already have been changed** — a short write, an I/O error part-way through, or the process
589
+ or the machine going away while the append was running, so that you cannot say how much landed. Answer
590
+ `outcome_unknown` (§3.3). Never answer `timeout` or `transport_lost` here: both are retryable, and a retry
591
+ appends the content a second time.
592
+ - **The whole payload was appended.** Answer `{ok:true, kind:"void"}`. Do not report success for a partial
593
+ append.
594
+
595
+ The server settles an executor-reported `outcome_unknown` exactly like one it reaches by its own deadlines:
596
+ the caller gets the code `outcome_unknown`, which nothing upstream retries, and your message is kept and
597
+ followed by a fixed sentence telling the model to verify the file's contents before running the append again.
598
+ So make your message specific about what you know (bytes written, where it stopped).
599
+
558
600
  ### 3.5 `writeFileGuarded` and its `expect`
559
601
 
560
602
  `expect` is `{ canonicalPath: string, fileId?: string, exclusive?: boolean, noFollow?: boolean }`:
@@ -605,7 +647,7 @@ server delivers in order and buffers out-of-order chunks; it acks with `chunkAck
605
647
  **cumulative** watermark meaning "everything up to and including this is accepted" (`-1` = nothing yet).
606
648
  You may release buffers at or below the watermark.
607
649
 
608
- - A gap that is not filled within the deployment's gap timeout (default 30 s) **fails the instruction**
650
+ - A gap that is not filled within the deployment's gap timeout (`helloAck.chunkGapTimeoutMs`, default 30 s) **fails the instruction**
609
651
  with `outcome_unknown` — output is never silently truncated.
610
652
  - The reorder buffer is bounded (default 64 entries / 2 MiB decoded). Running far ahead of the watermark
611
653
  overflows it and also fails the instruction.
@@ -711,7 +753,7 @@ process — that work is accounted for as "may have run".
711
753
 
712
754
  **R4 — no reconnect within the grace ⇒ the books close.**
713
755
  *Given* the socket dropped with `I` in flight, *when* no connection for this device is established within
714
- the reconnect grace (default 120 s), *then* `I` settles `outcome_unknown`; a later reconnect does not list
756
+ the reconnect grace (`helloAck.reconnectGraceMs`, default 120 s), *then* `I` settles `outcome_unknown`; a later reconnect does not list
715
757
  it, and its `result` is either refused or memo-acked without being believed (§2.4).
716
758
  *Executor:* reconnect promptly. If you were offline longer than the grace, your results are no longer
717
759
  deliverable — that is not a licence to re-run anything.
@@ -742,7 +784,7 @@ as the original — the first copy the server buffered is the one it will delive
742
784
 
743
785
  **R8 — a gap is a failure, not a truncation.**
744
786
  *Given* the server is waiting for `streamSeq k`, *when* `k` does not arrive within the gap timeout
745
- (default 30 s) or the reorder buffer overflows (default 64 entries / 2 MiB), *then* `I` fails with
787
+ (`helloAck.chunkGapTimeoutMs`, default 30 s) or the reorder buffer overflows (default 64 entries / 2 MiB), *then* `I` fails with
746
788
  `outcome_unknown` — the command already ran, so the result cannot be faithfully reported.
747
789
  *Executor:* never skip a `streamSeq`, never renumber after a reconnect, never "compact" a stream.
748
790
 
@@ -766,18 +808,18 @@ machine from taking over the lease mid-command; only you can.
766
808
 
767
809
  **R11 — `cancel` demands a terminal, within a grace.**
768
810
  *Given* the server sent `cancel` for `I`, *when* you do not send a terminal `result` within the cancel
769
- grace (default 10 s, restarted each time the cancel is delivered), *then* `I` settles `outcome_unknown`.
811
+ grace (`helloAck.cancelGraceMs`, default 10 s, restarted each time the cancel is delivered), *then* `I` settles `outcome_unknown`.
770
812
  `cancel` is idempotent, is delivered at most once per generation, and **is replayed on the first active
771
813
  tick after a same-epoch reconnect** (a cancel written to a socket that then dropped may never have
772
814
  arrived).
773
815
  *Executor:* treat a repeated `cancel` for the same `I` as a no-op. Answer exactly one terminal:
774
816
  `{ok:true, kind:"cancelled", state:"not_started"}` if the work had not begun, `state:"killed"` if you
775
- killed it, or the work's ordinary outcome if it had already finished. Answer promptly — 10 s is the budget.
817
+ killed it, or the work's ordinary outcome if it had already finished. Answer promptly — `cancelGraceMs` (default 10 s) is the budget.
776
818
 
777
819
  **R12 — two-layer timeout: yours is authoritative, the server's is a backstop.**
778
820
  *Given* an instruction carrying `timeoutMs`, *when* that budget elapses, *then* you must kill the work and
779
821
  answer `{ok:false, errorCode:"timeout"}` — that is the only path to the retryable `timeout` word. *When*
780
- you answer nothing, the server gives up at `timeoutMs` + a grace (default 30 s) and settles
822
+ you answer nothing, the server gives up at `timeoutMs` + `helloAck.executionCeilingGraceMs` (default 30 s) and settles
781
823
  `outcome_unknown`; a later result for `I` is then undeliverable.
782
824
  *Executor:* enforce `timeoutMs` yourself, and prefer answering late-but-honestly over not answering.
783
825
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.119.0",
3
+ "version": "7.121.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -64,8 +64,8 @@
64
64
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
65
65
  },
66
66
  "dependencies": {
67
- "@sema-agent/core": "7.45.0",
68
- "@sema-agent/settings-schema": "8.0.0",
67
+ "@sema-agent/core": "7.46.0",
68
+ "@sema-agent/settings-schema": "9.0.0",
69
69
  "e2b": "^2.28.0",
70
70
  "libsodium-wrappers": "^0.8.4",
71
71
  "mysql2": "^3.22.4",