@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.
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.d.ts.map +1 -0
- package/dist/capabilities/send-user-file-tool.js +92 -0
- package/dist/capabilities/send-user-file-tool.js.map +1 -0
- package/dist/config.d.ts +15 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -1
- package/dist/config.js.map +1 -1
- package/dist/http/server.d.ts +1 -0
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +246 -11
- package/dist/http/server.js.map +1 -1
- package/dist/key-resolver.d.ts +2 -1
- package/dist/key-resolver.d.ts.map +1 -1
- package/dist/key-resolver.js +3 -0
- package/dist/key-resolver.js.map +1 -1
- package/dist/main.js +39 -2
- package/dist/main.js.map +1 -1
- package/dist/memory-sync-client.d.ts +3 -0
- package/dist/memory-sync-client.d.ts.map +1 -1
- package/dist/memory-sync-client.js +35 -9
- package/dist/memory-sync-client.js.map +1 -1
- package/dist/memory-sync.d.ts +4 -0
- package/dist/memory-sync.d.ts.map +1 -1
- package/dist/memory-sync.js +26 -2
- package/dist/memory-sync.js.map +1 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +7 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts.map +1 -1
- package/dist/orchestration/workflow-completion-inbox.js +19 -3
- package/dist/orchestration/workflow-completion-inbox.js.map +1 -1
- package/dist/plugins/pg-workflow-run-store.d.ts.map +1 -1
- package/dist/plugins/pg-workflow-run-store.js +5 -4
- package/dist/plugins/pg-workflow-run-store.js.map +1 -1
- package/dist/plugins/send-user-file.d.ts +31 -0
- package/dist/plugins/send-user-file.d.ts.map +1 -0
- package/dist/plugins/send-user-file.js +74 -0
- package/dist/plugins/send-user-file.js.map +1 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts.map +1 -1
- package/dist/plugins/tidb-workflow-run-store.js +5 -4
- package/dist/plugins/tidb-workflow-run-store.js.map +1 -1
- package/dist/runs.d.ts +2 -1
- package/dist/runs.d.ts.map +1 -1
- package/dist/runs.js +5 -2
- package/dist/runs.js.map +1 -1
- package/dist/sealed-key.d.ts +16 -0
- package/dist/sealed-key.d.ts.map +1 -1
- package/dist/sealed-key.js +20 -0
- package/dist/sealed-key.js.map +1 -1
- package/dist/sema-registry.d.ts +1 -1
- package/dist/sema-registry.d.ts.map +1 -1
- package/dist/sema-registry.js +10 -5
- package/dist/sema-registry.js.map +1 -1
- package/package.json +6 -2
package/dist/http/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3194
|
-
error: "
|
|
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
|
-
|
|
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;
|