@sema-agent/server 1.185.0 → 1.187.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 (53) hide show
  1. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  2. package/dist/capabilities/send-user-file-tool.d.ts.map +1 -0
  3. package/dist/capabilities/send-user-file-tool.js +92 -0
  4. package/dist/capabilities/send-user-file-tool.js.map +1 -0
  5. package/dist/config.d.ts +15 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +16 -1
  8. package/dist/config.js.map +1 -1
  9. package/dist/http/server.d.ts +1 -0
  10. package/dist/http/server.d.ts.map +1 -1
  11. package/dist/http/server.js +246 -11
  12. package/dist/http/server.js.map +1 -1
  13. package/dist/key-resolver.d.ts +2 -1
  14. package/dist/key-resolver.d.ts.map +1 -1
  15. package/dist/key-resolver.js +3 -0
  16. package/dist/key-resolver.js.map +1 -1
  17. package/dist/main.js +39 -2
  18. package/dist/main.js.map +1 -1
  19. package/dist/memory-sync-client.d.ts +3 -0
  20. package/dist/memory-sync-client.d.ts.map +1 -1
  21. package/dist/memory-sync-client.js +35 -9
  22. package/dist/memory-sync-client.js.map +1 -1
  23. package/dist/memory-sync.d.ts +4 -0
  24. package/dist/memory-sync.d.ts.map +1 -1
  25. package/dist/memory-sync.js +26 -2
  26. package/dist/memory-sync.js.map +1 -1
  27. package/dist/orchestration/workflow-completion-inbox.d.ts +7 -0
  28. package/dist/orchestration/workflow-completion-inbox.d.ts.map +1 -1
  29. package/dist/orchestration/workflow-completion-inbox.js +19 -3
  30. package/dist/orchestration/workflow-completion-inbox.js.map +1 -1
  31. package/dist/plugins/pg-workflow-run-store.d.ts.map +1 -1
  32. package/dist/plugins/pg-workflow-run-store.js +5 -4
  33. package/dist/plugins/pg-workflow-run-store.js.map +1 -1
  34. package/dist/plugins/send-user-file.d.ts +31 -0
  35. package/dist/plugins/send-user-file.d.ts.map +1 -0
  36. package/dist/plugins/send-user-file.js +74 -0
  37. package/dist/plugins/send-user-file.js.map +1 -0
  38. package/dist/plugins/tidb-workflow-run-store.d.ts.map +1 -1
  39. package/dist/plugins/tidb-workflow-run-store.js +5 -4
  40. package/dist/plugins/tidb-workflow-run-store.js.map +1 -1
  41. package/dist/runs.d.ts +2 -1
  42. package/dist/runs.d.ts.map +1 -1
  43. package/dist/runs.js +5 -2
  44. package/dist/runs.js.map +1 -1
  45. package/dist/sealed-key.d.ts +16 -0
  46. package/dist/sealed-key.d.ts.map +1 -1
  47. package/dist/sealed-key.js +20 -0
  48. package/dist/sealed-key.js.map +1 -1
  49. package/dist/sema-registry.d.ts +1 -1
  50. package/dist/sema-registry.d.ts.map +1 -1
  51. package/dist/sema-registry.js +10 -5
  52. package/dist/sema-registry.js.map +1 -1
  53. package/package.json +6 -2
@@ -1,7 +1,7 @@
1
1
  import http from "node:http";
2
2
  import { once } from "node:events";
3
3
  import { timingSafeEqual, createHash, randomBytes } from "node:crypto";
4
- import { uuidv7, isThinkingLevel, DEFAULT_EFFORT_LEVELS, materializeMcpTools, runWithVerification, resumeWithVerification, runCascade, CheckpointError, SessionPolicyError, SessionError, HAND_TOOL_EFFECTS, canonicalToolName, formatUserScope, validatePendingSteer, StreamingImportValidator, listWorkflowRuns, getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus } from "@sema-agent/core";
4
+ import { uuidv7, isThinkingLevel, DEFAULT_EFFORT_LEVELS, materializeMcpTools, runWithVerification, resumeWithVerification, runCascade, CheckpointError, mintCheckpointToken, SessionPolicyError, SessionError, HAND_TOOL_EFFECTS, canonicalToolName, formatUserScope, validatePendingSteer, StreamingImportValidator, listWorkflowRuns, getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus } from "@sema-agent/core";
5
5
  import { mcpForScenario } from "../sema-registry.js";
