agent-conveyor 0.1.21 → 0.1.23
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/README.md +41 -2
- package/dist/cli/typescript-runtime.js +687 -9
- package/dist/cli/typescript-runtime.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/app-autonomy.js +4 -3
- package/dist/runtime/app-autonomy.js.map +1 -1
- package/dist/runtime/audit.d.ts +14 -1
- package/dist/runtime/audit.js +24 -0
- package/dist/runtime/audit.js.map +1 -1
- package/dist/runtime/notifications.d.ts +32 -0
- package/dist/runtime/notifications.js +94 -1
- package/dist/runtime/notifications.js.map +1 -1
- package/dist/runtime/replay.js +24 -0
- package/dist/runtime/replay.js.map +1 -1
- package/dist/state/schema-v23.js +18 -0
- package/dist/state/schema-v23.js.map +1 -1
- package/dist/state/sqlite-contract.d.ts +1 -1
- package/dist/state/sqlite-contract.js +4 -1
- package/dist/state/sqlite-contract.js.map +1 -1
- package/package.json +1 -1
- package/plugin/agent-conveyor/plugin.json +3 -1
- package/plugin/agent-conveyor/skills/conveyor-app-wake-relay/SKILL.md +122 -0
- package/plugin/agent-conveyor/skills/conveyor-check-status/SKILL.md +2 -0
- package/plugin/agent-conveyor/skills/conveyor-create-pair/SKILL.md +9 -1
- package/plugin/agent-conveyor/skills/conveyor-create-worker-set/SKILL.md +12 -2
- package/plugin/agent-conveyor/skills/conveyor-smoke-app-connections/SKILL.md +220 -0
- package/plugin/agent-conveyor/skills/conveyor-whats-next-nudger/SKILL.md +4 -0
|
@@ -18,7 +18,7 @@ import { executeDispatchCommandSync } from "../runtime/dispatch.js";
|
|
|
18
18
|
import { deregisterSessionSync, discoverRegistrySync, findRolloutPathForPid, listRegisteredSessionsSync, registerSessionSync, readSessionMeta, sessionRow, } from "../runtime/codex-session.js";
|
|
19
19
|
import { managerConfigPermissionAllowed, managerConfigSync } from "../runtime/manager-config.js";
|
|
20
20
|
import { canonicalManagerPermissionNames, flattenManagerPermissions, normalizeManagerPermissions, } from "../runtime/manager-permissions.js";
|
|
21
|
-
import { deferRoutedNotificationBeforeSideEffectSync, deliveryModeForTargetSessionSync, finishRoutedNotificationSync, insertRoutedNotificationSync, markRoutedNotificationSideEffectStartedSync, consumeNextSessionInboxItemSync, routedNotificationsSync, sessionInboxSync, } from "../runtime/notifications.js";
|
|
21
|
+
import { deferRoutedNotificationBeforeSideEffectSync, deliveryModeForTargetSessionSync, finishRoutedNotificationSync, insertRoutedNotificationSync, insertNotificationAcknowledgementSync, latestNotificationAcknowledgementSync, markRoutedNotificationSideEffectStartedSync, notificationAcknowledgementsSync, consumeNextSessionInboxItemSync, routedNotificationsSync, sessionInboxSync, } from "../runtime/notifications.js";
|
|
22
22
|
import { activeBindingForTaskSync, bindSessionsSync, createTaskSync, listTasksSync, unbindTaskSync, } from "../runtime/tasks.js";
|
|
23
23
|
import { captureTmuxTargetWithRunner, captureTranscriptTmuxTargetWithRunner, currentPaneIdWithRunner, killTmuxSessionWithRunner, sendEnterToTmuxSessionWithRunner, sendTextToSessionWithRunner, sessionExists, startTmuxSessionWithRunner, tmuxCommandFailureMessage, tmuxSession, tmuxSessionRunning, } from "../runtime/tmux.js";
|
|
24
24
|
import { captureMetaPath, configPath, defaultDbPath, eventsPath, loadJsonSync, stateRoot, statusPath, transcriptPath, validateWorkerName, workerDir, writeJsonSync, } from "../state/files.js";
|
|
@@ -144,6 +144,9 @@ export function runTypescriptRuntimeCommand(options) {
|
|
|
144
144
|
if (parsed.command === "app-autopilot") {
|
|
145
145
|
return runAppAutopilotCommand(parsed, options);
|
|
146
146
|
}
|
|
147
|
+
if (parsed.command === "app-smoke") {
|
|
148
|
+
return runAppSmokeCommand(parsed, options);
|
|
149
|
+
}
|
|
147
150
|
if (parsed.command === "qa-plan") {
|
|
148
151
|
return runQaPlanCommand(parsed);
|
|
149
152
|
}
|
|
@@ -261,6 +264,9 @@ export function runTypescriptRuntimeCommand(options) {
|
|
|
261
264
|
if (parsed.command === "manager-ack") {
|
|
262
265
|
return runTaskAckCommand(parsed, options, "manager");
|
|
263
266
|
}
|
|
267
|
+
if (parsed.command === "inbox-ack") {
|
|
268
|
+
return runInboxAckCommand(parsed, options);
|
|
269
|
+
}
|
|
264
270
|
if (parsed.command === "session-inbox") {
|
|
265
271
|
return runSessionInboxCommand(parsed, options, "session");
|
|
266
272
|
}
|
|
@@ -420,6 +426,15 @@ function commandHelpText(program, command) {
|
|
|
420
426
|
"Example JSON:",
|
|
421
427
|
` {"task":"my-task","manager_session":"mgr","supervision_contract":"I will supervise through Conveyor and verify criteria before finishing.","will_not_edit_project_files":true}`,
|
|
422
428
|
],
|
|
429
|
+
"inbox-ack": [
|
|
430
|
+
`usage: ${program} inbox-ack <task> --notification-id N --role manager|worker --status received|accepted|blocked --from-stdin ${path} [--json]`,
|
|
431
|
+
`usage: ${program} inbox-ack <task> --notification-id N --json ${path}`,
|
|
432
|
+
"",
|
|
433
|
+
"Record or read a notification-scoped manager/worker acknowledgement.",
|
|
434
|
+
"",
|
|
435
|
+
"Example JSON:",
|
|
436
|
+
` {"summary":"Received manager instruction and will run the bounded task.","evidence":[]}`,
|
|
437
|
+
],
|
|
423
438
|
nudge: [
|
|
424
439
|
`usage: ${program} nudge <worker-or-session> <message> ${path} [--dry-run]`,
|
|
425
440
|
`usage: ${program} session-nudge <session> <message> ${path} [--dry-run]`,
|
|
@@ -439,6 +454,18 @@ function commandHelpText(program, command) {
|
|
|
439
454
|
` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
|
|
440
455
|
` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
|
|
441
456
|
],
|
|
457
|
+
"app-smoke": [
|
|
458
|
+
`usage: ${program} app-smoke preflight|start|record|status <task> [--mode required|advisory|skip] [--scope pair|worker-set] [--smoke-id ID] [--nonce NONCE] [--role manager|worker] [--status sent|skipped|received|accepted|blocked] [--thread-id ID] [--notification-id N] [--worker-count N] ${path} [--from-stdin] [--json]`,
|
|
459
|
+
"",
|
|
460
|
+
"Manage a blocking Codex app connection smoke lifecycle for manager/worker bindings.",
|
|
461
|
+
"The CLI records durable smoke receipts; the Codex app/plugin layer still performs live app-thread sends.",
|
|
462
|
+
"",
|
|
463
|
+
"Examples:",
|
|
464
|
+
` ${program} app-smoke preflight dogfood --mode required --path /tmp/work/workerctl.db --json`,
|
|
465
|
+
` ${program} app-smoke start dogfood --mode required --path /tmp/work/workerctl.db --json`,
|
|
466
|
+
` ${program} app-smoke record dogfood --smoke-id smoke-123 --nonce nonce --role worker --status accepted --thread-id thread-worker --from-stdin --path /tmp/work/workerctl.db --json`,
|
|
467
|
+
` ${program} app-smoke status dogfood --path /tmp/work/workerctl.db --json`,
|
|
468
|
+
],
|
|
442
469
|
"app-worker-rotation-plan": [
|
|
443
470
|
`usage: ${program} app-worker-rotation-plan <task> --old-worker-thread-id ID [--require-handoff] [--reason TEXT] ${path} [--json]`,
|
|
444
471
|
"",
|
|
@@ -584,6 +611,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
584
611
|
nextAction: null,
|
|
585
612
|
noFollowup: false,
|
|
586
613
|
nextSteps: [],
|
|
614
|
+
notificationId: null,
|
|
587
615
|
output: null,
|
|
588
616
|
path: null,
|
|
589
617
|
pid: null,
|
|
@@ -617,6 +645,10 @@ function parseRuntimeArgs(args, env) {
|
|
|
617
645
|
subtype: null,
|
|
618
646
|
summary: null,
|
|
619
647
|
source: null,
|
|
648
|
+
smokeId: null,
|
|
649
|
+
smokeMode: null,
|
|
650
|
+
smokeNonce: null,
|
|
651
|
+
smokeScope: null,
|
|
620
652
|
objective: null,
|
|
621
653
|
promptSummary: null,
|
|
622
654
|
proof: null,
|
|
@@ -722,6 +754,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
722
754
|
telemetryView: null,
|
|
723
755
|
telemetryViewTask: null,
|
|
724
756
|
workerName: null,
|
|
757
|
+
workerCount: null,
|
|
725
758
|
search: null,
|
|
726
759
|
severity: null,
|
|
727
760
|
staleCycleSeconds: 3600.0,
|
|
@@ -1502,7 +1535,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
1502
1535
|
index += 1;
|
|
1503
1536
|
}
|
|
1504
1537
|
else if (arg === "--from-stdin") {
|
|
1505
|
-
if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack") {
|
|
1538
|
+
if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack" && command !== "inbox-ack" && command !== "app-smoke") {
|
|
1506
1539
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --from-stdin", explicit, flags, task };
|
|
1507
1540
|
}
|
|
1508
1541
|
flags.fromStdin = true;
|
|
@@ -1636,7 +1669,8 @@ function parseRuntimeArgs(args, env) {
|
|
|
1636
1669
|
&& command !== "app-loop-status"
|
|
1637
1670
|
&& command !== "app-wakeup-plan"
|
|
1638
1671
|
&& command !== "app-wakeup-dispatch"
|
|
1639
|
-
&& command !== "app-autopilot"
|
|
1672
|
+
&& command !== "app-autopilot"
|
|
1673
|
+
&& command !== "app-smoke") {
|
|
1640
1674
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
|
|
1641
1675
|
}
|
|
1642
1676
|
const value = valueAfter(queue, index, arg);
|
|
@@ -2034,7 +2068,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
2034
2068
|
index += 1;
|
|
2035
2069
|
}
|
|
2036
2070
|
else if (arg === "--thread-id") {
|
|
2037
|
-
if (command !== "app-wakeup-record-delivery" && command !== "campaign") {
|
|
2071
|
+
if (command !== "app-wakeup-record-delivery" && command !== "campaign" && command !== "app-smoke") {
|
|
2038
2072
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --thread-id", explicit, flags, task };
|
|
2039
2073
|
}
|
|
2040
2074
|
const value = valueAfter(queue, index, arg);
|
|
@@ -2312,6 +2346,14 @@ function parseRuntimeArgs(args, env) {
|
|
|
2312
2346
|
index += 1;
|
|
2313
2347
|
continue;
|
|
2314
2348
|
}
|
|
2349
|
+
if (command === "app-smoke") {
|
|
2350
|
+
if (value !== "required" && value !== "advisory" && value !== "skip") {
|
|
2351
|
+
return { command, enabled, error: `Unsupported app-smoke mode: ${value}`, explicit, flags, task };
|
|
2352
|
+
}
|
|
2353
|
+
flags.smokeMode = value;
|
|
2354
|
+
index += 1;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2315
2357
|
if (!isTranscriptCaptureMode(value)) {
|
|
2316
2358
|
return { command, enabled, error: `Unsupported transcript capture mode: ${value}`, explicit, flags, task };
|
|
2317
2359
|
}
|
|
@@ -2373,7 +2415,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
2373
2415
|
flags.epilogueStatus = true;
|
|
2374
2416
|
continue;
|
|
2375
2417
|
}
|
|
2376
|
-
if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign") {
|
|
2418
|
+
if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign" && command !== "inbox-ack" && command !== "app-smoke") {
|
|
2377
2419
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --status", explicit, flags, task };
|
|
2378
2420
|
}
|
|
2379
2421
|
const parsedValue = valueAfter(queue, index, arg);
|
|
@@ -2383,11 +2425,66 @@ function parseRuntimeArgs(args, env) {
|
|
|
2383
2425
|
if (command === "criteria") {
|
|
2384
2426
|
flags.statuses.push(parsedValue.value);
|
|
2385
2427
|
}
|
|
2428
|
+
else if (command === "inbox-ack" || command === "app-smoke") {
|
|
2429
|
+
flags.statusState = parsedValue.value;
|
|
2430
|
+
}
|
|
2386
2431
|
else {
|
|
2387
2432
|
flags.statusState = parsedValue.value;
|
|
2388
2433
|
}
|
|
2389
2434
|
index += 1;
|
|
2390
2435
|
}
|
|
2436
|
+
else if (arg === "--notification-id") {
|
|
2437
|
+
if (command !== "inbox-ack" && command !== "app-smoke") {
|
|
2438
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --notification-id", explicit, flags, task };
|
|
2439
|
+
}
|
|
2440
|
+
const parsedValue = valueAfter(queue, index, arg);
|
|
2441
|
+
if (parsedValue.error) {
|
|
2442
|
+
return { command, enabled, error: parsedValue.error, explicit, flags, task };
|
|
2443
|
+
}
|
|
2444
|
+
const value = Number(parsedValue.value);
|
|
2445
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2446
|
+
return { command, enabled, error: "--notification-id must be a positive integer.", explicit, flags, task };
|
|
2447
|
+
}
|
|
2448
|
+
flags.notificationId = value;
|
|
2449
|
+
index += 1;
|
|
2450
|
+
}
|
|
2451
|
+
else if (arg === "--smoke-id" || arg === "--nonce" || arg === "--scope") {
|
|
2452
|
+
if (command !== "app-smoke") {
|
|
2453
|
+
return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
|
|
2454
|
+
}
|
|
2455
|
+
const parsedValue = valueAfter(queue, index, arg);
|
|
2456
|
+
if (parsedValue.error) {
|
|
2457
|
+
return { command, enabled, error: parsedValue.error, explicit, flags, task };
|
|
2458
|
+
}
|
|
2459
|
+
if (arg === "--smoke-id") {
|
|
2460
|
+
flags.smokeId = parsedValue.value;
|
|
2461
|
+
}
|
|
2462
|
+
else if (arg === "--nonce") {
|
|
2463
|
+
flags.smokeNonce = parsedValue.value;
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
if (parsedValue.value !== "pair" && parsedValue.value !== "worker-set") {
|
|
2467
|
+
return { command, enabled, error: `Unsupported app-smoke scope: ${parsedValue.value}`, explicit, flags, task };
|
|
2468
|
+
}
|
|
2469
|
+
flags.smokeScope = parsedValue.value;
|
|
2470
|
+
}
|
|
2471
|
+
index += 1;
|
|
2472
|
+
}
|
|
2473
|
+
else if (arg === "--worker-count") {
|
|
2474
|
+
if (command !== "app-smoke") {
|
|
2475
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --worker-count", explicit, flags, task };
|
|
2476
|
+
}
|
|
2477
|
+
const parsedValue = valueAfter(queue, index, arg);
|
|
2478
|
+
if (parsedValue.error) {
|
|
2479
|
+
return { command, enabled, error: parsedValue.error, explicit, flags, task };
|
|
2480
|
+
}
|
|
2481
|
+
const value = Number(parsedValue.value);
|
|
2482
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2483
|
+
return { command, enabled, error: "--worker-count must be a positive integer.", explicit, flags, task };
|
|
2484
|
+
}
|
|
2485
|
+
flags.workerCount = value;
|
|
2486
|
+
index += 1;
|
|
2487
|
+
}
|
|
2391
2488
|
else if (arg === "--type") {
|
|
2392
2489
|
const value = valueAfter(queue, index, arg);
|
|
2393
2490
|
if (value.error) {
|
|
@@ -2537,10 +2634,12 @@ function parseRuntimeArgs(args, env) {
|
|
|
2537
2634
|
&& command !== "enqueue-continue-iteration"
|
|
2538
2635
|
&& command !== "worker-ack"
|
|
2539
2636
|
&& command !== "manager-ack"
|
|
2637
|
+
&& command !== "inbox-ack"
|
|
2540
2638
|
&& command !== "loop-evidence"
|
|
2541
2639
|
&& command !== "continuation"
|
|
2542
2640
|
&& command !== "continuation-reviewer"
|
|
2543
|
-
&& command !== "epilogue"
|
|
2641
|
+
&& command !== "epilogue"
|
|
2642
|
+
&& command !== "app-smoke") {
|
|
2544
2643
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --correlation-id", explicit, flags, task };
|
|
2545
2644
|
}
|
|
2546
2645
|
const value = valueAfter(queue, index, arg);
|
|
@@ -3036,6 +3135,12 @@ function parseRuntimeArgs(args, env) {
|
|
|
3036
3135
|
}
|
|
3037
3136
|
flags.action = arg;
|
|
3038
3137
|
}
|
|
3138
|
+
else if (command === "app-smoke" && flags.action === null) {
|
|
3139
|
+
if (!["preflight", "record", "start", "status"].includes(arg)) {
|
|
3140
|
+
return { command, enabled, error: `Unsupported app-smoke action: ${arg}`, explicit, flags, task };
|
|
3141
|
+
}
|
|
3142
|
+
flags.action = arg;
|
|
3143
|
+
}
|
|
3039
3144
|
else if (command === "campaign" && flags.action === null) {
|
|
3040
3145
|
if (!CAMPAIGN_ACTIONS.has(arg)) {
|
|
3041
3146
|
return { command, enabled, error: unsupportedCampaignActionMessage(arg), explicit, flags, task };
|
|
@@ -3863,7 +3968,7 @@ function runAppWakeupRecordDeliveryCommand(parsed, options) {
|
|
|
3863
3968
|
if (!action) {
|
|
3864
3969
|
throw new Error(`Receipt ${source.id} has no ${role} wake action.`);
|
|
3865
3970
|
}
|
|
3866
|
-
validateAppWakeupDelivery({ action, deliveryStatus, role, threadId: parsed.flags.threadId });
|
|
3971
|
+
validateAppWakeupDelivery({ action, deliveryStatus, reason: parsed.flags.reason, role, threadId: parsed.flags.threadId });
|
|
3867
3972
|
const timestamp = nowIsoSeconds(options);
|
|
3868
3973
|
const eventId = emitTelemetrySync(database, {
|
|
3869
3974
|
actor: "manager",
|
|
@@ -4326,6 +4431,407 @@ function runAppAutopilotCommand(parsed, options) {
|
|
|
4326
4431
|
database.close();
|
|
4327
4432
|
}
|
|
4328
4433
|
}
|
|
4434
|
+
function runAppSmokeCommand(parsed, options) {
|
|
4435
|
+
const action = parsed.flags.action;
|
|
4436
|
+
if (action !== "preflight" && action !== "record" && action !== "start" && action !== "status") {
|
|
4437
|
+
return errorResult("app-smoke requires an action: preflight, start, record, or status");
|
|
4438
|
+
}
|
|
4439
|
+
const taskName = requireTask(parsed);
|
|
4440
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
4441
|
+
try {
|
|
4442
|
+
const timestamp = nowIsoSeconds(options);
|
|
4443
|
+
const dbPath = runtimeDbPath(parsed, options);
|
|
4444
|
+
const task = taskRowForDiagnostics(database, taskName);
|
|
4445
|
+
if (action === "preflight") {
|
|
4446
|
+
const output = appSmokePreflightSync(database, {
|
|
4447
|
+
dbPath,
|
|
4448
|
+
mode: parseAppSmokeMode(parsed.flags.smokeMode),
|
|
4449
|
+
scope: parseAppSmokeScope(parsed.flags.smokeScope),
|
|
4450
|
+
task,
|
|
4451
|
+
workerCount: parsed.flags.workerCount ?? 1,
|
|
4452
|
+
});
|
|
4453
|
+
return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(output) };
|
|
4454
|
+
}
|
|
4455
|
+
if (action === "start") {
|
|
4456
|
+
const mode = parseAppSmokeMode(parsed.flags.smokeMode);
|
|
4457
|
+
const scope = parseAppSmokeScope(parsed.flags.smokeScope);
|
|
4458
|
+
const binding = activeBindingForTaskSync(database, task.name);
|
|
4459
|
+
const smokeId = parsed.flags.smokeId ?? `smoke-${randomUUID()}`;
|
|
4460
|
+
const nonce = parsed.flags.smokeNonce ?? `nonce-${randomUUID()}`;
|
|
4461
|
+
const eventId = emitTelemetrySync(database, {
|
|
4462
|
+
actor: "operator",
|
|
4463
|
+
attributes: {
|
|
4464
|
+
binding_id: binding.binding_id,
|
|
4465
|
+
mode,
|
|
4466
|
+
nonce,
|
|
4467
|
+
scope,
|
|
4468
|
+
smoke_id: smokeId,
|
|
4469
|
+
worker_count: parsed.flags.workerCount ?? 1,
|
|
4470
|
+
},
|
|
4471
|
+
correlation: {
|
|
4472
|
+
command: "app-smoke",
|
|
4473
|
+
smoke_id: smokeId,
|
|
4474
|
+
},
|
|
4475
|
+
eventType: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
|
|
4476
|
+
severity: mode === "skip" ? "warning" : "info",
|
|
4477
|
+
summary: mode === "skip" ? `App smoke skipped for ${task.name}.` : `App smoke started for ${task.name}.`,
|
|
4478
|
+
taskId: task.id,
|
|
4479
|
+
timestamp,
|
|
4480
|
+
});
|
|
4481
|
+
const status = appSmokeStatusSync(database, {
|
|
4482
|
+
dbPath,
|
|
4483
|
+
now: timestamp,
|
|
4484
|
+
smokeId,
|
|
4485
|
+
staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
|
|
4486
|
+
task,
|
|
4487
|
+
});
|
|
4488
|
+
const output = {
|
|
4489
|
+
receipt: {
|
|
4490
|
+
event_id: eventId,
|
|
4491
|
+
event_type: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
|
|
4492
|
+
recorded_at: timestamp,
|
|
4493
|
+
},
|
|
4494
|
+
smoke: {
|
|
4495
|
+
id: smokeId,
|
|
4496
|
+
mode,
|
|
4497
|
+
nonce,
|
|
4498
|
+
scope,
|
|
4499
|
+
worker_count: parsed.flags.workerCount ?? 1,
|
|
4500
|
+
},
|
|
4501
|
+
status,
|
|
4502
|
+
task: { id: task.id, name: task.name },
|
|
4503
|
+
};
|
|
4504
|
+
return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
|
|
4505
|
+
}
|
|
4506
|
+
if (action === "record") {
|
|
4507
|
+
const smokeId = requiredStringFlag(parsed.flags.smokeId, "--smoke-id");
|
|
4508
|
+
const nonce = requiredStringFlag(parsed.flags.smokeNonce, "--nonce");
|
|
4509
|
+
const role = parseAppSmokeRole(parsed.flags.role);
|
|
4510
|
+
const status = parseAppSmokeRecordStatus(parsed.flags.statusState);
|
|
4511
|
+
if (role instanceof Error) {
|
|
4512
|
+
return errorResult(role.message);
|
|
4513
|
+
}
|
|
4514
|
+
if (status instanceof Error) {
|
|
4515
|
+
return errorResult(status.message);
|
|
4516
|
+
}
|
|
4517
|
+
const session = latestAppSmokeSessionSync(database, { smokeId, taskId: task.id });
|
|
4518
|
+
if (session.nonce !== nonce) {
|
|
4519
|
+
throw new Error(`Smoke nonce mismatch for ${smokeId}; expected ${session.nonce}.`);
|
|
4520
|
+
}
|
|
4521
|
+
validateAppSmokeRecordThread(database, {
|
|
4522
|
+
role,
|
|
4523
|
+
status,
|
|
4524
|
+
taskId: task.id,
|
|
4525
|
+
taskName: task.name,
|
|
4526
|
+
threadId: parsed.flags.threadId,
|
|
4527
|
+
});
|
|
4528
|
+
const payload = parsed.flags.fromStdin ? parseStdinJsonObject(options.stdin) : {};
|
|
4529
|
+
const eventId = emitTelemetrySync(database, {
|
|
4530
|
+
actor: role,
|
|
4531
|
+
attributes: {
|
|
4532
|
+
notification_id: parsed.flags.notificationId,
|
|
4533
|
+
nonce,
|
|
4534
|
+
payload,
|
|
4535
|
+
payload_keys: Object.keys(payload).sort(),
|
|
4536
|
+
role,
|
|
4537
|
+
smoke_id: smokeId,
|
|
4538
|
+
status,
|
|
4539
|
+
thread_id: parsed.flags.threadId,
|
|
4540
|
+
},
|
|
4541
|
+
correlation: {
|
|
4542
|
+
command: "app-smoke",
|
|
4543
|
+
correlation_id: parsed.flags.correlationId,
|
|
4544
|
+
nonce,
|
|
4545
|
+
role,
|
|
4546
|
+
smoke_id: smokeId,
|
|
4547
|
+
},
|
|
4548
|
+
eventType: "app_smoke_receipt_recorded",
|
|
4549
|
+
severity: status === "blocked" ? "warning" : "info",
|
|
4550
|
+
summary: `App smoke ${status} recorded for ${role} on ${task.name}.`,
|
|
4551
|
+
taskId: task.id,
|
|
4552
|
+
timestamp,
|
|
4553
|
+
});
|
|
4554
|
+
const output = {
|
|
4555
|
+
receipt: {
|
|
4556
|
+
event_id: eventId,
|
|
4557
|
+
event_type: "app_smoke_receipt_recorded",
|
|
4558
|
+
recorded_at: timestamp,
|
|
4559
|
+
},
|
|
4560
|
+
smoke: {
|
|
4561
|
+
id: smokeId,
|
|
4562
|
+
nonce,
|
|
4563
|
+
},
|
|
4564
|
+
task: { id: task.id, name: task.name },
|
|
4565
|
+
};
|
|
4566
|
+
return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: `App smoke ${status} recorded for ${role}.\n` };
|
|
4567
|
+
}
|
|
4568
|
+
const status = appSmokeStatusSync(database, {
|
|
4569
|
+
dbPath,
|
|
4570
|
+
now: timestamp,
|
|
4571
|
+
smokeId: parsed.flags.smokeId,
|
|
4572
|
+
staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
|
|
4573
|
+
task,
|
|
4574
|
+
});
|
|
4575
|
+
return parsed.flags.json ? jsonResult(status) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
|
|
4576
|
+
}
|
|
4577
|
+
finally {
|
|
4578
|
+
database.close();
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
function appSmokePreflightSync(database, options) {
|
|
4582
|
+
let binding = null;
|
|
4583
|
+
let bindingError = null;
|
|
4584
|
+
try {
|
|
4585
|
+
binding = activeBindingForTaskSync(database, options.task.name);
|
|
4586
|
+
}
|
|
4587
|
+
catch (error) {
|
|
4588
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
4589
|
+
}
|
|
4590
|
+
const bound = binding ? appSmokeBoundSessionsSync(database, options.task.id) : null;
|
|
4591
|
+
const checks = [
|
|
4592
|
+
{ ok: true, name: "ledger_writable", path: options.dbPath },
|
|
4593
|
+
{ ok: true, name: "package_cli_available", command: "app-smoke" },
|
|
4594
|
+
{
|
|
4595
|
+
detail: bindingError,
|
|
4596
|
+
ok: binding !== null,
|
|
4597
|
+
name: "active_binding",
|
|
4598
|
+
},
|
|
4599
|
+
{
|
|
4600
|
+
ok: Boolean(bound?.manager.thread_id),
|
|
4601
|
+
name: "manager_thread_metadata",
|
|
4602
|
+
thread_id: bound?.manager.thread_id ?? null,
|
|
4603
|
+
},
|
|
4604
|
+
{
|
|
4605
|
+
ok: Boolean(bound?.worker.thread_id),
|
|
4606
|
+
name: "worker_thread_metadata",
|
|
4607
|
+
thread_id: bound?.worker.thread_id ?? null,
|
|
4608
|
+
},
|
|
4609
|
+
{
|
|
4610
|
+
ok: false,
|
|
4611
|
+
name: "codex_app_thread_tools",
|
|
4612
|
+
note: "Plain CLI cannot call Codex app thread tools; the plugin skill must perform live sends.",
|
|
4613
|
+
required_in_plugin: true,
|
|
4614
|
+
},
|
|
4615
|
+
];
|
|
4616
|
+
const blockers = options.mode === "required"
|
|
4617
|
+
? checks.filter((check) => !check.ok && check.name !== "codex_app_thread_tools").map((check) => `${check.name}${check.detail ? `: ${check.detail}` : ""}`)
|
|
4618
|
+
: [];
|
|
4619
|
+
return {
|
|
4620
|
+
blockers,
|
|
4621
|
+
checks,
|
|
4622
|
+
mode: options.mode,
|
|
4623
|
+
ok: blockers.length === 0,
|
|
4624
|
+
real_work_allowed: options.mode !== "required" || blockers.length === 0,
|
|
4625
|
+
scope: options.scope,
|
|
4626
|
+
task: { id: options.task.id, name: options.task.name },
|
|
4627
|
+
worker_count: options.workerCount,
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
function appSmokeStatusSync(database, options) {
|
|
4631
|
+
const smoke = latestAppSmokeSessionSync(database, { smokeId: options.smokeId, taskId: options.task.id });
|
|
4632
|
+
const bound = appSmokeBoundSessionsSync(database, options.task.id);
|
|
4633
|
+
const receipts = appSmokeReceiptsSync(database, { smokeId: smoke.smoke_id, taskId: options.task.id });
|
|
4634
|
+
const manager = appSmokeRoleStatus("manager", bound.manager, receipts, smoke, options);
|
|
4635
|
+
const worker = appSmokeRoleStatus("worker", bound.worker, receipts, smoke, options);
|
|
4636
|
+
const blockers = smoke.mode === "skip" ? [] : [
|
|
4637
|
+
...appSmokeScopeBlockers(smoke),
|
|
4638
|
+
...appSmokeRoleBlockers("manager", manager),
|
|
4639
|
+
...appSmokeRoleBlockers("worker", worker),
|
|
4640
|
+
];
|
|
4641
|
+
const ok = blockers.length === 0;
|
|
4642
|
+
return {
|
|
4643
|
+
blockers,
|
|
4644
|
+
ok,
|
|
4645
|
+
real_work_allowed: smoke.mode === "skip" || smoke.mode === "advisory" || ok,
|
|
4646
|
+
roles: {
|
|
4647
|
+
manager,
|
|
4648
|
+
worker,
|
|
4649
|
+
},
|
|
4650
|
+
smoke: {
|
|
4651
|
+
id: smoke.smoke_id,
|
|
4652
|
+
mode: smoke.mode,
|
|
4653
|
+
nonce: smoke.nonce,
|
|
4654
|
+
recorded_at: smoke.recorded_at,
|
|
4655
|
+
scope: smoke.scope,
|
|
4656
|
+
worker_count: smoke.worker_count,
|
|
4657
|
+
},
|
|
4658
|
+
task: { id: options.task.id, name: options.task.name },
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
function appSmokeRoleStatus(role, session, receipts, smoke, options) {
|
|
4662
|
+
const roleReceipts = receipts.filter((receipt) => receipt.role === role && receipt.nonce === smoke.nonce);
|
|
4663
|
+
const sent = roleReceipts.some((receipt) => receipt.status === "sent");
|
|
4664
|
+
const received = roleReceipts.some((receipt) => receipt.status === "received");
|
|
4665
|
+
const accepted = roleReceipts.some((receipt) => receipt.status === "accepted");
|
|
4666
|
+
const blockedReceipt = roleReceipts.find((receipt) => receipt.status === "blocked");
|
|
4667
|
+
const heartbeatFresh = session.last_heartbeat_at !== null
|
|
4668
|
+
&& session.last_heartbeat_at >= smoke.recorded_at
|
|
4669
|
+
&& secondsBetweenIso(session.last_heartbeat_at, options.now) <= options.staleAfterSeconds;
|
|
4670
|
+
return {
|
|
4671
|
+
accepted,
|
|
4672
|
+
blocked: Boolean(blockedReceipt),
|
|
4673
|
+
blocker: typeof blockedReceipt?.payload.summary === "string" ? blockedReceipt.payload.summary : null,
|
|
4674
|
+
heartbeat_fresh: heartbeatFresh,
|
|
4675
|
+
last_heartbeat_at: session.last_heartbeat_at,
|
|
4676
|
+
received,
|
|
4677
|
+
sent,
|
|
4678
|
+
session_id: session.session_id,
|
|
4679
|
+
session_name: session.session_name,
|
|
4680
|
+
thread_id: session.thread_id,
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
function appSmokeRoleBlockers(role, status) {
|
|
4684
|
+
const blockers = [];
|
|
4685
|
+
if (!status.thread_id) {
|
|
4686
|
+
blockers.push(`${role} has no Codex app thread id`);
|
|
4687
|
+
}
|
|
4688
|
+
if (!status.sent) {
|
|
4689
|
+
blockers.push(`${role} smoke prompt has not been recorded as sent`);
|
|
4690
|
+
}
|
|
4691
|
+
if (!status.heartbeat_fresh) {
|
|
4692
|
+
blockers.push(`${role} heartbeat is not fresh after smoke start`);
|
|
4693
|
+
}
|
|
4694
|
+
if (role === "worker" && !status.received) {
|
|
4695
|
+
blockers.push("worker has not recorded smoke received");
|
|
4696
|
+
}
|
|
4697
|
+
if (!status.accepted) {
|
|
4698
|
+
blockers.push(`${role} has not accepted smoke`);
|
|
4699
|
+
}
|
|
4700
|
+
if (status.blocked) {
|
|
4701
|
+
blockers.push(`${role} smoke blocked${status.blocker ? `: ${status.blocker}` : ""}`);
|
|
4702
|
+
}
|
|
4703
|
+
return blockers;
|
|
4704
|
+
}
|
|
4705
|
+
function appSmokeScopeBlockers(smoke) {
|
|
4706
|
+
if (smoke.scope === "worker-set" && smoke.worker_count > 1) {
|
|
4707
|
+
return [`worker-set smoke for one task proves 1 of ${smoke.worker_count} workers; run per-worker smoke and aggregate in the plugin skill`];
|
|
4708
|
+
}
|
|
4709
|
+
return [];
|
|
4710
|
+
}
|
|
4711
|
+
function latestAppSmokeSessionSync(database, options) {
|
|
4712
|
+
const params = [options.taskId];
|
|
4713
|
+
const smokeClause = options.smokeId ? "and json_extract(attributes_json, '$.smoke_id') = ?" : "";
|
|
4714
|
+
if (options.smokeId) {
|
|
4715
|
+
params.push(options.smokeId);
|
|
4716
|
+
}
|
|
4717
|
+
const row = database.prepare(`
|
|
4718
|
+
select id, timestamp, event_type, attributes_json
|
|
4719
|
+
from telemetry_events
|
|
4720
|
+
where task_id = ?
|
|
4721
|
+
and event_type in ('app_smoke_started', 'app_smoke_skipped')
|
|
4722
|
+
${smokeClause}
|
|
4723
|
+
order by timestamp desc, id desc
|
|
4724
|
+
limit 1
|
|
4725
|
+
`).get(...params);
|
|
4726
|
+
if (!row) {
|
|
4727
|
+
throw new Error(options.smokeId ? `Unknown app smoke session: ${options.smokeId}` : "No app smoke session has been started for this task.");
|
|
4728
|
+
}
|
|
4729
|
+
const attributes = JSON.parse(row.attributes_json);
|
|
4730
|
+
return {
|
|
4731
|
+
event_id: row.id,
|
|
4732
|
+
mode: attributes.mode,
|
|
4733
|
+
nonce: attributes.nonce,
|
|
4734
|
+
recorded_at: row.timestamp,
|
|
4735
|
+
scope: attributes.scope,
|
|
4736
|
+
smoke_id: attributes.smoke_id,
|
|
4737
|
+
worker_count: attributes.worker_count,
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
function appSmokeReceiptsSync(database, options) {
|
|
4741
|
+
const rows = database.prepare(`
|
|
4742
|
+
select attributes_json
|
|
4743
|
+
from telemetry_events
|
|
4744
|
+
where task_id = ?
|
|
4745
|
+
and event_type = 'app_smoke_receipt_recorded'
|
|
4746
|
+
and json_extract(attributes_json, '$.smoke_id') = ?
|
|
4747
|
+
order by timestamp, id
|
|
4748
|
+
`).all(options.taskId, options.smokeId);
|
|
4749
|
+
return rows.map((row) => JSON.parse(row.attributes_json));
|
|
4750
|
+
}
|
|
4751
|
+
function appSmokeBoundSessionsSync(database, taskId) {
|
|
4752
|
+
const row = database.prepare(`
|
|
4753
|
+
select
|
|
4754
|
+
ms.id as manager_session_id,
|
|
4755
|
+
ms.name as manager_session_name,
|
|
4756
|
+
ms.codex_app_thread_id as manager_thread_id,
|
|
4757
|
+
ms.codex_app_thread_title as manager_thread_title,
|
|
4758
|
+
ms.last_heartbeat_at as manager_last_heartbeat_at,
|
|
4759
|
+
ws.id as worker_session_id,
|
|
4760
|
+
ws.name as worker_session_name,
|
|
4761
|
+
ws.codex_app_thread_id as worker_thread_id,
|
|
4762
|
+
ws.codex_app_thread_title as worker_thread_title,
|
|
4763
|
+
ws.last_heartbeat_at as worker_last_heartbeat_at
|
|
4764
|
+
from bindings b
|
|
4765
|
+
join sessions ms on ms.id = b.manager_session_id
|
|
4766
|
+
join sessions ws on ws.id = b.worker_session_id
|
|
4767
|
+
where b.task_id = ?
|
|
4768
|
+
and b.state in ('active', 'ending')
|
|
4769
|
+
order by b.created_at desc
|
|
4770
|
+
limit 1
|
|
4771
|
+
`).get(taskId);
|
|
4772
|
+
if (!row) {
|
|
4773
|
+
throw new Error("No active app smoke binding for task.");
|
|
4774
|
+
}
|
|
4775
|
+
return {
|
|
4776
|
+
manager: {
|
|
4777
|
+
last_heartbeat_at: row.manager_last_heartbeat_at,
|
|
4778
|
+
session_id: row.manager_session_id,
|
|
4779
|
+
session_name: row.manager_session_name,
|
|
4780
|
+
thread_id: row.manager_thread_id,
|
|
4781
|
+
thread_title: row.manager_thread_title,
|
|
4782
|
+
},
|
|
4783
|
+
worker: {
|
|
4784
|
+
last_heartbeat_at: row.worker_last_heartbeat_at,
|
|
4785
|
+
session_id: row.worker_session_id,
|
|
4786
|
+
session_name: row.worker_session_name,
|
|
4787
|
+
thread_id: row.worker_thread_id,
|
|
4788
|
+
thread_title: row.worker_thread_title,
|
|
4789
|
+
},
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
function validateAppSmokeRecordThread(database, options) {
|
|
4793
|
+
const bound = appSmokeBoundSessionsSync(database, options.taskId);
|
|
4794
|
+
const expected = options.role === "manager" ? bound.manager.thread_id : bound.worker.thread_id;
|
|
4795
|
+
if (options.status === "sent" && !options.threadId) {
|
|
4796
|
+
throw new Error("app-smoke record --status sent requires --thread-id.");
|
|
4797
|
+
}
|
|
4798
|
+
if (options.threadId && expected !== options.threadId) {
|
|
4799
|
+
throw new Error(`Thread id mismatch for ${options.role}; active binding for ${options.taskName} targets ${expected ?? "(missing)"}.`);
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
function parseAppSmokeMode(value) {
|
|
4803
|
+
return value === "advisory" || value === "skip" ? value : "required";
|
|
4804
|
+
}
|
|
4805
|
+
function parseAppSmokeScope(value) {
|
|
4806
|
+
return value === "worker-set" ? "worker-set" : "pair";
|
|
4807
|
+
}
|
|
4808
|
+
function parseAppSmokeRole(value) {
|
|
4809
|
+
if (value === "manager" || value === "worker") {
|
|
4810
|
+
return value;
|
|
4811
|
+
}
|
|
4812
|
+
return new Error("app-smoke record requires --role manager|worker");
|
|
4813
|
+
}
|
|
4814
|
+
function parseAppSmokeRecordStatus(value) {
|
|
4815
|
+
if (value === "accepted" || value === "blocked" || value === "received" || value === "sent" || value === "skipped") {
|
|
4816
|
+
return value;
|
|
4817
|
+
}
|
|
4818
|
+
return new Error("app-smoke record requires --status sent|skipped|received|accepted|blocked");
|
|
4819
|
+
}
|
|
4820
|
+
function secondsBetweenIso(left, right) {
|
|
4821
|
+
return Math.max(0, (Date.parse(right) - Date.parse(left)) / 1000);
|
|
4822
|
+
}
|
|
4823
|
+
function renderAppSmokeStatusText(status) {
|
|
4824
|
+
const smoke = isPlainRecord(status.smoke) ? status.smoke : {};
|
|
4825
|
+
const blockers = Array.isArray(status.blockers) ? status.blockers.map(String) : [];
|
|
4826
|
+
const lines = [
|
|
4827
|
+
`App smoke ${String(smoke.id ?? "(preflight)")}: ${status.ok ? "ok" : "blocked"}`,
|
|
4828
|
+
`Real work allowed: ${String(status.real_work_allowed ?? false)}`,
|
|
4829
|
+
];
|
|
4830
|
+
for (const blocker of blockers) {
|
|
4831
|
+
lines.push(`Blocker: ${blocker}`);
|
|
4832
|
+
}
|
|
4833
|
+
return `${lines.join("\n")}\n`;
|
|
4834
|
+
}
|
|
4329
4835
|
function parseAppWakeupDeliveryStatus(value) {
|
|
4330
4836
|
if (value === "sent" || value === "skipped" || value === "blocked") {
|
|
4331
4837
|
return value;
|
|
@@ -4383,9 +4889,16 @@ function validateAppWakeupDelivery(options) {
|
|
|
4383
4889
|
}
|
|
4384
4890
|
return;
|
|
4385
4891
|
}
|
|
4386
|
-
if (sourceStatus
|
|
4387
|
-
|
|
4892
|
+
if (sourceStatus === "blocked_missing_thread") {
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
if (sourceStatus === "ready_to_send" && options.action.send_ready === true) {
|
|
4896
|
+
if (!options.reason?.trim()) {
|
|
4897
|
+
throw new Error(`Cannot record blocked wakeup for ${options.role}; ready-to-send source actions require --reason.`);
|
|
4898
|
+
}
|
|
4899
|
+
return;
|
|
4388
4900
|
}
|
|
4901
|
+
throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
|
|
4389
4902
|
}
|
|
4390
4903
|
function parseAppWakeupSourceActionStatus(value) {
|
|
4391
4904
|
if (value === "ready_to_send" || value === "skipped_healthy" || value === "blocked_missing_thread") {
|
|
@@ -6698,6 +7211,8 @@ function runInstallSkillsCommand(parsed, options) {
|
|
|
6698
7211
|
}
|
|
6699
7212
|
const AGENT_CONVEYOR_PLUGIN_NAME = "agent-conveyor";
|
|
6700
7213
|
const AGENT_CONVEYOR_PLUGIN_SKILLS = [
|
|
7214
|
+
"conveyor-app-wake-relay",
|
|
7215
|
+
"conveyor-smoke-app-connections",
|
|
6701
7216
|
"conveyor-create-pair",
|
|
6702
7217
|
"conveyor-create-worker-set",
|
|
6703
7218
|
"conveyor-check-status",
|
|
@@ -9092,6 +9607,94 @@ function runTaskAckCommand(parsed, options, role) {
|
|
|
9092
9607
|
database.close();
|
|
9093
9608
|
}
|
|
9094
9609
|
}
|
|
9610
|
+
function runInboxAckCommand(parsed, options) {
|
|
9611
|
+
const unsupported = unsupportedInboxAckOptions(parsed);
|
|
9612
|
+
if (unsupported) {
|
|
9613
|
+
return unsupportedRuntimeResult(parsed, unsupported);
|
|
9614
|
+
}
|
|
9615
|
+
const taskName = requireTask(parsed);
|
|
9616
|
+
const notificationId = parsed.flags.notificationId;
|
|
9617
|
+
if (notificationId === null) {
|
|
9618
|
+
return errorResult("inbox-ack requires --notification-id.");
|
|
9619
|
+
}
|
|
9620
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
9621
|
+
try {
|
|
9622
|
+
const task = taskRowForLifecycle(database, taskName);
|
|
9623
|
+
if (task === null) {
|
|
9624
|
+
throw new Error(`Unknown task: ${taskName}`);
|
|
9625
|
+
}
|
|
9626
|
+
const binding = activeBindingForTaskSync(database, task.name);
|
|
9627
|
+
const notification = notificationForInboxAckSync(database, {
|
|
9628
|
+
notificationId,
|
|
9629
|
+
taskId: task.id,
|
|
9630
|
+
taskName: task.name,
|
|
9631
|
+
});
|
|
9632
|
+
if (notification.binding_id !== binding.binding_id) {
|
|
9633
|
+
throw new Error(`Notification ${notificationId} does not belong to the active binding for task ${task.name}.`);
|
|
9634
|
+
}
|
|
9635
|
+
if (!parsed.flags.fromStdin) {
|
|
9636
|
+
const acknowledgements = notificationAcknowledgementsSync(database, { notificationId, taskId: task.id });
|
|
9637
|
+
return jsonResult({
|
|
9638
|
+
acknowledgements,
|
|
9639
|
+
latest: latestNotificationAcknowledgementSync(database, { notificationId, taskId: task.id }),
|
|
9640
|
+
notification: inboxAckNotificationPayload(notification),
|
|
9641
|
+
task: { id: task.id, name: task.name },
|
|
9642
|
+
});
|
|
9643
|
+
}
|
|
9644
|
+
const role = parsed.flags.role;
|
|
9645
|
+
const status = parseNotificationAcknowledgementStatus(parsed.flags.statusState);
|
|
9646
|
+
if (status instanceof Error) {
|
|
9647
|
+
return errorResult(status.message);
|
|
9648
|
+
}
|
|
9649
|
+
validateInboxAcknowledgement({ notification, role, status });
|
|
9650
|
+
const payload = parseStdinJsonObject(options.stdin);
|
|
9651
|
+
const ack = insertNotificationAcknowledgementSync(database, {
|
|
9652
|
+
bindingId: notification.binding_id,
|
|
9653
|
+
correlationId: parsed.flags.correlationId,
|
|
9654
|
+
notificationId,
|
|
9655
|
+
now: nowIsoSeconds(options),
|
|
9656
|
+
payload,
|
|
9657
|
+
role,
|
|
9658
|
+
status,
|
|
9659
|
+
taskId: task.id,
|
|
9660
|
+
});
|
|
9661
|
+
const eventId = emitTelemetrySync(database, {
|
|
9662
|
+
actor: role,
|
|
9663
|
+
attributes: {
|
|
9664
|
+
binding_id: notification.binding_id,
|
|
9665
|
+
notification_id: notificationId,
|
|
9666
|
+
payload_keys: Object.keys(payload).sort(),
|
|
9667
|
+
role,
|
|
9668
|
+
signal_type: notification.signal_type,
|
|
9669
|
+
status,
|
|
9670
|
+
},
|
|
9671
|
+
correlation: {
|
|
9672
|
+
command: "inbox-ack",
|
|
9673
|
+
correlation_id: parsed.flags.correlationId,
|
|
9674
|
+
notification_id: notificationId,
|
|
9675
|
+
role,
|
|
9676
|
+
},
|
|
9677
|
+
eventType: "dispatch_inbox_ack_recorded",
|
|
9678
|
+
severity: status === "blocked" ? "warning" : "info",
|
|
9679
|
+
summary: `${role} recorded ${status} acknowledgement for notification ${notificationId}.`,
|
|
9680
|
+
taskId: task.id,
|
|
9681
|
+
timestamp: ack.created_at,
|
|
9682
|
+
});
|
|
9683
|
+
return jsonResult({
|
|
9684
|
+
acknowledgement: ack,
|
|
9685
|
+
notification: inboxAckNotificationPayload(notification),
|
|
9686
|
+
receipt: {
|
|
9687
|
+
event_id: eventId,
|
|
9688
|
+
event_type: "dispatch_inbox_ack_recorded",
|
|
9689
|
+
recorded_at: ack.created_at,
|
|
9690
|
+
},
|
|
9691
|
+
task: { id: task.id, name: task.name },
|
|
9692
|
+
});
|
|
9693
|
+
}
|
|
9694
|
+
finally {
|
|
9695
|
+
database.close();
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9095
9698
|
function runSessionInboxCommand(parsed, options, kind) {
|
|
9096
9699
|
const unsupported = unsupportedSessionInboxOptions(parsed, kind);
|
|
9097
9700
|
if (unsupported) {
|
|
@@ -9341,6 +9944,59 @@ function latestTaskAcknowledgementSync(database, options) {
|
|
|
9341
9944
|
task_id: row.task_id,
|
|
9342
9945
|
};
|
|
9343
9946
|
}
|
|
9947
|
+
function notificationForInboxAckSync(database, options) {
|
|
9948
|
+
const row = database.prepare(`
|
|
9949
|
+
select rn.id, rn.task_id, rn.binding_id, rn.signal_type, rn.delivered_at,
|
|
9950
|
+
rn.consumed_at, rn.delivery_mode, rn.state, rn.target_session_id,
|
|
9951
|
+
ts.name as target_session_name, ts.role as target_session_role
|
|
9952
|
+
from routed_notifications rn
|
|
9953
|
+
join sessions ts on ts.id = rn.target_session_id
|
|
9954
|
+
where rn.id = ? and rn.task_id = ?
|
|
9955
|
+
limit 1
|
|
9956
|
+
`).get(options.notificationId, options.taskId);
|
|
9957
|
+
if (!row) {
|
|
9958
|
+
throw new Error(`Unknown routed notification for task ${options.taskName}: ${options.notificationId}`);
|
|
9959
|
+
}
|
|
9960
|
+
return row;
|
|
9961
|
+
}
|
|
9962
|
+
function inboxAckNotificationPayload(notification) {
|
|
9963
|
+
return {
|
|
9964
|
+
binding_id: notification.binding_id,
|
|
9965
|
+
consumed_at: notification.consumed_at,
|
|
9966
|
+
delivered_at: notification.delivered_at,
|
|
9967
|
+
delivery_mode: notification.delivery_mode,
|
|
9968
|
+
id: notification.id,
|
|
9969
|
+
signal_type: notification.signal_type,
|
|
9970
|
+
state: notification.state,
|
|
9971
|
+
target_session_id: notification.target_session_id,
|
|
9972
|
+
target_session_name: notification.target_session_name,
|
|
9973
|
+
target_session_role: notification.target_session_role,
|
|
9974
|
+
task_id: notification.task_id,
|
|
9975
|
+
};
|
|
9976
|
+
}
|
|
9977
|
+
function parseNotificationAcknowledgementStatus(value) {
|
|
9978
|
+
if (value === "received" || value === "accepted" || value === "blocked") {
|
|
9979
|
+
return value;
|
|
9980
|
+
}
|
|
9981
|
+
return new Error("inbox-ack --status must be received, accepted, or blocked.");
|
|
9982
|
+
}
|
|
9983
|
+
function validateInboxAcknowledgement(options) {
|
|
9984
|
+
if (options.notification.target_session_role !== options.role) {
|
|
9985
|
+
throw new Error(`Cannot record ${options.role} acknowledgement for notification ${options.notification.id}; target role is ${options.notification.target_session_role}.`);
|
|
9986
|
+
}
|
|
9987
|
+
if (options.notification.state !== "delivered") {
|
|
9988
|
+
throw new Error(`Cannot acknowledge notification ${options.notification.id}; state is ${options.notification.state}, expected delivered.`);
|
|
9989
|
+
}
|
|
9990
|
+
if (options.status === "received") {
|
|
9991
|
+
if (!options.notification.delivered_at) {
|
|
9992
|
+
throw new Error(`Cannot record received acknowledgement for notification ${options.notification.id}; it has no delivered_at receipt.`);
|
|
9993
|
+
}
|
|
9994
|
+
return;
|
|
9995
|
+
}
|
|
9996
|
+
if (!options.notification.consumed_at) {
|
|
9997
|
+
throw new Error(`Cannot record ${options.status} acknowledgement for notification ${options.notification.id}; it has not been consumed by the target session.`);
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
9344
10000
|
function sessionInboxResponse(database, options) {
|
|
9345
10001
|
if (options.timeoutSeconds < 0) {
|
|
9346
10002
|
throw new Error("--timeout must be non-negative");
|
|
@@ -15206,6 +15862,26 @@ function unsupportedTaskAckOptions(parsed) {
|
|
|
15206
15862
|
}
|
|
15207
15863
|
return null;
|
|
15208
15864
|
}
|
|
15865
|
+
function unsupportedInboxAckOptions(parsed) {
|
|
15866
|
+
if (!parsed.task) {
|
|
15867
|
+
return "inbox-ack requires a task.";
|
|
15868
|
+
}
|
|
15869
|
+
if (parsed.flags.notificationId === null) {
|
|
15870
|
+
return "inbox-ack requires --notification-id.";
|
|
15871
|
+
}
|
|
15872
|
+
if (!parsed.flags.fromStdin && !parsed.flags.json) {
|
|
15873
|
+
return "inbox-ack requires --from-stdin to write or --json to read.";
|
|
15874
|
+
}
|
|
15875
|
+
if (parsed.flags.fromStdin) {
|
|
15876
|
+
if (parsed.flags.role !== "manager" && parsed.flags.role !== "worker") {
|
|
15877
|
+
return "inbox-ack write requires --role manager|worker.";
|
|
15878
|
+
}
|
|
15879
|
+
if (parsed.flags.statusState === null) {
|
|
15880
|
+
return "inbox-ack write requires --status received|accepted|blocked.";
|
|
15881
|
+
}
|
|
15882
|
+
}
|
|
15883
|
+
return null;
|
|
15884
|
+
}
|
|
15209
15885
|
function unsupportedSessionInboxOptions(parsed, kind) {
|
|
15210
15886
|
if (!parsed.task) {
|
|
15211
15887
|
return kind === "session" ? "session-inbox requires a session name." : `${kind}-inbox requires a task.`;
|
|
@@ -15677,6 +16353,7 @@ function isDefaultRuntimeCommand(command) {
|
|
|
15677
16353
|
|| command === "app-worker-rotation-plan"
|
|
15678
16354
|
|| command === "app-worker-rotation-record"
|
|
15679
16355
|
|| command === "app-autopilot"
|
|
16356
|
+
|| command === "app-smoke"
|
|
15680
16357
|
|| command === "loop-templates"
|
|
15681
16358
|
|| command === "loop-triggers"
|
|
15682
16359
|
|| command === "ralph-loop-presets"
|
|
@@ -15723,6 +16400,7 @@ function isDefaultRuntimeCommand(command) {
|
|
|
15723
16400
|
|| command === "nudge"
|
|
15724
16401
|
|| command === "worker-ack"
|
|
15725
16402
|
|| command === "manager-ack"
|
|
16403
|
+
|| command === "inbox-ack"
|
|
15726
16404
|
|| command === "session-inbox"
|
|
15727
16405
|
|| command === "manager-inbox"
|
|
15728
16406
|
|| command === "worker-inbox"
|