@sema-agent/server 7.124.1 → 7.125.1

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 (152) hide show
  1. package/MIGRATION.md +138 -2
  2. package/USAGE.md +46 -8
  3. package/dist/audit.d.ts +27 -5
  4. package/dist/audit.js +42 -8
  5. package/dist/boot/execution-env.d.ts +33 -1
  6. package/dist/boot/execution-env.js +31 -2
  7. package/dist/boot/resolve-spec.js +4 -3
  8. package/dist/boot/session-faces.d.ts +14 -1
  9. package/dist/boot/session-faces.js +6 -5
  10. package/dist/boot/stage-02-web-admin-pull.js +5 -1
  11. package/dist/boot/stage-05-execution-env.js +6 -2
  12. package/dist/boot/stage-09-leader.d.ts +1 -1
  13. package/dist/boot/stage-10-http-server.d.ts +1 -1
  14. package/dist/boot/web-admin.js +130 -140
  15. package/dist/capabilities/center-wiring.d.ts +2 -1
  16. package/dist/capabilities/composite-members.d.ts +1 -0
  17. package/dist/capabilities/composite-members.js +1 -0
  18. package/dist/capability-facts.d.ts +28 -3
  19. package/dist/capability-facts.js +20 -0
  20. package/dist/config-catalog.js +4 -1
  21. package/dist/config-d-migration.js +2 -1
  22. package/dist/config-lkg.d.ts +4 -1
  23. package/dist/config-lkg.js +16 -12
  24. package/dist/config-provider.d.ts +4 -1
  25. package/dist/config-provider.js +1 -0
  26. package/dist/config-types.d.ts +10 -3
  27. package/dist/config.js +44 -14
  28. package/dist/env-facts.d.ts +2 -2
  29. package/dist/env-facts.js +3 -2
  30. package/dist/fleet/fleet-bus.d.ts +24 -12
  31. package/dist/fleet/fleet-bus.js +4 -1
  32. package/dist/fleet/workspace-snapshot-residual.d.ts +10 -0
  33. package/dist/fleet/workspace-snapshot-residual.js +35 -0
  34. package/dist/hooks/hook-runner.d.ts +15 -27
  35. package/dist/hooks/hook-runner.js +19 -59
  36. package/dist/http/active-run-conflict.d.ts +3 -0
  37. package/dist/http/active-run-conflict.js +3 -0
  38. package/dist/http/admission.js +7 -7
  39. package/dist/http/approval-sweeps.js +2 -4
  40. package/dist/http/device-placement.js +1 -0
  41. package/dist/http/dispatch.js +1 -1
  42. package/dist/http/durable-run-leg.d.ts +20 -6
  43. package/dist/http/durable-run-leg.js +4 -4
  44. package/dist/http/idempotency.d.ts +83 -38
  45. package/dist/http/idempotency.js +38 -13
  46. package/dist/http/request-guards.d.ts +18 -0
  47. package/dist/http/request-guards.js +12 -3
  48. package/dist/http/resume-legs.js +7 -1
  49. package/dist/http/route-ctx.d.ts +3 -3
  50. package/dist/http/route-ctx.js +1 -1
  51. package/dist/http/routes/a2a-serve.js +27 -11
  52. package/dist/http/routes/capabilities.js +1 -0
  53. package/dist/http/routes/fleet.js +2 -0
  54. package/dist/http/routes/images.js +2 -2
  55. package/dist/http/routes/leader.d.ts +4 -0
  56. package/dist/http/routes/leader.js +13 -3
  57. package/dist/http/routes/observability.js +2 -0
  58. package/dist/http/routes/runs.d.ts +6 -2
  59. package/dist/http/routes/runs.js +71 -101
  60. package/dist/http/routes/session-sync.d.ts +3 -0
  61. package/dist/http/routes/session-sync.js +30 -1
  62. package/dist/http/routes/sessions-list.d.ts +11 -0
  63. package/dist/http/routes/sessions-list.js +6 -6
  64. package/dist/http/routes/sessions.d.ts +6 -1
  65. package/dist/http/routes/sessions.js +41 -10
  66. package/dist/http/routes/tasks.js +63 -21
  67. package/dist/http/run-read-face.d.ts +26 -0
  68. package/dist/http/run-read-face.js +67 -0
  69. package/dist/http/server.d.ts +4 -4
  70. package/dist/http/server.js +1 -2
  71. package/dist/http/submit-replay.d.ts +40 -0
  72. package/dist/http/submit-replay.js +22 -0
  73. package/dist/leader/endpoint.d.ts +10 -5
  74. package/dist/leader/endpoint.js +27 -5
  75. package/dist/observability/cost-quota.d.ts +51 -7
  76. package/dist/observability/cost-quota.js +35 -17
  77. package/dist/observability/fail-open.d.ts +1 -1
  78. package/dist/observability/fail-open.js +1 -1
  79. package/dist/observability/run-terminal.d.ts +5 -9
  80. package/dist/observability/run-terminal.js +2 -7
  81. package/dist/plugins/approval-ask-store-sql.js +1 -1
  82. package/dist/plugins/caching-session-store.d.ts +7 -2
  83. package/dist/plugins/caching-session-store.js +6 -0
  84. package/dist/plugins/device-store-sql.js +1 -1
  85. package/dist/plugins/ensure-index.js +22 -0
  86. package/dist/plugins/file-run-store.d.ts +24 -14
  87. package/dist/plugins/file-run-store.js +52 -12
  88. package/dist/plugins/leader-run-store-sql.d.ts +1 -11
  89. package/dist/plugins/leader-run-store-sql.js +1 -5
  90. package/dist/plugins/local-session-store.d.ts +35 -6
  91. package/dist/plugins/local-session-store.js +87 -18
  92. package/dist/plugins/memory-run-store.d.ts +13 -12
  93. package/dist/plugins/memory-run-store.js +25 -0
  94. package/dist/plugins/one-time-migrations.js +1 -1
  95. package/dist/plugins/pg-cost-quota.d.ts +1 -1
  96. package/dist/plugins/pg-cost-quota.js +2 -5
  97. package/dist/plugins/pg-pool.js +3 -0
  98. package/dist/plugins/pg-session-storage.d.ts +11 -1
  99. package/dist/plugins/pg-session-storage.js +44 -8
  100. package/dist/plugins/remote-env-k8s.d.ts +127 -12
  101. package/dist/plugins/remote-env-k8s.js +270 -27
  102. package/dist/plugins/run-store-sql.d.ts +26 -12
  103. package/dist/plugins/run-store-sql.js +50 -25
  104. package/dist/plugins/schema-consistency.d.ts +1 -13
  105. package/dist/plugins/schema-consistency.js +1 -41
  106. package/dist/plugins/schema-indexes.js +1 -0
  107. package/dist/plugins/schema-live-columns.d.ts +20 -0
  108. package/dist/plugins/schema-live-columns.js +42 -0
  109. package/dist/plugins/store-contracts.d.ts +40 -2
  110. package/dist/plugins/tidb-cost-quota.d.ts +1 -1
  111. package/dist/plugins/tidb-cost-quota.js +2 -5
  112. package/dist/plugins/tidb-pool.js +14 -3
  113. package/dist/plugins/tidb-session-store.d.ts +22 -2
  114. package/dist/plugins/tidb-session-store.js +33 -6
  115. package/dist/plugins/write-behind-counter.d.ts +12 -5
  116. package/dist/plugins/write-behind-counter.js +14 -7
  117. package/dist/prompts-domain-validate.js +3 -2
  118. package/dist/rules-consent.d.ts +2 -2
  119. package/dist/rules-consent.js +2 -2
  120. package/dist/run-local.js +31 -23
  121. package/dist/runs.d.ts +9 -0
  122. package/dist/runs.js +8 -0
  123. package/dist/runtime-governance.d.ts +3 -3
  124. package/dist/runtime-governance.js +2 -4
  125. package/dist/security.d.ts +19 -2
  126. package/dist/session-row.d.ts +53 -0
  127. package/dist/session-row.js +47 -0
  128. package/dist/session-sync.d.ts +6 -0
  129. package/dist/session-sync.js +5 -1
  130. package/dist/session-titler.d.ts +9 -5
  131. package/dist/session-titler.js +3 -1
  132. package/dist/spec-fields.js +2 -2
  133. package/dist/task-cwd.d.ts +0 -20
  134. package/dist/task-cwd.js +2 -10
  135. package/dist/tool-approval.d.ts +5 -7
  136. package/dist/trace/redact.d.ts +23 -50
  137. package/dist/trace/redact.js +42 -9
  138. package/dist/trace/self-minted-wire.d.ts +25 -0
  139. package/dist/trace/self-minted-wire.js +9 -0
  140. package/dist/web-admin-client/apply-effective.d.ts +10 -12
  141. package/dist/web-admin-client/apply-effective.js +2 -3
  142. package/dist/web-admin-client/candidate-gate.d.ts +69 -0
  143. package/dist/web-admin-client/candidate-gate.js +27 -0
  144. package/dist/web-admin-client/http-client.d.ts +2 -1
  145. package/dist/web-admin-client/http-client.js +2 -1
  146. package/dist/web-admin-client/pull-state.d.ts +6 -4
  147. package/dist/web-admin-client/read-warnings.d.ts +29 -5
  148. package/dist/web-admin-client/read-warnings.js +24 -4
  149. package/dist/web-admin-client/stage-limits.js +4 -1
  150. package/dist/workspace-space.d.ts +41 -0
  151. package/dist/workspace-space.js +5 -0
  152. package/package.json +3 -3
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ import { gatedPrincipal } from "./principal-gate.js";
1
3
  export class IdempotencyCache {
2
4
  ttlMs;
3
5
  sweepThreshold;
@@ -9,7 +11,7 @@ export class IdempotencyCache {
9
11
  peek(key) {
10
12
  return this.live(key);
11
13
  }
12
- run(key, produce, shouldCache = () => true) {
14
+ run(key, produce) {
13
15
  if (!key)
14
16
  return produce();
15
17
  this.maybeSweep();
@@ -17,17 +19,13 @@ export class IdempotencyCache {
17
19
  if (hit)
18
20
  return hit;
19
21
  const promise = produce();
20
- this.entries.set(key, { at: Date.now(), promise });
21
- promise.then((value) => {
22
- let cacheable = false;
23
- try {
24
- cacheable = shouldCache(value);
25
- }
26
- catch {
27
- }
28
- if (!cacheable)
22
+ const entry = { at: Date.now(), promise };
23
+ this.entries.set(key, entry);
24
+ const settle = () => {
25
+ if (this.entries.get(key) === entry)
29
26
  this.entries.delete(key);
30
- }, () => this.entries.delete(key));
27
+ };
28
+ promise.then(settle, settle);
31
29
  return promise;
32
30
  }
33
31
  get size() {
@@ -52,7 +50,34 @@ export class IdempotencyCache {
52
50
  this.entries.delete(k);
53
51
  }
54
52
  }
55
- export function scopedIdempotencyKey(raw, source, principal) {
56
- return raw ? `${source ?? ""}\x1f${principal ?? ""}\x1f${raw}` : undefined;
53
+ function idemFamilyTag(family) {
54
+ switch (family) {
55
+ case "run":
56
+ return "run";
57
+ case "leader":
58
+ return "leader";
59
+ case "bake":
60
+ return "bake";
61
+ default: {
62
+ const unknown = family;
63
+ throw new Error(`idemKeyOf: unknown resource family ${String(unknown)}`);
64
+ }
65
+ }
66
+ }
67
+ export function idemCallerOf(req, ctx) {
68
+ return { principal: gatedPrincipal(req, ctx.deps.config), source: ctx.req.source };
69
+ }
70
+ export function idemIdentityOf(caller) {
71
+ if (caller.principal !== undefined && caller.principal !== null)
72
+ return `principal\x1e${caller.principal}`;
73
+ if (caller.source !== undefined && caller.source !== null)
74
+ return `source\x1e${caller.source}`;
75
+ return "";
76
+ }
77
+ export function idemKeyOf(raw, caller, family) {
78
+ return createHash("sha256").update(`${idemIdentityOf(caller)}\x1f${idemFamilyTag(family)}\x1f${raw}`).digest("hex");
79
+ }
80
+ export function inflightKeyOf(idemKey, face, clientTaskId) {
81
+ return `${idemKey}\x1f${face}\x1f${clientTaskId ?? ""}`;
57
82
  }
58
83
  //# sourceMappingURL=idempotency.js.map
@@ -6,6 +6,7 @@
6
6
  import type { IncomingMessage, ServerResponse } from "node:http";
7
7
  import type { FlatServiceDeps } from "./server.js";
8
8
  import type { LeaseRefusalAdmission } from "../fleet-lease.js";
9
+ import { type QuotaDecision } from "../observability/cost-quota.js";
9
10
  /**
10
11
  * S-866 ③ —— 租约拒绝的应答形({@link leaseRefusalOf} 的产物;`{status, body}` 交回形 + 镜像头用的 `retryAfterSec`)。
11
12
  * 体键序固定 `error, errorCode, …`(冻结门 `FROZEN_RESULT_BODY` 按这个字面形锚)。
@@ -30,6 +31,23 @@ export interface LeaseRefusal {
30
31
  * 一只映射之后两形同体(resume 族的 429 体多出中心的 E4 字段,additive)。
31
32
  */
32
33
  export declare function leaseRefusalOf(adm: LeaseRefusalAdmission): LeaseRefusal;
34
+ /**
35
+ * 成本配额越顶 → 429 `limit.cost_quota_exceeded` 的**唯一**体铸点(三只门同调:路由门 `quotaExceeded`、属主门
36
+ * `ownerCostQuotaDenied`、赎回席 `approval-sweeps.ts`;修前三份手抄)。体键序 `error, errorCode, …`(冻结门
37
+ * `FROZEN_RESULT_BODY` 按这个字面形锚)。`retryAfterSec` 与 `windowResetsAt` 同源于决策的 `windowEndsAtMs`
38
+ * (同一次读钟),`Retry-After` 头由各发送点从体里的 `retryAfterSec` 镜像。
39
+ */
40
+ export declare function costQuotaRefusalOf(d: QuotaDecision): {
41
+ status: 429;
42
+ body: {
43
+ error: string;
44
+ errorCode: string;
45
+ usedMicroUsd: number;
46
+ limitMicroUsd: number;
47
+ retryAfterSec: number;
48
+ windowResetsAt: string;
49
+ };
50
+ };
33
51
  export declare function createRequestGuards(ctx: {
34
52
  deps: FlatServiceDeps;
35
53
  }): {
@@ -3,6 +3,7 @@ import { HttpError, principalFrom } from "../security.js";
3
3
  import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
4
4
  import { sendError } from "./send.js";
5
5
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
6
+ import { quotaWindowResetsAt } from "../observability/cost-quota.js";
6
7
  const MAX_BODY = 8 * 1024 * 1024;
7
8
  export function leaseRefusalOf(adm) {
8
9
  switch (adm.reason) {
@@ -20,6 +21,12 @@ export function leaseRefusalOf(adm) {
20
21
  }
21
22
  }
22
23
  }
24
+ export function costQuotaRefusalOf(d) {
25
+ return {
26
+ status: 429,
27
+ body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec, windowResetsAt: quotaWindowResetsAt(d) },
28
+ };
29
+ }
23
30
  function sendLeaseRefusal(res, refusal) {
24
31
  const { error, errorCode, ...extra } = refusal.body;
25
32
  res.setHeader("retry-after", String(refusal.retryAfterSec));
@@ -37,8 +44,10 @@ export function createRequestGuards(ctx) {
37
44
  if (d.allowed)
38
45
  return false;
39
46
  deps.metrics?.inc("cost_quota_rejected_total");
40
- res.setHeader("retry-after", String(d.retryAfterSec));
41
- sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
47
+ const { status, body } = costQuotaRefusalOf(d);
48
+ const { error, errorCode, ...extra } = body;
49
+ res.setHeader("retry-after", String(body.retryAfterSec));
50
+ sendError(res, status, errorCode, error, extra);
42
51
  return true;
43
52
  }
44
53
  function ownerCostQuotaDenied(principal) {
@@ -48,7 +57,7 @@ export function createRequestGuards(ctx) {
48
57
  if (d.allowed)
49
58
  return undefined;
50
59
  deps.metrics?.inc("cost_quota_rejected_total");
51
- return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
60
+ return costQuotaRefusalOf(d);
52
61
  }
53
62
  async function ownerFleetLeaseDenied(owner) {
54
63
  if (!deps.fleetLease || !owner)
@@ -511,6 +511,9 @@ export function createResumeLegs(ctx) {
511
511
  source: args.mintFreshRun.source,
512
512
  objectivePreview: buildRunObjectivePreview(resumeObjective ?? ""),
513
513
  });
514
+ if (created.existing) {
515
+ throw new Error(`wake fresh run: createRun observed an existing row for the freshly minted taskId ${freshId} (no idempotency key was sent) — refusing to treat it as a busy session`);
516
+ }
514
517
  if (!created.ok) {
515
518
  return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
516
519
  }
@@ -521,7 +524,7 @@ export function createResumeLegs(ctx) {
521
524
  freshRunClaimed = true;
522
525
  }
523
526
  const fleetPub = taskId && deps.fleetBus
524
- ? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, ...fleetRunLabels(resumeObjective) })
527
+ ? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, sessionId, ...fleetRunLabels(resumeObjective) })
525
528
  : undefined;
526
529
  const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
527
530
  if (deps.toolApproval && taskId) {
@@ -1217,6 +1220,9 @@ export function createResumeLegs(ctx) {
1217
1220
  source: "workflow_parked_decide",
1218
1221
  trustedInternals,
1219
1222
  });
1223
+ if (out.existing) {
1224
+ throw new Error(`decide host turn: createRun observed an existing row ${out.existing.taskId} for a freshly minted taskId (no idempotency key was sent)`);
1225
+ }
1220
1226
  if (out.status !== 202)
1221
1227
  return { status: out.status, body: out.body };
1222
1228
  const minted = out.body;
@@ -579,11 +579,11 @@ export type RouteIdsOf<T extends readonly RouteDecl[]> = Extract<T[number], {
579
579
  * (抛),不静默 200 也不静默 404。
580
580
  */
581
581
  export declare function assertNever(x: never): never;
582
- /** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 —— 加第五个动词是编译红。 */
583
- export type RouteMethod = "GET" | "POST" | "PUT" | "DELETE";
582
+ /** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 —— 加一个动词是编译红(`PATCH` = 会话行可写位,`PATCH /v1/sessions/:id`)。 */
583
+ export type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
584
584
  /** 闭集的**规范序**(405 的 `Allow` 头按它排,于是同一条路径的头字节恒定,不随声明序抖)。
585
585
  * `satisfies` 让它与 {@link RouteMethod} 双向闭合:漏一个动词 / 多一个都是编译红。 */
586
- export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "DELETE"];
586
+ export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
587
587
  export interface RouteDeclFacts {
588
588
  /** 「这条 url **烧模型**」—— drain / model-roster-pending / 租约过期 / 无 service-token 四道 503 的共同判别。
589
589
  * 没有这一列 = 不烧模型(零默认值:要它在场必须显式写出来,漏写的方向是**少收一笔账单**而不是
@@ -20,7 +20,7 @@ export function sendResumeOutcome(res, out, logger, extraBody) {
20
20
  export function assertNever(x) {
21
21
  throw new Error(`unreachable route id: ${String(x)}`);
22
22
  }
23
- export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
23
+ export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE"];
24
24
  export function buildHostLegOutcome(out) {
25
25
  return { status: out.status, body: out.body };
26
26
  }
@@ -3,7 +3,9 @@ import { createHash } from "node:crypto";
3
3
  import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
4
4
  import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
5
5
  import { HttpError } from "../../security.js";
6
- import { scopedIdempotencyKey } from "../idempotency.js";
6
+ import { idemCallerOf, idemIdentityOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
7
+ import { isRunExistsConflictBody, replayOfExistingRun } from "../submit-replay.js";
8
+ import { isActiveRunConflictBody } from "../active-run-conflict.js";
7
9
  import { sendJson, sendError } from "../send.js";
8
10
  import { admissionRefusalOf, readinessOf, sendBillableRefusal } from "../readiness.js";
9
11
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
@@ -228,28 +230,33 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
228
230
  const skillId = skill?.id;
229
231
  const scenario = skill?.scenario;
230
232
  const blocking = readBlocking(params);
231
- const idemScope = `${ctx.req.source ?? ""}\x1f${gatedPrincipal(req, deps.config) ?? ""}`;
233
+ const caller = idemCallerOf(req, ctx);
232
234
  const fingerprint = createHash("sha256")
233
235
  .update(messageId).update("\x1f")
234
236
  .update(objective).update("\x1f")
235
237
  .update(contextId ?? "").update("\x1f")
236
238
  .update(skillId ?? "")
237
239
  .digest("hex");
238
- const idemKey = scopedIdempotencyKey(fingerprint, ctx.req.source, gatedPrincipal(req, deps.config));
240
+ const submitter = caller.principal ?? undefined;
241
+ const idemKey = idemKeyOf(fingerprint, caller, "run");
239
242
  const body = {
240
243
  objective,
241
- taskId: durableIdempotencyTaskId(idemScope, fingerprint),
244
+ taskId: durableIdempotencyTaskId(idemIdentityOf(caller), fingerprint),
242
245
  ...(contextId !== undefined ? { sessionId: contextId } : {}),
243
246
  ...(scenario !== undefined ? { scenario } : {}),
244
247
  };
245
- const replay = await runStore.getRun(body.taskId).catch(() => undefined);
246
- if (replay && (replay.owner === null || replay.owner === gatedPrincipal(req, deps.config))) {
247
- return taskFromRun(replay, isStaleRun(replay, deps.config.runStaleSec));
248
+ const keyed = (await runStore.getRunByIdem(idemKey).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
249
+ const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId }, "durable", deps, runStore) : undefined;
250
+ if (keyed && replay?.status === 202) {
251
+ return taskFromRun(keyed, isStaleRun(keyed, deps.config.runStaleSec));
248
252
  }
249
- const cached = idemKey ? ctx.registry.idemCache.peek(idemKey) : undefined;
253
+ const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idemKey, "durable", body.taskId)) : undefined;
250
254
  let receipt;
251
- if (cached) {
252
- receipt = await cached;
255
+ if (replay !== undefined) {
256
+ receipt = replay;
257
+ }
258
+ else if (inflight) {
259
+ receipt = await inflight;
253
260
  }
254
261
  else {
255
262
  if (submitUnavailable(res, ctx))
@@ -270,7 +277,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
270
277
  return undefined;
271
278
  const receiptBody = receipt.body;
272
279
  if (receipt.status !== 202 || typeof receiptBody.taskId !== "string" || typeof receiptBody.sessionId !== "string") {
273
- throw new RpcError(JSONRPC_INVALID_PARAMS, `the serving deployment did not accept this submission (it answered ${receipt.status}); a session already running a turn must finish or be cancelled before the next message on the same contextId`);
280
+ throw rpcErrorFromRefusedReceipt(receipt);
274
281
  }
275
282
  const taskId = receiptBody.taskId;
276
283
  const sessionId = receiptBody.sessionId;
@@ -284,6 +291,15 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
284
291
  status: { state: "submitted", timestamp: new Date().toISOString() },
285
292
  };
286
293
  }
294
+ function rpcErrorFromRefusedReceipt(receipt) {
295
+ const code = receipt.body?.errorCode;
296
+ const head = `the serving deployment did not accept this submission (it answered ${receipt.status}${typeof code === "string" ? ` ${code}` : ""})`;
297
+ if (isActiveRunConflictBody(receipt.body))
298
+ return new RpcError(JSONRPC_INVALID_PARAMS, `${head}; a session already running a turn must finish or be cancelled before the next message on the same contextId`);
299
+ if (isRunExistsConflictBody(receipt.body))
300
+ return new RpcError(JSONRPC_INVALID_PARAMS, `${head}; this message's idempotency key already names a run this caller cannot take as this message's task — send it with a new messageId`);
301
+ return new RpcError(JSONRPC_INVALID_PARAMS, head);
302
+ }
287
303
  function durableIdempotencyTaskId(scope, messageId) {
288
304
  const digest = createHash("sha256").update(scope).update("\x1f").update(messageId).digest();
289
305
  const b = Buffer.from(digest.subarray(0, 16));
@@ -205,6 +205,7 @@ export async function handleCapabilities(req, res, match, ctx) {
205
205
  manualCompact: facts.durableRuns(deps).available,
206
206
  configCatalog: facts.configCatalog(deps).available,
207
207
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
208
+ workspaceSync: ((f) => (f.available ? f.policy : undefined))(facts.workspaceSpace(deps.config)),
208
209
  }));
209
210
  return;
210
211
  }
@@ -45,6 +45,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
45
45
  startedAt: "keep", elapsedMs: "keep", tokens: "keep",
46
46
  awaitingPlanApproval: "keep", toolUses: "keep", usage: "keep", resumable: "keep", sourceLane: "keep",
47
47
  cycleSeq: "keep", retiredBy: "keep",
48
+ workspaceSnapshot: "keep",
48
49
  name: "rewrite", scope: "strip", sessionId: "strip",
49
50
  description: "scrub", agentType: "scrub", agentName: "scrub", currentAction: "scrub",
50
51
  currentTool: "scrub", transcriptId: "scrub", stoppedBy: "scrub", editedFiles: "scrub",
@@ -70,6 +71,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
70
71
  ...(rest.sourceLane !== undefined ? { sourceLane: rest.sourceLane } : {}),
71
72
  ...(rest.cycleSeq !== undefined ? { cycleSeq: rest.cycleSeq } : {}),
72
73
  ...(rest.retiredBy !== undefined ? { retiredBy: rest.retiredBy } : {}),
74
+ ...(rest.workspaceSnapshot !== undefined ? { workspaceSnapshot: rest.workspaceSnapshot } : {}),
73
75
  };
74
76
  }
75
77
  return rest;
@@ -6,7 +6,7 @@ import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
6
6
  import { streamSseLog } from "../sse-log.js";
7
7
  import { sendJson, sendError } from "../send.js";
8
8
  import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired } from "../principal-gate.js";
9
- import { scopedIdempotencyKey } from "../idempotency.js";
9
+ import { idemCallerOf, idemKeyOf } from "../idempotency.js";
10
10
  import { assertNever } from "../route-ctx.js";
11
11
  import * as facts from "../../capability-facts.js";
12
12
  export function createImagesLocal(deps) {
@@ -93,7 +93,7 @@ export async function handleImages(req, res, match, ctx) {
93
93
  }
94
94
  const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
95
95
  const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
96
- const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, principal) ?? null : null;
96
+ const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "bake") : null;
97
97
  const createInput = {
98
98
  profile: v.profile,
99
99
  bands: v.bands,
@@ -18,6 +18,10 @@ export declare const LEADER_ROUTES: readonly [{
18
18
  readonly path: "/v1/leader";
19
19
  readonly billable: "submit";
20
20
  readonly methods: readonly ["POST"];
21
+ }, {
22
+ readonly id: "leader-by-idem";
23
+ readonly path: "/v1/leader";
24
+ readonly methods: readonly ["GET"];
21
25
  }, {
22
26
  readonly id: "leader-detail";
23
27
  readonly pattern: RegExp;
@@ -1,5 +1,6 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
3
+ import { idemCallerOf } from "../idempotency.js";
3
4
  import { assertNever } from "../route-ctx.js";
4
5
  import * as facts from "../../capability-facts.js";
5
6
  import { leaderEntryRefusal, leaderRunEntry } from "../../leader/run-entry.js";
@@ -8,6 +9,7 @@ export async function handleLeader(req, res, match, ctx) {
8
9
  switch (match.decl.id) {
9
10
  case "leader-submit":
10
11
  case "leader-detail":
12
+ case "leader-by-idem":
11
13
  break;
12
14
  default:
13
15
  return assertNever(match.decl.id);
@@ -24,7 +26,7 @@ export async function handleLeader(req, res, match, ctx) {
24
26
  sendPrincipalRefusal(res, req, deps.config);
25
27
  return;
26
28
  }
27
- const requester = gatedPrincipal(req, deps.config) ?? null;
29
+ const caller = idemCallerOf(req, ctx);
28
30
  if (match.decl.id === "leader-submit") {
29
31
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
30
32
  return;
@@ -41,14 +43,21 @@ export async function handleLeader(req, res, match, ctx) {
41
43
  sendError(res, 422, "request.autonomy_lane_refused", laneRefusal);
42
44
  return;
43
45
  }
44
- const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, requester, headerStr(req.headers["idempotency-key"]) ?? null);
46
+ const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, caller, headerStr(req.headers["idempotency-key"]) ?? null);
47
+ if (r) {
48
+ sendJson(res, r.status, r.body);
49
+ return;
50
+ }
51
+ }
52
+ else if (match.decl.id === "leader-by-idem") {
53
+ const r = await endpoint.handle("GET", LEADER_SUBMIT_PATH, undefined, caller, new URLSearchParams(match.search).get("idempotencyKey"));
45
54
  if (r) {
46
55
  sendJson(res, r.status, r.body);
47
56
  return;
48
57
  }
49
58
  }
50
59
  else {
51
- const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined, requester);
60
+ const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined, caller);
52
61
  if (r) {
53
62
  sendJson(res, r.status, r.body);
54
63
  return;
@@ -58,6 +67,7 @@ export async function handleLeader(req, res, match, ctx) {
58
67
  }
59
68
  export const LEADER_ROUTES = [
60
69
  { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: "submit", methods: ["POST"] },
70
+ { id: "leader-by-idem", path: LEADER_SUBMIT_PATH, methods: ["GET"] },
61
71
  { id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
62
72
  ];
63
73
  //# sourceMappingURL=leader.js.map
@@ -2,6 +2,7 @@ import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, isOperator, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
+ import { quotaWindowResetsAt } from "../../observability/cost-quota.js";
5
6
  export async function handleObservability(req, res, match, ctx) {
6
7
  const { deps } = ctx;
7
8
  switch (match.decl.id) {
@@ -64,6 +65,7 @@ export async function handleObservability(req, res, match, ctx) {
64
65
  remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
65
66
  overLimit: !d.allowed,
66
67
  retryAfterSec: d.retryAfterSec,
68
+ windowResetsAt: quotaWindowResetsAt(d),
67
69
  maxTaskCostUsd: deps.config.maxTaskCostUsd,
68
70
  maxTaskTokens: deps.config.maxTaskTokens,
69
71
  });
@@ -31,8 +31,8 @@ export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse
31
31
  * 防御性复检(缺席 ⇒ 501),但两个调用方都在自己的入口先判过,那才是各自面上的正确错误形。
32
32
  */
33
33
  export declare function createDurableRun(req: IncomingMessage, res: ServerResponse, ctx: RouteCtx, prepared: PreparedTaskSubmission,
34
- /** in-memory 幂等键(`POST /v1/runs` = `Idempotency-Key` 头派生;A2A = `message.messageId` 派生)。
35
- * undefined = 本次提交不参与 in-memory 去重(durable `body.taskId` 重放是独立的第二道)。 */
34
+ /** 这次提交的幂等键(`idemKeyOf(…, "run")`:`POST /v1/runs` = `Idempotency-Key` 头派生;A2A = `message.messageId`
35
+ * + 内容指纹派生)—— 落 `task_run.idem_key`,同时是同进程在飞合流的键。undefined = 调用方没带键。 */
36
36
  idemKey: string | undefined): Promise<{
37
37
  status: number;
38
38
  body: unknown;
@@ -42,6 +42,10 @@ export declare const RUNS_ROUTES: readonly [{
42
42
  readonly path: "/v1/runs";
43
43
  readonly billable: "submit";
44
44
  readonly methods: readonly ["POST"];
45
+ }, {
46
+ readonly id: "runs-by-idem";
47
+ readonly path: "/v1/runs";
48
+ readonly methods: readonly ["GET"];
45
49
  }, {
46
50
  readonly id: "run-steer";
47
51
  readonly pattern: RegExp;