6
6
  import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isUuidShape, verifyDirectDoorProof } from "../security.js";
7
7
  import { exportSession, exportSessionManifest, importSession, classifySyncRelationshipByIds, SyncConflictError, stagingIdFor } from "../session-sync.js";
@@ -17,7 +17,7 @@ import { manifestToIndexEntry } from "../images/manifest.js";
17
17
  import { runInBackground, evictIfConflict, stripCheckpointToken, resumeAtHttpStatus, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
18
18
  import { looksLikeJwt } from "../auth-bridge.js";
19
19
  import { redactSteerIn } from "../orchestration/workflow-agent-steer.js";
20
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry } from "../orchestration/workflow-completion-inbox.js";
20
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationFoldKey } from "../orchestration/workflow-completion-inbox.js";
21
21
  import { fleetRunPublisher, fleetRunLabels } from "../fleet/fleet-bus.js";
22
22
  import { RateLimiter } from "../observability/rate-limit.js";
23
23
  import { withPrincipal } from "../observability/principal-context.js";
@@ -177,6 +177,7 @@ export function createHttpServer(deps) {
177
177
  const inflightRuns = new Map();
178
178
  const preemptableRuns = new Map();
179
179
  const steerableRuns = new Map();
180
+ const wakeParkMints = new Map();
180
181
  let uncountedBillableInflight = 0;
181
182
  if (deps.drainState)
182
183
  deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + uncountedBillableInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
@@ -590,6 +591,8 @@ export function createHttpServer(deps) {
590
591
  rewindFilesTo: Boolean(deps.fileSnapshotStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
591
592
  scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
592
593
  sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.fileSnapshotStore),
594
+ sendUserFile: Boolean(deps.config.sendUserFile),
595
+ s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
593
596
  workflows: deps.workflowsCapable ?? Boolean(deps.workflowRunStore),
594
597
  workflowsList: Boolean(deps.workflowRunStore),
595
598
  manualCompact: Boolean(deps.runStore),
@@ -847,9 +850,12 @@ export function createHttpServer(deps) {
847
850
  const withElicit = deps.elicitation
848
851
  ? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
849
852
  : streamBody;
850
- await (deps.question
853
+ const withQuestion = () => deps.question
851
854
  ? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, withElicit)
852
- : withElicit());
855
+ : withElicit();
856
+ await (deps.sendUserFile
857
+ ? deps.sendUserFile.runWithContext({ taskId: askTaskId, emit: async (f) => void (await emitAsk(f)) }, withQuestion)
858
+ : withQuestion());
853
859
  if (durableTaskId && deps.runStore) {
854
860
  if (finalResult?.status === "suspended" && deps.checkpointStore)
855
861
  await deps.runStore.setSuspended(durableTaskId);
@@ -1030,7 +1036,7 @@ export function createHttpServer(deps) {
1030
1036
  rootTaskId: taskId,
1031
1037
  ...fleetRunLabels(prepared.spec.objective),
1032
1038
  });
1033
- void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify?.maxRounds, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x));
1039
+ void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify?.maxRounds, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile);
1034
1040
  return { status: 202, body: { taskId, sessionId, status: "running" } };
1035
1041
  }, (r) => r.status === 202);
1036
1042
  sendJson(res, resp.status, resp.body);
@@ -1240,7 +1246,66 @@ export function createHttpServer(deps) {
1240
1246
  sendNotRunning("run is no longer suspended (resolved or expired)");
1241
1247
  return;
1242
1248
  }
