@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,17 +1,13 @@
1
- import { persistedPlaneOf } from "../../terminal.js";
2
- import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
1
+ import { redactHead, redactLedgerEventForRead } from "../../observability/run-terminal.js";
3
2
  import { z } from "zod";
4
3
  import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
5
- import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
4
+ import { isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
6
5
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
6
  import { redactSecrets } from "../../trace/redact.js";
8
- import { HttpError, verifiedPrincipal, isUuidV7, encodeCheckpointScope } from "../../security.js";
7
+ import { HttpError, isUuidV7, encodeCheckpointScope } from "../../security.js";
9
8
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
10
9
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
11
- import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../../observability/cost-taxonomy.js";
12
- import { recordFailOpen } from "../../observability/fail-open.js";
13
- import { createHash } from "node:crypto";
14
- import { scopedIdempotencyKey } from "../idempotency.js";
10
+ import { idemCallerOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
15
11
  import { streamSseLog } from "../sse-log.js";
16
12
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
13
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
@@ -21,15 +17,15 @@ import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
21
17
  import { causeOfAskRow } from "../../approval-deny-reasons.js";
22
18
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
23
19
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
24
- import { readTurnActivityMs } from "../../turn-activity.js";
25
20
  import { mintCancelledResult } from "../../run-cancel-context.js";
26
21
  import { acceptedInputKindOf } from "../../user-input-ledger.js";
27
22
  import { INJECTION_TIERS } from "../../trace/injection-tier.js";
28
- import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
29
23
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
30
24
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
31
25
  import * as facts from "../../capability-facts.js";
32
26
  import { assertNever } from "../route-ctx.js";
27
+ import { readRunFace } from "../run-read-face.js";
28
+ import { replayOfExistingRun } from "../submit-replay.js";
33
29
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
34
30
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
35
31
  import { withPrincipal } from "../../observability/principal-context.js";
@@ -191,16 +187,27 @@ export async function handleRuns(req, res, match, ctx) {
191
187
  }
192
188
  const runStore = runs.runStore;
193
189
  const rawIdem = headerStr(req.headers["idempotency-key"]);
194
- const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
195
- const cached = idemKey ? idemCache.peek(idemKey) : undefined;
196
- if (cached) {
197
- const resp = await cached;
198
- sendJson(res, resp.status, resp.body);
199
- return;
190
+ const submitter = gatedPrincipal(req, deps.config);
191
+ const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "run") : undefined;
192
+ let keyedBody;
193
+ if (idemKey) {
194
+ keyedBody = await readJson(req);
195
+ const clientTaskId = clientTaskIdOf(keyedBody);
196
+ if (clientTaskId === false) {
197
+ sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
198
+ return;
199
+ }
200
+ const inflight = idemCache.peek(inflightKeyOf(idemKey, "durable", clientTaskId));
201
+ const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idemKey);
202
+ if (inflight || keyedRow) {
203
+ const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
204
+ sendJson(res, resp.status, resp.body);
205
+ return;
206
+ }
200
207
  }
201
208
  if (rateLimited(req, res) || quotaExceeded(req, res))
202
209
  return;
203
- const prepared = await prepareSpec(req, res);
210
+ const prepared = await prepareSpec(req, res, keyedBody);
204
211
  if (!prepared)
205
212
  return;
206
213
  const resp = await createDurableRun(req, res, ctx, prepared, idemKey);
@@ -209,23 +216,38 @@ export async function handleRuns(req, res, match, ctx) {
209
216
  sendJson(res, resp.status, resp.body);
210
217
  return;
211
218
  }
212
- if (match.decl.id === "run-detail" || match.decl.id === "run-events") {
219
+ if (match.decl.id === "run-detail" || match.decl.id === "run-events" || match.decl.id === "runs-by-idem") {
213
220
  const runs = facts.durableRuns(deps);
214
221
  if (!runs.available) {
215
222
  sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
216
223
  return;
217
224
  }
218
225
  const runStore = runs.runStore;
219
- const taskId = match.params.taskId;
220
226
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
221
227
  sendPrincipalRefusal(res, req, deps.config);
222
228
  return;
223
229
  }
224
- const run = await runStore.getRun(taskId);
230
+ let run;
231
+ let notFound;
232
+ if (match.decl.id === "runs-by-idem") {
233
+ const rawKey = new URLSearchParams(match.search).get("idempotencyKey");
234
+ if (!rawKey) {
235
+ sendError(res, 400, "request.query_invalid", "idempotencyKey is required: pass the raw Idempotency-Key the submission was sent with");
236
+ return;
237
+ }
238
+ run = await runStore.getRunByIdem(idemKeyOf(rawKey, idemCallerOf(req, ctx), "run"));
239
+ notFound = RUN_NOT_FOUND_MESSAGE;
240
+ }
241
+ else {
242
+ const taskId = match.params.taskId;
243
+ run = await runStore.getRun(taskId);
244
+ notFound = runNotFoundMessage(taskId);
245
+ }
225
246
  if (!run) {
226
- sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
247
+ sendError(res, 404, "not_found.run", notFound);
227
248
  return;
228
249
  }
250
+ const taskId = run.taskId;
229
251
  if (!runOwnerOk(req, res, run.owner))
230
252
  return;
231
253
  if (!runSessionAcceptOk(req, res, run, match.decl.id === "run-events" ? "run.events" : "run.poll"))
@@ -234,62 +256,7 @@ export async function handleRuns(req, res, match, ctx) {
234
256
  await streamRunEvents(req, res, deps, runStore, taskId, deps.config.runStaleSec * 1000);
235
257
  }
236
258
  else {
237
- const stale = run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
238
- const infraRates = deps.config.infraCostRates;
239
- const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
240
- const needSuggestions = !stale && run.status === "completed";
241
- const resourceWindow = run.status === "running" && !stale ? readResourceWindow(run.taskId) : undefined;
242
- let events;
243
- let eventsUnavailable = false;
244
- if (needCost || needSuggestions || resourceWindow !== undefined) {
245
- if (runStore.getEvents) {
246
- events = await runStore.getEvents(taskId, 0).catch(() => {
247
- eventsUnavailable = true;
248
- return [];
249
- });
250
- }
251
- else {
252
- eventsUnavailable = true;
253
- }
254
- }
255
- let supervisorCost;
256
- if (needCost && events && run.result?.stats) {
257
- const durMs = new Date(run.updatedAt).getTime() - new Date(run.createdAt).getTime();
258
- const usage = infraUsageFromEvents(events, Number.isFinite(durMs) ? durMs : 0);
259
- const st = run.result.stats;
260
- supervisorCost = composeSupervisorCost(st.costBreakdown ?? null, st.costMicroUsd ?? 0, infraCost(usage, infraRates));
261
- }
262
- let suggestions;
263
- if (needSuggestions && events) {
264
- const sug = [...events].reverse().find((e) => e.type === "suggestions");
265
- const arr = sug?.data?.suggestions;
266
- if (Array.isArray(arr))
267
- suggestions = arr.map((s) => String(s));
268
- }
269
- const lastActivityAt = run.status === "running" && !stale ? readTurnActivityMs(run.taskId) : undefined;
270
- let crossSliceUsage;
271
- if (resourceWindow !== undefined) {
272
- if (eventsUnavailable || events === undefined)
273
- recordFailOpen("server.runs.cross-slice-usage-unavailable");
274
- else
275
- crossSliceUsage = buildCrossSliceUsage(resourceWindow, events);
276
- }
277
- sendJson(res, 200, {
278
- taskId: run.taskId,
279
- sessionId: run.sessionId,
280
- status: stale ? "failed" : run.status,
281
- ...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
282
- ...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
283
- result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
284
- supervisorCost,
285
- suggestions,
286
- errorCode: stale ? RUN_HOLDER_DEATH_CODES.stalled : (persistedPlaneOf(run.result).errorCode ?? run.errorCode ?? undefined),
287
- error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
288
- jobId: run.jobId ?? undefined,
289
- source: run.source ?? undefined,
290
- heldBy: run.instanceId ?? null,
291
- cancelRequested: run.cancelRequested,
292
- });
259
+ sendJson(res, 200, await readRunFace(deps, runStore, run));
293
260
  }
294
261
  return;
295
262
  }
@@ -458,8 +425,7 @@ export async function handleRuns(req, res, match, ctx) {
458
425
  const text = body.text;
459
426
  const priority = body.priority;
460
427
  const steerIdemRaw = headerStr(req.headers["idempotency-key"]);
461
- const steerScoped = steerIdemRaw ? scopedIdempotencyKey(steerIdemRaw, source, principal) : undefined;
462
- const steerInputId = steerScoped ? `idem-${createHash("sha256").update(steerScoped).digest("hex").slice(0, 32)}` : undefined;
428
+ const steerInputId = steerIdemRaw ? `idem-${idemKeyOf(steerIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : undefined;
463
429
  const messageId = steerInputId ?? uuidv7();
464
430
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
465
431
  const steerInput = () => ({
@@ -774,8 +740,7 @@ export async function handleRuns(req, res, match, ctx) {
774
740
  text = interruptBody.text;
775
741
  }
776
742
  const interruptIdemRaw = headerStr(req.headers["idempotency-key"]);
777
- const interruptScoped = interruptIdemRaw ? scopedIdempotencyKey(interruptIdemRaw, source, principal) : undefined;
778
- const interruptInputId = interruptScoped ? `idem-${createHash("sha256").update(interruptScoped).digest("hex").slice(0, 32)}` : uuidv7();
743
+ const interruptInputId = interruptIdemRaw ? `idem-${idemKeyOf(interruptIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : uuidv7();
779
744
  const interruptMessageId = interruptInputId;
780
745
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
781
746
  if (!interruptBare) {
@@ -1623,7 +1588,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1623
1588
  }
1624
1589
  export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1625
1590
  const { deps } = ctx;
1626
- const { idemCache, inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
1591
+ const { idemCache } = ctx.registry;
1627
1592
  const { leaseDenied, usageWindowDenied } = ctx.helpers;
1628
1593
  const source = ctx.req.source;
1629
1594
  const runs = facts.durableRuns(deps);
@@ -1632,36 +1597,41 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1632
1597
  return null;
1633
1598
  }
1634
1599
  const runStore = runs.runStore;
1635
- const rawClientTaskId = prepared.body.taskId;
1636
- if (rawClientTaskId !== undefined && (typeof rawClientTaskId !== "string" || !isUuidV7(rawClientTaskId))) {
1600
+ const clientTaskId = clientTaskIdOf(prepared.body);
1601
+ if (clientTaskId === false) {
1637
1602
  sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
1638
1603
  return null;
1639
1604
  }
1640
- const clientTaskId = rawClientTaskId;
1605
+ const submitter = gatedPrincipal(req, deps.config);
1606
+ const caller = { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1641
1607
  if (clientTaskId) {
1642
1608
  const existing = await runStore.getRun(clientTaskId);
1643
- if (existing) {
1644
- const verified = verifiedPrincipal(req, deps.config);
1645
- if (existing.owner !== null && existing.owner !== verified) {
1646
- sendError(res, 409, "conflict.run_exists", "taskId already exists");
1647
- return null;
1648
- }
1649
- return { status: 202, body: { taskId: existing.taskId, sessionId: existing.sessionId, status: existing.status } };
1650
- }
1609
+ if (existing)
1610
+ return await replayOfExistingRun(existing, caller, "durable", deps, runStore);
1651
1611
  }
1652
1612
  if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
1653
1613
  return null;
1654
- const resp = await idemCache.run(idemKey, async () => await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1655
- prepared,
1656
- ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1657
- fleetScope: gatedPrincipal(req, deps.config) ?? "default",
1658
- elicitOwner: gatedPrincipal(req, deps.config) ?? null,
1659
- source,
1660
- }), (r) => r.status === 202);
1661
- return resp;
1614
+ return await idemCache.run(idemKey !== undefined ? inflightKeyOf(idemKey, "durable", clientTaskId) : undefined, async () => {
1615
+ const out = await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1616
+ prepared,
1617
+ ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1618
+ ...(idemKey !== undefined ? { idemKey } : {}),
1619
+ fleetScope: submitter ?? "default",
1620
+ elicitOwner: submitter ?? null,
1621
+ source,
1622
+ });
1623
+ return out.existing ? await replayOfExistingRun(out.existing, caller, "durable", deps, runStore) : out;
1624
+ });
1625
+ }
1626
+ function clientTaskIdOf(body) {
1627
+ const raw = typeof body === "object" && body !== null ? body.taskId : undefined;
1628
+ if (raw === undefined)
1629
+ return undefined;
1630
+ return typeof raw === "string" && isUuidV7(raw) ? raw : false;
1662
1631
  }
1663
1632
  export const RUNS_ROUTES = [
1664
1633
  { id: "runs-submit", path: "/v1/runs", billable: "submit", methods: ["POST"] },
1634
+ { id: "runs-by-idem", path: "/v1/runs", methods: ["GET"] },
1665
1635
  { id: "run-steer", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/steer$/, label: "/v1/runs/:id/steer", methods: ["POST"] },
1666
1636
  { id: "run-interrupt", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/interrupt$/, label: "/v1/runs/:id/interrupt", methods: ["POST"] },
1667
1637
  { id: "run-detach", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/detach$/, label: "/v1/runs/:id/detach", methods: ["POST"] },
@@ -11,6 +11,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
11
11
  import { type SessionRulesRecord, type FileHistoryExport } from "@sema-agent/core";
12
12
  import { type StagingHandle } from "../../session-sync-kernel.js";
13
13
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
14
+ import { type SessionRowPatch } from "../../session-row.js";
14
15
  export type PendingStaging = {
15
16
  handle: StagingHandle;
16
17
  realSessionId: string;
@@ -25,6 +26,8 @@ export type PendingStaging = {
25
26
  entryId: string;
26
27
  owner: string | null;
27
28
  }>;
29
+ /** 会话行的两位可写位(Phase A 体里带了哪几键就重放哪几键;老客户端不带 ⇒ `{}`,目的端两位不动)。提交后经会话店唯一写口落。 */
30
+ row: SessionRowPatch;
28
31
  /** 交叉复查 A-3(HIGH):Phase A's DECLARED entry-id set — Phase B verifies the streamed NDJSON matches it
29
32
  * (a different tree under the same staging would otherwise commit while the parked snapshots/anchors
30
33
  * still reference the DECLARED one). */
@@ -15,6 +15,7 @@ const SYNC_IMPORT_BATCH = 500;
15
15
  import { SHA256_HEX_RE } from "../../digest-form.js";
16
16
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
17
17
  import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
18
+ import { readSessionPinned, readSessionTitle } from "../../session-row.js";
18
19
  export function createSessionSyncLocal() {
19
20
  const pendingStagings = new Map();
20
21
  const importLeases = new Map();
@@ -74,6 +75,14 @@ export async function handleSessionSync(req, res, match, ctx) {
74
75
  }
75
76
  return true;
76
77
  };
78
+ const replaySessionRow = async (row, importedOwner) => {
79
+ if (Object.keys(row).length === 0)
80
+ return;
81
+ const face = await sessionStorage.sessionFaceOf(sessionId);
82
+ if (face === undefined || face.owner !== importedOwner)
83
+ return;
84
+ await sessionStorage.patchSessionMeta(sessionId, row, face);
85
+ };
77
86
  if (match.decl.id === "session-sync-manifest") {
78
87
  if (!pullGateOk())
79
88
  return;
@@ -247,6 +256,23 @@ export async function handleSessionSync(req, res, match, ctx) {
247
256
  sendError(res, 400, "request.field_invalid", "resolution must be 'overwrite-dst'");
248
257
  return;
249
258
  }
259
+ let row = {};
260
+ if ("title" in body) {
261
+ const read = readSessionTitle(body.title);
262
+ if ("problem" in read) {
263
+ sendError(res, 400, read.problem.code, read.problem.message);
264
+ return;
265
+ }
266
+ row = { ...row, title: read.title };
267
+ }
268
+ if ("pinned" in body) {
269
+ const read = readSessionPinned(body.pinned);
270
+ if ("problem" in read) {
271
+ sendError(res, 400, read.problem.code, read.problem.message);
272
+ return;
273
+ }
274
+ row = { ...row, pinned: read.pinned };
275
+ }
250
276
  const resolution = body.resolution;
251
277
  const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
252
278
  const entryIds = body.entryIds;
@@ -305,6 +331,7 @@ export async function handleSessionSync(req, res, match, ctx) {
305
331
  if (deps.resumeAnchorStore)
306
332
  for (const a of anchors)
307
333
  await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
334
+ await replaySessionRow(row, owner);
308
335
  sendJson(res, 200, {
309
336
  relation: "identical",
310
337
  basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
@@ -368,7 +395,7 @@ export async function handleSessionSync(req, res, match, ctx) {
368
395
  importLeases.set(sessionId, { at: Date.now(), stagingId });
369
396
  pendingStagings.set(stagingId, {
370
397
  handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
371
- fileHistory, policy, anchors, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
398
+ fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
372
399
  });
373
400
  sendJson(res, 200, { stagingId, relation: relA.relation });
374
401
  return;
@@ -494,6 +521,7 @@ export async function handleSessionSync(req, res, match, ctx) {
494
521
  if (anchorStore)
495
522
  for (const a of pending.anchors)
496
523
  await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
524
+ await replaySessionRow(pending.row, pending.owner);
497
525
  sessionStorage.evict?.(sessionId);
498
526
  cleanupStaging(stagingId);
499
527
  sendJson(res, 200, { relation: committed.relation });
@@ -540,6 +568,7 @@ function sendSessionSyncUnavailable(res, why) {
540
568
  switch (why) {
541
569
  case "store_backend_absent":
542
570
  case "session_owner_face_absent":
571
+ case "session_row_faces_absent":
543
572
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
544
573
  return;
545
574
  case "session_export_absent":
@@ -5,11 +5,22 @@
5
5
  * owner-scope 与 trace 列表同一权威模型(system/traceToken=fleet-wide,principal=钉住本人)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import { type SessionListItem } from "../../security.js";
9
+ import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
8
10
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
9
11
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
10
12
  * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
11
13
  * 跨域的东西(deps / 助手 / 单域状态)一律在首行解构出来,于是下面每一行都逐字不变。 */
12
14
  export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionsListRouteId>, ctx: RouteCtx): Promise<void>;
15
+ /**
16
+ * 会话列表行的 **wire 投影** —— `GET /v1/sessions` 的每一行与 `PATCH /v1/sessions/:id` 的 200 回执是**同一只**(店行 → wire 行只有这一处)。
17
+ * · 预览:店行读的是 run 行预览列的事实位(`""` = 有记录、剥完没有用户正文;NULL = 没记录);本面只说「有没有可显示的预览」⇒ 两者都是 `null`。
18
+ * · `tempo`:见上方 [ref] 注(由 `lastStatus` 经唯一一张表派生;omit-when-absent)。
19
+ * · 其余键(含 `title` / `pinned`)零投影透传 —— 店行是唯一铸点。
20
+ */
21
+ export declare function projectSessionListRow(r: SessionListItem): SessionListItem & {
22
+ tempo?: ReturnType<typeof sessionTempoOfRunStatus>;
23
+ };
13
24
  export declare const SESSIONS_LIST_ROUTES: readonly [{
14
25
  readonly id: "sessions-list";
15
26
  readonly path: "/v1/sessions";
@@ -125,12 +125,12 @@ async function handleSessionList(res, lister, query, forceOwner, includeUnowned
125
125
  const page = hasMore ? rows.slice(0, limit) : rows;
126
126
  const last = page[page.length - 1];
127
127
  const nextCursor = hasMore && last ? encodeSessionCursor({ lastActivityAt: last.lastActivityAt, sessionId: last.sessionId }, f) : undefined;
128
- const sessions = page.map((r) => {
129
- const tempo = sessionTempoOfRunStatus(r.lastStatus);
130
- const row = r.objectivePreview === "" ? { ...r, objectivePreview: null } : r;
131
- return tempo === undefined ? row : { ...row, tempo };
132
- });
133
- sendJson(res, 200, { sessions, ...(nextCursor ? { nextCursor } : {}) });
128
+ sendJson(res, 200, { sessions: page.map(projectSessionListRow), ...(nextCursor ? { nextCursor } : {}) });
129
+ }
130
+ export function projectSessionListRow(r) {
131
+ const tempo = sessionTempoOfRunStatus(r.lastStatus);
132
+ const row = r.objectivePreview === "" ? { ...r, objectivePreview: null } : r;
133
+ return tempo === undefined ? row : { ...row, tempo };
134
134
  }
135
135
  export const SESSIONS_LIST_ROUTES = [
136
136
  { id: "sessions-list", path: "/v1/sessions", methods: ["GET"] },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * [ref] A9 域模块 —— 单会话面。
3
3
  *
4
- * 路由:`/v1/sessions/:id` 的 init / settings / mcp / head / events(SSE)/ fork / DELETE / policy / workspace,
4
+ * 路由:`/v1/sessions/:id` 的 init / settings / mcp / head / events(SSE)/ fork / PATCH(会话行可写位)/ DELETE / policy / workspace,
5
5
  * 外加同段的 `deps.sessionAudit` 只读审计面。
6
6
  *
7
7
  * 单域独占状态(lens1 §C2 的 12 条里的 5 条)全部收进 `SessionsLocal`,每个 server 实例一份:
@@ -115,6 +115,11 @@ export declare const SESSIONS_ROUTES: readonly [{
115
115
  readonly pattern: RegExp;
116
116
  readonly label: "/v1/sessions/:id";
117
117
  readonly methods: readonly ["GET"];
118
+ }, {
119
+ readonly id: "session-patch";
120
+ readonly pattern: RegExp;
121
+ readonly label: "/v1/sessions/:id";
122
+ readonly methods: readonly ["PATCH"];
118
123
  }, {
119
124
  readonly id: "session-delete";
120
125
  readonly pattern: RegExp;
@@ -1,6 +1,6 @@
1
1
  import { SessionError, materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
2
2
  import { a2aForScenario, mcpForScenario } from "../../web-admin-client/facade.js";
3
- import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
3
+ import { isUuidShape, legacyUnownedSessionOk } from "../../security.js";
4
4
  import { windowMessages, truncateMessageBlobs, redactSessionViewForRead } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
@@ -13,6 +13,8 @@ import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefu
13
13
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
14
14
  import * as facts from "../../capability-facts.js";
15
15
  import { rosterAgentId } from "./agents-roster.js";
16
+ import { projectSessionListRow } from "./sessions-list.js";
17
+ import { readSessionRowPatch } from "../../session-row.js";
16
18
  const MCP_STATUS_TTL_MS = 5_000;
17
19
  const A2A_STATUS_TTL_MS = 5_000;
18
20
  const A2A_MATERIALIZE_TIMEOUT_MS = 10_000;
@@ -69,7 +71,7 @@ async function readLastLegMcp(deps, sessionId, principal) {
69
71
  export async function handleSessions(req, res, match, ctx) {
70
72
  const { deps } = ctx;
71
73
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
72
- const readOwnedSessionView = async (sessionAudit, sessionId) => {
74
+ const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
73
75
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
74
76
  if (ownerOf !== undefined) {
75
77
  const owner = await ownerOf(sessionId);
@@ -82,7 +84,7 @@ export async function handleSessions(req, res, match, ctx) {
82
84
  }
83
85
  let audit;
84
86
  try {
85
- audit = await sessionAudit(sessionId);
87
+ audit = await sessionAudit(sessionId, readOpts);
86
88
  }
87
89
  catch (e) {
88
90
  if (ownerOf !== undefined)
@@ -596,7 +598,7 @@ export async function handleSessions(req, res, match, ctx) {
596
598
  sendError(res, 501, "capability.session_store_required", "the session audit view requires a durable session store (DB_BACKEND=mysql|pg|local)");
597
599
  return;
598
600
  }
599
- const owned = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
601
+ const owned = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId, { reader: gatedPrincipal(req, deps.config) ?? null });
600
602
  if (owned === undefined)
601
603
  return;
602
604
  if (!Array.isArray(owned.messages)) {
@@ -651,6 +653,34 @@ export async function handleSessions(req, res, match, ctx) {
651
653
  return;
652
654
  }
653
655
  }
656
+ if (match.decl.id === "session-patch") {
657
+ const rowFact = facts.sessionPatch(deps);
658
+ if (!rowFact.available) {
659
+ sendError(res, 501, "capability.session_store_required", "renaming or pinning a session requires a durable session store (DB_BACKEND=mysql|pg|local)");
660
+ return;
661
+ }
662
+ const store = rowFact.sessionStorage;
663
+ const sessionId = match.params.sessionId;
664
+ const face = await store.sessionFaceOf(sessionId);
665
+ if (face === undefined) {
666
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
667
+ return;
668
+ }
669
+ if (!runOwnerOk(req, res, face.owner, "session"))
670
+ return;
671
+ const read = readSessionRowPatch(await readJson(req));
672
+ if ("problem" in read) {
673
+ sendError(res, 400, read.problem.code, read.problem.message);
674
+ return;
675
+ }
676
+ const row = await store.patchSessionMeta(sessionId, read.patch, face);
677
+ if (row === undefined) {
678
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
679
+ return;
680
+ }
681
+ sendJson(res, 200, projectSessionListRow(row));
682
+ return;
683
+ }
654
684
  {
655
685
  if (match.decl.id === "session-fork") {
656
686
  const forkFact = facts.sessionFork(deps);
@@ -666,8 +696,8 @@ export async function handleSessions(req, res, match, ctx) {
666
696
  return;
667
697
  }
668
698
  const sourceId = safeDecode(match.params.sessionId);
669
- if (sourceId === null || !isUuidV7(sourceId)) {
670
- sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
699
+ if (sourceId === null || !isUuidShape(sourceId)) {
700
+ sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
671
701
  return;
672
702
  }
673
703
  const owner = await ownerOf(sourceId);
@@ -704,8 +734,8 @@ export async function handleSessions(req, res, match, ctx) {
704
734
  return;
705
735
  }
706
736
  const sessionId = safeDecode(match.params.sessionId);
707
- if (sessionId === null || !isUuidV7(sessionId)) {
708
- sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
737
+ if (sessionId === null || !isUuidShape(sessionId)) {
738
+ sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
709
739
  return;
710
740
  }
711
741
  const owner = await ownerOf(sessionId);
@@ -750,8 +780,8 @@ export async function handleSessions(req, res, match, ctx) {
750
780
  return;
751
781
  }
752
782
  const sessionId = safeDecode(match.params.sessionId);
753
- if (sessionId === null || !isUuidV7(sessionId)) {
754
- sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
783
+ if (sessionId === null || !isUuidShape(sessionId)) {
784
+ sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
755
785
  return;
756
786
  }
757
787
  const owner = await ownerOf(sessionId);
@@ -849,6 +879,7 @@ export const SESSIONS_ROUTES = [
849
879
  { id: "session-memory-status", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/memory-status$/, label: "/v1/sessions/:id/memory-status", methods: ["GET"] },
850
880
  { id: "session-workspace", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/workspace(?:\/(?<key>[^/]+)(?:\/(?<sub>tree|file|archive))?)?$/, label: "/v1/sessions/:id/workspace", methods: ["GET"] },
851
881
  { id: "session-detail", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["GET"] },
882
+ { id: "session-patch", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["PATCH"] },
852
883
  { id: "session-delete", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["DELETE"] },
853
884
  ];
854
885
  //# sourceMappingURL=sessions.js.map