1243
- sendNotRunning(run.status === "running" ? "run is active on another replica — cross-replica live-steer is not yet supported" : `run is ${run.status} — not accepting steers`);
1249
+ if (run.status === "running") {
1250
+ sendNotRunning("run is active on another replica — cross-replica live-steer is not yet supported");
1251
+ return;
1252
+ }
1253
+ if (deps.checkpointStore && deps.sessionStorage?.getLeafId) {
1254
+ const activeTaskId = await deps.runStore.getActiveTaskId?.(run.sessionId).catch(() => undefined);
1255
+ if (activeTaskId !== undefined && activeTaskId !== null && activeTaskId !== taskId) {
1256
+ sendNotRunning(`run is ${run.status} and its session has an ACTIVE run (${activeTaskId}) — steer that run instead`);
1257
+ return;
1258
+ }
1259
+ const prior = wakeParkMints.get(run.sessionId) ?? Promise.resolve();
1260
+ const mintResult = { parked: false };
1261
+ const job = prior.then(async () => {
1262
+ const cs = deps.checkpointStore;
1263
+ if (await tryPark() === "parked") {
1264
+ mintResult.parked = true;
1265
+ return;
1266
+ }
1267
+ const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
1268
+ if (leafId === undefined || leafId === null)
1269
+ return;
1270
+ const cpScope = run.owner ?? "_";
1271
+ const wakeToken = mintCheckpointToken();
1272
+ await cs.put(wakeToken, {
1273
+ token: wakeToken,
1274
+ scope: cpScope,
1275
+ sessionId: run.sessionId,
1276
+ leafId,
1277
+ gate: { kind: "task_done" },
1278
+ pendingAction: { kind: "task_done" },
1279
+ state: { activeTools: [], nestedStats: { tokens: 0, turns: 0, tasks: 0, costUsd: 0, costMicroUsd: 0 } },
1280
+ status: "pending",
1281
+ createdAt: Date.now(),
1282
+ sourceTaskId: taskId,
1283
+ });
1284
+ const winner = await cs.findPendingTokenBySession(run.sessionId).catch(() => null);
1285
+ if (winner !== null && winner !== wakeToken) {
1286
+ await cs.expire(wakeToken, cpScope).catch(() => undefined);
1287
+ if (await cs.setPendingSteer(winner, cpScope, { text, trusted }))
1288
+ mintResult.parked = true;
1289
+ return;
1290
+ }
1291
+ if (await cs.setPendingSteer(wakeToken, cpScope, { text, trusted }))
1292
+ mintResult.parked = true;
1293
+ });
1294
+ const wrapped = job.catch(() => undefined);
1295
+ wakeParkMints.set(run.sessionId, wrapped);
1296
+ try {
1297
+ await job;
1298
+ }
1299
+ finally {
1300
+ if (wakeParkMints.get(run.sessionId) === wrapped)
1301
+ wakeParkMints.delete(run.sessionId);
1302
+ }
1303
+ if (mintResult.parked) {
1304
+ sendJson(res, 202, { taskId, status: run.status, delivery: "parked_for_wake", messageId, ...(priority ? { priority } : {}), note: "run already ended — steer parked on a task_done checkpoint; deliver it with POST /v1/sessions/:id/wake" });
1305
+ return;
1306
+ }
1307
+ }
1308
+ sendNotRunning(`run is ${run.status} — not accepting steers`);
1244
1309
  return;
1245
1310
  }
1246
1311
  const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
@@ -3189,10 +3254,121 @@ export function createHttpServer(deps) {
3189
3254
  return;
3190
3255
  }
3191
3256
  }
3257
+ if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/notify$/.test(url)) {
3258
+ if (rateLimited(req, res) || quotaExceeded(req, res))
3259
+ return;
3260
+ const principal = gatedPrincipal(req, deps.config);
3261
+ if (deps.config.requirePrincipal && principal === undefined) {
3262
+ sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3263
+ return;
3264
+ }
3265
+ const notifySession = decodeURIComponent(url.split("/")[3]);
3266
+ let nb;
3267
+ try {
3268
+ nb = (await readJson(req));
3269
+ }
3270
+ catch {
3271
+ sendJson(res, 400, { error: "body must be JSON: { task_id, status, summary, result?, seq?, source? }" });
3272
+ return;
3273
+ }
3274
+ if (typeof nb.task_id !== "string" || nb.task_id.trim().length === 0 || nb.task_id.length > 190) {
3275
+ sendJson(res, 400, { error: "task_id must be a non-empty string of at most 190 characters" });
3276
+ return;
3277
+ }
3278
+ const NOTIFY_STATUSES = ["completed", "failed", "killed", "cancelled", "event"];
3279
+ if (typeof nb.status !== "string" || !NOTIFY_STATUSES.includes(nb.status)) {
3280
+ sendJson(res, 400, { error: `status must be one of ${NOTIFY_STATUSES.join("/")}` });
3281
+ return;
3282
+ }
3283
+ if (typeof nb.summary !== "string" || nb.summary.trim().length === 0) {
3284
+ sendJson(res, 400, { error: "summary must be a non-empty string" });
3285
+ return;
3286
+ }
3287
+ if (nb.result !== undefined && typeof nb.result !== "string") {
3288
+ sendJson(res, 400, { error: "result must be a string when present" });
3289
+ return;
3290
+ }
3291
+ if (nb.seq !== undefined && (typeof nb.seq !== "number" || !Number.isInteger(nb.seq) || nb.seq < 1)) {
3292
+ sendJson(res, 400, { error: "seq must be a positive integer when present" });
3293
+ return;
3294
+ }
3295
+ if (nb.source !== undefined && (typeof nb.source !== "string" || nb.source.length > 190)) {
3296
+ sendJson(res, 400, { error: "source must be a string of at most 190 characters when present" });
3297
+ return;
3298
+ }
3299
+ if (!deps.sessionStorage?.ownerOf) {
3300
+ sendJson(res, 501, { error: "external notify requires the session-ownership face (sessionStorage.ownerOf)" });
3301
+ return;
3302
+ }
3303
+ const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
3304
+ if (notifyOwner === undefined) {
3305
+ sendJson(res, 404, { error: "session not found" });
3306
+ return;
3307
+ }
3308
+ const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
3309
+ if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
3310
+ sendJson(res, 404, { error: "session not found" });
3311
+ return;
3312
+ }
3313
+ const payload = {
3314
+ task_id: nb.task_id,
3315
+ status: nb.status,
3316
+ summary: nb.summary,
3317
+ ...(nb.result !== undefined ? { result: nb.result } : {}),
3318
+ ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
3319
+ ...(nb.source !== undefined ? { source: nb.source } : {}),
3320
+ };
3321
+ const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
3322
+ const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
3323
+ if (liveStream) {
3324
+ try {
3325
+ await liveStream.notify(payload);
3326
+ sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
3327
+ return;
3328
+ }
3329
+ catch (e) {
3330
+ const code = e.code;
3331
+ if (typeof code === "string" && code.startsWith("notify.")) {
3332
+ sendJson(res, 400, { errorCode: code, error: e instanceof Error ? e.message : "invalid notification" });
3333
+ return;
3334
+ }
3335
+ }
3336
+ }
3337
+ if (!deps.workflowCompletionInbox) {
3338
+ sendJson(res, 501, { error: "external notify park requires the workflow completion inbox (WORKFLOW_RUN_STORE)" });
3339
+ return;
3340
+ }
3341
+ await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
3342
+ sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
3343
+ return;
3344
+ }
3192
3345
  if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/wake$/.test(url)) {
3193
- sendJson(res, 501, {
3194
- error: "explicit wake is unnecessary — re-issue a task/run with the same sessionId; the durable session resumes (and reconciles) automatically",
3195
- });
3346
+ if (!deps.checkpointStore) {
3347
+ sendJson(res, 501, { error: "wake requires the checkpoint store" });
3348
+ return;
3349
+ }
3350
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
3351
+ return;
3352
+ const principal = gatedPrincipal(req, deps.config);
3353
+ if (deps.config.requirePrincipal && principal === undefined) {
3354
+ sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3355
+ return;
3356
+ }
3357
+ const wakeSession = decodeURIComponent(url.split("/")[3]);
3358
+ let wakeBody;
3359
+ try {
3360
+ wakeBody = (await readJson(req));
3361
+ }
3362
+ catch {
3363
+ sendJson(res, 400, { error: "body must be JSON: { message?: string }" });
3364
+ return;
3365
+ }
3366
+ if (wakeBody.message !== undefined && (typeof wakeBody.message !== "string" || wakeBody.message.length === 0)) {
3367
+ sendJson(res, 400, { error: "message must be a non-empty string when present" });
3368
+ return;
3369
+ }
3370
+ const out = await resumeWake(wakeSession, wakeBody.message, principal, req);
3371
+ sendJson(res, out.status, out.body);
3196
3372
  return;
3197
3373
  }
3198
3374
  sendJson(res, 404, { error: "not found" });
@@ -3799,7 +3975,8 @@ export function createHttpServer(deps) {
3799
3975
  if (e instanceof CheckpointError) {
3800
3976
  const retriable = e.code === "checkpoint.invalid_outcome" ||
3801
3977
  e.code === "checkpoint.reopen_revote" ||
3802
- e.code === "checkpoint.reopened_concurrently";
3978
+ e.code === "checkpoint.reopened_concurrently" ||
3979
+ e.code.startsWith("wake.");
3803
3980
  if (claimedRow && taskId && deps.runStore) {
3804
3981
  if (retriable) {
3805
3982
  await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
@@ -3894,6 +4071,63 @@ export function createHttpServer(deps) {
3894
4071
  const verifyRounds = ctx.body.verify === true ? clampVerifyRounds(ctx.body.verifyRounds) : undefined;
3895
4072
  return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds });
3896
4073
  }
4074
+ async function resumeWake(sessionId, message, caller, req) {
4075
+ const cs = deps.checkpointStore;
4076
+ const token = await cs.findPendingTokenBySession(sessionId);
4077
+ if (!token)
4078
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)" } };
4079
+ const operator = explicitOperatorOk(caller, deps.config.operatorPrincipals);
4080
+ if (!operator) {
4081
+ const scope = await cs.peekPendingScope(sessionId).catch(() => undefined);
4082
+ if (scope !== null && scope !== undefined && scope !== "_" && scope !== caller) {
4083
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)" } };
4084
+ }
4085
+ }
4086
+ let cp;
4087
+ try {
4088
+ cp = await cs.get(token);
4089
+ }
4090
+ catch (e) {
4091
+ return { status: 409, body: { error: e instanceof Error ? e.message : String(e) } };
4092
+ }
4093
+ if (!cp)
4094
+ return { status: 404, body: { error: "checkpoint not found" } };
4095
+ if (!operator && cp.scope !== "_" && cp.scope !== caller) {
4096
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)" } };
4097
+ }
4098
+ const gateKind = cp.gate?.kind;
4099
+ if (gateKind !== "task_done") {
4100
+ return { status: 409, body: { error: `cannot wake: this checkpoint awaits a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
4101
+ }
4102
+ const trusted = operator;
4103
+ if (message !== undefined) {
4104
+ try {
4105
+ validatePendingSteer({ text: message, trusted });
4106
+ }
4107
+ catch (e) {
4108
+ if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
4109
+ return { status: 422, body: { errorCode: "steering.invalid_content", error: e.message } };
4110
+ }
4111
+ throw e;
4112
+ }
4113
+ }
4114
+ const ctx = await cs.getCtx(sessionId).catch(() => null);
4115
+ if (!ctx)
4116
+ return { status: 409, body: { error: "resume context missing — cannot rebuild task config" } };
4117
+ const auth = { sessionId: cp.sessionId, principal: cp.scope === "_" ? undefined : cp.scope, memoryScope: ctx.memoryScope };
4118
+ const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
4119
+ const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
4120
+ return driveResumeIntoRunLog({
4121
+ token,
4122
+ sessionId: cp.sessionId,
4123
+ principal: auth.principal,
4124
+ fleetScope: resumeFleetScope(req, auth),
4125
+ taskConfig,
4126
+ resumeObjective,
4127
+ outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
4128
+ verifyRounds: undefined,
4129
+ });
4130
+ }
3897
4131
  async function resumePlanReview(sessionId, decision, editedPlan, reason, req) {
3898
4132
  const cs = deps.checkpointStore;
3899
4133
  const token = await cs.findPendingTokenBySession(sessionId);
@@ -4717,7 +4951,8 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
4717
4951
  const { ownerScope: _os, ownerSessionId: _osid, ...wire } = n;
4718
4952
  send("bg_notification", { type: "bg_notification", ...wire, ts: frame.ts });
4719
4953
  if (completionInbox && n.ownerSessionId && callerSession !== null && callerSession === n.ownerSessionId && !res.writableEnded) {
4720
- void completionInbox.markTerminalServed(n.ownerSessionId, n.taskId).catch(() => undefined);
4954
+ const family = taskNotificationFoldKey({ task_id: n.taskId, status: n.status });
4955
+ void completionInbox.markTerminalServed(n.ownerSessionId, family).catch(() => undefined);
4721
4956
  }
4722
4957
  }
4723
4958
  break;