pi-subagents 0.66.0 → 0.67.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/CHANGELOG.md +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree.ts +42 -12
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +4 -1
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +149 -28
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -54,6 +54,16 @@ export interface SubagentNotifyDetails {
|
|
|
54
54
|
watchdogBlockers?: SubagentNotifyWatchdogBlocker[];
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface IncrementalChildCompletion {
|
|
58
|
+
workflowRunId: string;
|
|
59
|
+
childKey: string;
|
|
60
|
+
childRunId?: string;
|
|
61
|
+
outcome: "completed" | "failed" | "paused" | "stopped";
|
|
62
|
+
outputReference?: string;
|
|
63
|
+
error?: string;
|
|
64
|
+
workflowRunning: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
export interface CompletionNotification {
|
|
58
68
|
[key: string]: unknown;
|
|
59
69
|
id?: string | null;
|
|
@@ -308,6 +318,26 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
|
308
318
|
.join("\n");
|
|
309
319
|
}
|
|
310
320
|
|
|
321
|
+
export function scheduledCompletionTriggersTurn(origin: ScheduleOrigin | undefined, outcome: string): boolean {
|
|
322
|
+
return !(origin?.quiet === true && outcome === "completed");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function formatIncrementalChildCompletion(child: IncrementalChildCompletion): string {
|
|
326
|
+
const statusText = child.outcome === "completed" ? "completed"
|
|
327
|
+
: child.outcome === "failed" ? "failed"
|
|
328
|
+
: child.outcome === "paused" ? "paused (needs attention)"
|
|
329
|
+
: "stopped";
|
|
330
|
+
const workflowStatus = child.workflowRunning ? "workflow still running" : "workflow finished";
|
|
331
|
+
return [
|
|
332
|
+
`Workflow child ${statusText}: **${child.childKey}**`,
|
|
333
|
+
`Workflow run: ${child.workflowRunId}`,
|
|
334
|
+
...(child.childRunId ? [`Child run: ${child.childRunId}`] : []),
|
|
335
|
+
...(child.outputReference ? [`Output: ${child.outputReference}`] : []),
|
|
336
|
+
...(child.error ? [`Error: ${child.error}`] : []),
|
|
337
|
+
`Status: ${workflowStatus}`,
|
|
338
|
+
].join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
311
341
|
export function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
312
342
|
const lines = content.split("\n");
|
|
313
343
|
const match = (lines[0] ?? "").match(/^(Background task|Detached foreground task) (completed|failed|paused|stopped): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
|
|
@@ -675,7 +705,7 @@ export default function registerSubagentNotify(
|
|
|
675
705
|
details,
|
|
676
706
|
sessionId: result.sessionId,
|
|
677
707
|
completionOwnerId: result.completionOwnerId,
|
|
678
|
-
triggerTurn: result.triggerTurn !== false,
|
|
708
|
+
triggerTurn: result.triggerTurn !== false && scheduledCompletionTriggersTurn(result.scheduleOrigin, details.status),
|
|
679
709
|
resolve,
|
|
680
710
|
};
|
|
681
711
|
if (notificationDebug.enabled) item.trace = traceIdentity(result);
|
|
@@ -35,6 +35,23 @@ function activeProcessGroupMembers(processGroupId: number): number[] | { diagnos
|
|
|
35
35
|
return members;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function knownDetachedDescendants(processGroupId: number): number[] {
|
|
39
|
+
const result = spawnSync("ps", ["-axo", "pid=,ppid=,pgid=,stat="], { encoding: "utf-8" });
|
|
40
|
+
if (result.error || result.status !== 0) return [];
|
|
41
|
+
const owned = new Set<number>();
|
|
42
|
+
const outside: { pid: number; ppid: number }[] = [];
|
|
43
|
+
for (const line of result.stdout.split("\n")) {
|
|
44
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/.exec(line);
|
|
45
|
+
if (!match || match[4]!.startsWith("Z")) continue;
|
|
46
|
+
const pid = Number(match[1]);
|
|
47
|
+
const ppid = Number(match[2]);
|
|
48
|
+
const pgid = Number(match[3]);
|
|
49
|
+
if (pgid === processGroupId) owned.add(pid);
|
|
50
|
+
else outside.push({ pid, ppid });
|
|
51
|
+
}
|
|
52
|
+
return outside.filter((row) => owned.has(row.ppid)).map((row) => row.pid);
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
async function waitUntilGroupTerminal(
|
|
39
56
|
processGroupId: number,
|
|
40
57
|
timeoutMs: number,
|
|
@@ -56,6 +73,15 @@ function observed(processGroupId: number): ProcessTreeTerminal {
|
|
|
56
73
|
return { state: "observed", mechanism: "posix-process-group", processGroupId, verifiedAt: Date.now() };
|
|
57
74
|
}
|
|
58
75
|
|
|
76
|
+
function observedUnlessLiveDetached(processGroupId: number, detached: readonly number[]): ProcessTreeTerminal {
|
|
77
|
+
const live = detached.filter((pid) => {
|
|
78
|
+
const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf-8" });
|
|
79
|
+
return result.status === 0 && Boolean(result.stdout.trim()) && !result.stdout.trim().startsWith("Z");
|
|
80
|
+
});
|
|
81
|
+
if (live.length === 0) return observed(processGroupId);
|
|
82
|
+
return { state: "unknown", reason: "verification-failed", diagnostic: `Owned detached descendant(s) still active: ${live.join(", ")}.` };
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/** Owns one writer process group and arbitrates its cleanup exactly once. */
|
|
60
86
|
export interface OwnedProcessTreeController {
|
|
61
87
|
terminate(): Promise<ProcessTreeTerminal>;
|
|
@@ -77,12 +103,13 @@ export function createOwnedProcessTreeController(
|
|
|
77
103
|
signalProcess(target, "SIGTERM");
|
|
78
104
|
return { state: "unknown", reason: "unsupported-platform" };
|
|
79
105
|
}
|
|
106
|
+
const detached = knownDetachedDescendants(pid);
|
|
80
107
|
const term = signalProcess(target, "SIGTERM");
|
|
81
108
|
if (term !== "sent" && term !== "absent") {
|
|
82
109
|
return { state: "unknown", reason: "signal-failed", diagnostic: term.diagnostic };
|
|
83
110
|
}
|
|
84
111
|
const termExit = await waitUntilGroupTerminal(pid, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS);
|
|
85
|
-
if (termExit === false) return
|
|
112
|
+
if (termExit === false) return observedUnlessLiveDetached(pid, detached);
|
|
86
113
|
|
|
87
114
|
const kill = signalProcess(target, "SIGKILL");
|
|
88
115
|
if (kill !== "sent" && kill !== "absent") {
|
|
@@ -95,7 +122,7 @@ export function createOwnedProcessTreeController(
|
|
|
95
122
|
if (killExit !== false) {
|
|
96
123
|
return { state: "unknown", reason: "verification-failed", diagnostic: killExit.diagnostic };
|
|
97
124
|
}
|
|
98
|
-
return
|
|
125
|
+
return observedUnlessLiveDetached(pid, detached);
|
|
99
126
|
})();
|
|
100
127
|
return termination;
|
|
101
128
|
};
|
|
@@ -23,10 +23,11 @@ import { formatSubagentModelVerificationError } from "../shared/model-fallback.t
|
|
|
23
23
|
import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.ts";
|
|
24
24
|
import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
|
|
25
25
|
import { createReportedChildSessionInput, type InProcessChildLaunch } from "../shared/child-launch.ts";
|
|
26
|
-
import { projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
|
|
26
|
+
import { childSessionHasQueuedMessages, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
|
|
27
27
|
import { formatSteerMessage } from "../shared/subagent-prompt-runtime.ts";
|
|
28
28
|
import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
|
|
29
29
|
import type { SteerDeliveryStatus, SteerRequest } from "./control-channel.ts";
|
|
30
|
+
import { takeMatchingAcceptedSteer, unconsumedSteerReason } from "./steering.ts";
|
|
30
31
|
|
|
31
32
|
export interface ChildEventContext {
|
|
32
33
|
runId: string;
|
|
@@ -84,6 +85,8 @@ export interface RunChildSessionInput {
|
|
|
84
85
|
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
85
86
|
registerStop?: (stop: (() => void) | undefined) => void;
|
|
86
87
|
registerSteer?: (steer: StepSteerHandler | undefined) => void;
|
|
88
|
+
/** Consumption (or unconsumed settlement) after the child accepted a steer or follow-up. */
|
|
89
|
+
onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
|
|
87
90
|
/** Receives the sink the child's watchdog hook reports status through; the launch's `watchdogStatus` must forward to it. */
|
|
88
91
|
registerWatchdogStatus?: (sink: ((event: ChildWatchdogStatusEvent) => void) | undefined) => void;
|
|
89
92
|
timeoutMessage?: string;
|
|
@@ -187,12 +190,14 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
187
190
|
let currentPath: string | undefined;
|
|
188
191
|
let toolCount = 0;
|
|
189
192
|
let session: ChildSession | undefined;
|
|
193
|
+
const acceptedSteers: Array<{ request: SteerRequest; text: string }> = [];
|
|
190
194
|
let unsubscribe: (() => void) | undefined;
|
|
191
195
|
let settled = false;
|
|
192
196
|
let promptSettled = false;
|
|
193
197
|
let forcedTermination = false;
|
|
194
198
|
let cleanTerminalAssistantStopReceived = false;
|
|
195
199
|
let agentSettledReceived = false;
|
|
200
|
+
let queuedDrainHold = false;
|
|
196
201
|
let compactionStartedReceived = false;
|
|
197
202
|
let afterCompactionSettlement = false;
|
|
198
203
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
@@ -311,14 +316,28 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
311
316
|
}
|
|
312
317
|
// If the child emits its terminal event but its run never settles (a hook
|
|
313
318
|
// is stuck), abort it after a short grace period and then finish without it.
|
|
319
|
+
const observeQueuedDrainHold = (): boolean => {
|
|
320
|
+
if (childSessionHasQueuedMessages(session)) queuedDrainHold = true;
|
|
321
|
+
return queuedDrainHold;
|
|
322
|
+
};
|
|
314
323
|
function startFinalDrain(): void {
|
|
315
324
|
if (childWatchdogIsActive(childWatchdogState)) {
|
|
316
325
|
armWatchdogTail();
|
|
317
326
|
return;
|
|
318
327
|
}
|
|
328
|
+
if (promptSettled || finalDrainTimer || settled) return;
|
|
329
|
+
if (observeQueuedDrainHold()) return;
|
|
330
|
+
armFinalDrainTimer();
|
|
331
|
+
}
|
|
332
|
+
function armFinalDrainTimer(): void {
|
|
319
333
|
if (promptSettled || finalDrainTimer || settled) return;
|
|
320
334
|
finalDrainTimer = setTimeout(() => {
|
|
321
335
|
if (settled || promptSettled) return;
|
|
336
|
+
if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold()) {
|
|
337
|
+
finalDrainTimer = undefined;
|
|
338
|
+
armFinalDrainTimer();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
322
341
|
forcedTermination = true;
|
|
323
342
|
if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
|
|
324
343
|
error = `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
|
|
@@ -334,6 +353,8 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
334
353
|
}
|
|
335
354
|
const applyChildLifecycle = (action: ChildLifecycleAction): void => {
|
|
336
355
|
if (action === "cancel-drain") {
|
|
356
|
+
cleanTerminalAssistantStopReceived = false;
|
|
357
|
+
agentSettledReceived = false;
|
|
337
358
|
clearFinalDrainTimers();
|
|
338
359
|
clearWatchdogTailTimer();
|
|
339
360
|
return;
|
|
@@ -401,6 +422,9 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
401
422
|
compactionStartedReceived = false;
|
|
402
423
|
afterCompactionSettlement = false;
|
|
403
424
|
}
|
|
425
|
+
if (event.type === "turn_start" || event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
426
|
+
queuedDrainHold = false;
|
|
427
|
+
}
|
|
404
428
|
if (event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
405
429
|
compactionStartedReceived = false;
|
|
406
430
|
afterCompactionSettlement = false;
|
|
@@ -470,6 +494,16 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
470
494
|
messages.push(event.message);
|
|
471
495
|
const text = extractTextFromContent(event.message.content);
|
|
472
496
|
if (text) writeOutputText(text);
|
|
497
|
+
if (event.type === "message_end" && event.message.role === "user" && text) {
|
|
498
|
+
const matched = takeMatchingAcceptedSteer(acceptedSteers, text);
|
|
499
|
+
if (matched) {
|
|
500
|
+
input.onSteerOutcome?.(matched.request, {
|
|
501
|
+
state: "delivered",
|
|
502
|
+
deliveryStatus: "delivered",
|
|
503
|
+
message: "Child consumed the steering input.",
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
473
507
|
|
|
474
508
|
if (input.childWatchdog && event.type === "message_end") {
|
|
475
509
|
const next = applyChildWatchdogMessage(childWatchdogState, event.message);
|
|
@@ -529,9 +563,15 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
529
563
|
};
|
|
530
564
|
|
|
531
565
|
/** The child run ended (or was forced to end); fold in the outcome once the child's shutdown work is done. */
|
|
566
|
+
const failUnconsumedSteers = (): void => {
|
|
567
|
+
for (const entry of acceptedSteers.splice(0)) {
|
|
568
|
+
input.onSteerOutcome?.(entry.request, { state: "failed", message: unconsumedSteerReason(entry.request.mode) });
|
|
569
|
+
}
|
|
570
|
+
};
|
|
532
571
|
const settle = (promptError: unknown, forced = false): void => {
|
|
533
572
|
if (settled) return;
|
|
534
573
|
settled = true;
|
|
574
|
+
failUnconsumedSteers();
|
|
535
575
|
const closed = finish();
|
|
536
576
|
const finalOutput = getFinalOutput(messages);
|
|
537
577
|
let finalError = error ?? assistantError;
|
|
@@ -611,6 +651,16 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
611
651
|
return;
|
|
612
652
|
}
|
|
613
653
|
session = created;
|
|
654
|
+
const steer = created.steer.bind(created);
|
|
655
|
+
const followUp = created.followUp.bind(created);
|
|
656
|
+
created.steer = async (text) => {
|
|
657
|
+
if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
|
|
658
|
+
return steer(text);
|
|
659
|
+
};
|
|
660
|
+
created.followUp = async (text) => {
|
|
661
|
+
if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
|
|
662
|
+
return followUp(text);
|
|
663
|
+
};
|
|
614
664
|
checkContinuation();
|
|
615
665
|
if (continuation && (created.modelId !== continuation.modelId || input.launch.capture.completionIntentContext?.()?.model?.api !== continuation.expected.api)) throw new Error("Read-only continuation model changed");
|
|
616
666
|
unsubscribe = created.subscribe(processEvent);
|
|
@@ -618,15 +668,22 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
618
668
|
input.registerSteer?.(async (request) => {
|
|
619
669
|
const text = formatSteerMessage(request);
|
|
620
670
|
const followUp = request.mode === "follow_up";
|
|
671
|
+
const accepted = { request, text };
|
|
672
|
+
acceptedSteers.push(accepted);
|
|
673
|
+
const queued: SteerDelivery = {
|
|
674
|
+
state: "queued",
|
|
675
|
+
deliveryStatus: "queued",
|
|
676
|
+
message: followUp ? "Pi queued the follow-up input." : "Pi accepted the steering input.",
|
|
677
|
+
};
|
|
621
678
|
try {
|
|
622
679
|
if (followUp) await created.followUp(text);
|
|
623
680
|
else await created.steer(text);
|
|
624
681
|
} catch (steerError) {
|
|
682
|
+
const index = acceptedSteers.indexOf(accepted);
|
|
683
|
+
if (index >= 0) acceptedSteers.splice(index, 1);
|
|
625
684
|
return { state: "failed", message: steerError instanceof Error ? steerError.message : String(steerError) };
|
|
626
685
|
}
|
|
627
|
-
return
|
|
628
|
-
? { state: "queued", deliveryStatus: "queued", message: "Pi queued the follow-up input." }
|
|
629
|
-
: { state: "delivered", deliveryStatus: "delivered", message: "Pi accepted the steering input." };
|
|
686
|
+
return queued;
|
|
630
687
|
});
|
|
631
688
|
if (interrupted || timedOut || stopped) abortChild();
|
|
632
689
|
checkContinuation();
|
|
@@ -32,6 +32,7 @@ import { getExternalJobProvider } from "../../api/external-job-provider.ts";
|
|
|
32
32
|
import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
|
|
33
33
|
import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
|
|
34
34
|
import { validHostStepNodes } from "../shared/host-step-status.ts";
|
|
35
|
+
import { workflowAsyncChildSteeringGuidance } from "../shared/workflow-async-child-guidance.ts";
|
|
35
36
|
|
|
36
37
|
interface RunStatusParams {
|
|
37
38
|
action?: string;
|
|
@@ -624,6 +625,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
624
625
|
const display = runStatusStepDisplayName(step);
|
|
625
626
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
626
627
|
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
|
|
628
|
+
if (status.mode === "workflow" && step.runId) lines.push(` Child run: ${step.runId}`);
|
|
627
629
|
const structuredOutputPreview = step.structuredOutput === undefined ? undefined : formatWorkflowJsonPreview(step.structuredOutput, 4_000);
|
|
628
630
|
if (structuredOutputPreview !== undefined) lines.push(` Structured output: ${structuredOutputPreview}`);
|
|
629
631
|
if (step.structuredOutputPath) lines.push(` Structured output path: ${step.structuredOutputPath}`);
|
|
@@ -688,6 +690,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
688
690
|
lines.push(`Steer live foreground child: subagent({ action: "steer", id: "${control.runId}", index: ${index}, message: "..." })`);
|
|
689
691
|
}
|
|
690
692
|
}
|
|
693
|
+
lines.push(...workflowAsyncChildSteeringGuidance(status, deps.state));
|
|
691
694
|
}
|
|
692
695
|
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
693
696
|
if (status.workflowReceiptPath) lines.push(`Workflow receipt: ${status.workflowReceiptPath}`);
|
|
@@ -21,8 +21,6 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
|
|
|
21
21
|
{ specifier: "@earendil-works/pi-coding-agent", pkg: "@earendil-works/pi-coding-agent", subpath: "." },
|
|
22
22
|
{ specifier: "@earendil-works/pi-agent-core", pkg: "@earendil-works/pi-agent-core", subpath: "." },
|
|
23
23
|
{ specifier: "@earendil-works/pi-agent-core/node", pkg: "@earendil-works/pi-agent-core", subpath: "./node" },
|
|
24
|
-
{ specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
|
|
25
|
-
{ specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
|
|
26
24
|
{ specifier: "@earendil-works/pi-tui", pkg: "@earendil-works/pi-tui", subpath: "." },
|
|
27
25
|
{ specifier: "@earendil-works/pi-ai", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
28
26
|
{ specifier: "@earendil-works/pi-ai/compat", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
@@ -33,6 +31,12 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
|
|
|
33
31
|
{ specifier: "typebox/value", pkg: "typebox", subpath: "./value" },
|
|
34
32
|
];
|
|
35
33
|
|
|
34
|
+
/** Public Pi manifests introduce chord in 0.85.0 (absent through 0.84.4). */
|
|
35
|
+
const CHORD_PEER_ALIASES = [
|
|
36
|
+
{ specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
|
|
37
|
+
{ specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
|
|
38
|
+
];
|
|
39
|
+
|
|
36
40
|
/** Experimental dependencies accidentally published in exactly Pi 0.85.0. */
|
|
37
41
|
const PI0850_PEER_ALIASES = [
|
|
38
42
|
{ specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
|
|
@@ -137,7 +141,11 @@ export function resolveHostPeerAliases(
|
|
|
137
141
|
const supplemental: string[] = [];
|
|
138
142
|
const hostManifest = readManifest(piPackageRoot);
|
|
139
143
|
const isPi0850 = hostManifest?.version === "0.85.0";
|
|
140
|
-
|
|
144
|
+
// Only known stable pre-chord versions may omit it. Unknown/prerelease
|
|
145
|
+
// hosts retain the required aliases, rather than hiding a broken install.
|
|
146
|
+
const stableVersion = typeof hostManifest?.version === "string" ? /^0\.(\d+)\.\d+$/.exec(hostManifest.version) : null;
|
|
147
|
+
const isPreChord = stableVersion !== null && Number(stableVersion[1]) < 85;
|
|
148
|
+
const required = [...HOST_PEER_ALIASES, ...(isPreChord ? [] : CHORD_PEER_ALIASES), ...(isPi0850 ? PI0850_PEER_ALIASES : [])];
|
|
141
149
|
for (const { specifier, pkg, subpath } of required) {
|
|
142
150
|
const packageDir = findPeerPackageDir(piPackageRoot, pkg, hostManifest?.name);
|
|
143
151
|
let target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
|
|
@@ -18,6 +18,7 @@ export interface RunnerChildLaunchContext {
|
|
|
18
18
|
runFanoutBudget?: BuildInProcessChildLaunchInput["runFanoutBudget"];
|
|
19
19
|
capabilityCeiling?: BuildInProcessChildLaunchInput["capabilityCeiling"];
|
|
20
20
|
inheritedChildRuntime?: InheritedChildRuntime;
|
|
21
|
+
hostAvailableBuiltins?: readonly string[];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChildLaunchContext, attempt: {
|
|
@@ -81,6 +82,7 @@ export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChil
|
|
|
81
82
|
thinkingCeiling: step.thinkingCeiling,
|
|
82
83
|
maxSubagentDepth: step.maxSubagentDepth,
|
|
83
84
|
inherited: ctx.inheritedChildRuntime,
|
|
85
|
+
hostAvailableBuiltins: ctx.hostAvailableBuiltins,
|
|
84
86
|
host: "runner",
|
|
85
87
|
});
|
|
86
88
|
}
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* the parent aliases that specifier (and the other host peer packages) to the
|
|
6
6
|
* installed pi package through `JITI_ALIAS` when it spawns the runner, see
|
|
7
7
|
* `runner-aliases.ts`. Tests replace the factory with a scripted one by
|
|
8
|
-
* naming a module in the runner config.
|
|
8
|
+
* naming a module in the runner config. The binary bootstrap supplies its
|
|
9
|
+
* embedded SDK through the default factory's existing loader option instead.
|
|
9
10
|
*/
|
|
10
11
|
import * as path from "node:path";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
12
|
-
import { createDefaultChildSessionFactory, type ChildSessionFactory } from "../shared/child-session.ts";
|
|
13
|
+
import { createDefaultChildSessionFactory, type ChildSessionFactory, type DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
|
|
13
14
|
|
|
14
15
|
export interface RunnerChildSessionConfig {
|
|
15
16
|
/** Test seam: module whose default export is a `ChildSessionFactory`, or a function returning one. */
|
|
@@ -20,8 +21,8 @@ function isChildSessionFactory(value: unknown): value is ChildSessionFactory {
|
|
|
20
21
|
return Boolean(value) && typeof value === "object" && typeof (value as ChildSessionFactory).create === "function" && typeof (value as ChildSessionFactory).dispose === "function";
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig): Promise<ChildSessionFactory> {
|
|
24
|
-
if (!config.childSessionFactoryModule) return createDefaultChildSessionFactory();
|
|
24
|
+
export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig, options?: DefaultChildSessionFactoryOptions): Promise<ChildSessionFactory> {
|
|
25
|
+
if (!config.childSessionFactoryModule) return createDefaultChildSessionFactory(options);
|
|
25
26
|
const loaded = await import(pathToFileURL(path.resolve(config.childSessionFactoryModule)).href) as { default?: unknown };
|
|
26
27
|
const candidate = typeof loaded.default === "function" ? (loaded.default as () => unknown)() : loaded.default;
|
|
27
28
|
if (!isChildSessionFactory(candidate)) {
|
|
@@ -54,6 +54,7 @@ export interface ScheduleRecord {
|
|
|
54
54
|
timeoutMs?: number;
|
|
55
55
|
paused: boolean;
|
|
56
56
|
sessionOnly?: boolean;
|
|
57
|
+
quiet?: boolean;
|
|
57
58
|
ownerSessionFile?: string;
|
|
58
59
|
createdAt: string;
|
|
59
60
|
updatedAt: string;
|
|
@@ -308,6 +309,7 @@ function parseSchedule(value: unknown, file: string): ScheduleRecord {
|
|
|
308
309
|
if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string") throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
|
|
309
310
|
} else throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
|
|
310
311
|
if (record.sessionOnly !== undefined && typeof record.sessionOnly !== "boolean") throw new Error(`Schedule record '${file}' has invalid sessionOnly.`);
|
|
312
|
+
if (record.quiet !== undefined && typeof record.quiet !== "boolean") throw new Error(`Schedule record '${file}' has invalid quiet.`);
|
|
311
313
|
if (record.sessionOnly === true && (typeof record.ownerSessionFile !== "string" || !record.ownerSessionFile.trim())) throw new Error(`Schedule record '${file}' is session-only but has no owner session file.`);
|
|
312
314
|
return { ...record, target: parseScheduleTarget(record.target, file) } as ScheduleRecord;
|
|
313
315
|
}
|
|
@@ -448,7 +450,7 @@ function sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget;
|
|
|
448
450
|
return { target: { workflowScript: params.workflowScript.trim(), ...(baseRef === undefined ? {} : { baseRef }) } };
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
|
|
453
|
+
function executionParams(schedule: ScheduleRecord, quiet = false): SubagentParamsLike {
|
|
452
454
|
return {
|
|
453
455
|
...schedule.target,
|
|
454
456
|
async: true,
|
|
@@ -456,7 +458,7 @@ function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
|
|
|
456
458
|
cwd: schedule.cwd,
|
|
457
459
|
mission: false,
|
|
458
460
|
// Scheduled fires have no operator watching, so completions must name the origin.
|
|
459
|
-
scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}) },
|
|
461
|
+
scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}), ...(quiet ? { quiet: true } : {}) },
|
|
460
462
|
...(schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }),
|
|
461
463
|
};
|
|
462
464
|
}
|
|
@@ -612,6 +614,8 @@ export class ScheduledRunManager {
|
|
|
612
614
|
if (params.catchUp !== undefined && params.catchUp !== "none" && params.catchUp !== "latest") return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
|
|
613
615
|
if (params.missionId !== undefined || params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined) return textResult("Mission attachment is deferred from this first schedule slice.", undefined, undefined, true);
|
|
614
616
|
if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year") return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
|
|
617
|
+
if (params.quiet !== undefined && typeof params.quiet !== "boolean") return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
618
|
+
if (at && params.quiet === true) return textResult("quiet is only supported for recurring schedules.", undefined, undefined, true);
|
|
615
619
|
const sessionOnly = params.sessionOnly === true;
|
|
616
620
|
if (sessionOnly && params.cwd !== undefined && !samePath(params.cwd, ctx.cwd)) return textResult("sessionOnly schedules cannot use an explicit cross-project cwd.", undefined, undefined, true);
|
|
617
621
|
const ownerSessionFile = sessionOnly ? ctx.sessionManager.getSessionFile() : undefined;
|
|
@@ -644,13 +648,14 @@ export class ScheduledRunManager {
|
|
|
644
648
|
...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
|
|
645
649
|
paused: false,
|
|
646
650
|
...(sessionOnly ? { sessionOnly: true, ownerSessionFile: path.resolve(ownerSessionFile!) } : {}),
|
|
651
|
+
...(trigger.kind === "interval" && params.quiet === true ? { quiet: true } : {}),
|
|
647
652
|
createdAt: timestamp(now),
|
|
648
653
|
updatedAt: timestamp(now),
|
|
649
654
|
};
|
|
650
655
|
store.write(schedule);
|
|
651
656
|
store.appendEvent(schedule, "schedule.created");
|
|
652
657
|
this.arm(schedule, store);
|
|
653
|
-
return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
|
|
658
|
+
return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nQuiet: ${schedule.quiet === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
|
|
654
659
|
}
|
|
655
660
|
|
|
656
661
|
private list(): AgentToolResult<Details> {
|
|
@@ -661,7 +666,7 @@ export class ScheduledRunManager {
|
|
|
661
666
|
|
|
662
667
|
private show(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
663
668
|
const schedule = this.resolve(params);
|
|
664
|
-
return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
|
|
669
|
+
return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Quiet: ${schedule.quiet === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
|
|
665
670
|
}
|
|
666
671
|
|
|
667
672
|
private history(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
@@ -689,7 +694,18 @@ export class ScheduledRunManager {
|
|
|
689
694
|
if (!scheduleBelongsToSession(schedule, context)) {
|
|
690
695
|
return textResult(`Skipped schedule ${schedule.id}: current session is not its owner.`, [schedule]);
|
|
691
696
|
}
|
|
692
|
-
|
|
697
|
+
if (params.quiet !== undefined && typeof params.quiet !== "boolean") return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
698
|
+
const run = await this.launch(store, schedule, this.now(), "manual", false, params.quiet === true);
|
|
699
|
+
const updated = store.get(schedule.id);
|
|
700
|
+
if (run.state === "running") {
|
|
701
|
+
const now = this.now();
|
|
702
|
+
if (updated.trigger.kind === "interval") updated.trigger.nextRunAt = timestamp(now + updated.trigger.everyMs);
|
|
703
|
+
else updated.trigger.nextRunAt = undefined;
|
|
704
|
+
updated.updatedAt = timestamp(now);
|
|
705
|
+
store.write(updated);
|
|
706
|
+
store.appendEvent(updated, "schedule.manual_satisfied");
|
|
707
|
+
this.arm(updated, store);
|
|
708
|
+
}
|
|
693
709
|
return textResult(`Manual schedule run ${run.id}: ${run.state}${run.asyncId ? ` (async ${run.asyncId})` : ""}.`, [store.get(schedule.id)], [run], run.state === "failed_launch");
|
|
694
710
|
}
|
|
695
711
|
|
|
@@ -708,8 +724,16 @@ export class ScheduledRunManager {
|
|
|
708
724
|
|
|
709
725
|
private remove(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
710
726
|
const schedule = this.resolve(params);
|
|
711
|
-
if (schedule.activeRunId) return textResult(`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`, [schedule], undefined, true);
|
|
712
727
|
const store = this.requireStore();
|
|
728
|
+
if (schedule.activeRunId) {
|
|
729
|
+
const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
|
|
730
|
+
let terminal = false;
|
|
731
|
+
if (run?.scheduleId === schedule.id && run.state === "running" && run.asyncId && run.asyncDir) {
|
|
732
|
+
const status = readJson(path.join(run.asyncDir, "status.json"), "async status") as Partial<AsyncStatus>;
|
|
733
|
+
terminal = status.runId === run.asyncId && typeof status.state === "string" && ["complete", "failed", "stopped", "rejected"].includes(status.state);
|
|
734
|
+
}
|
|
735
|
+
if (!terminal) return textResult(`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`, [schedule], undefined, true);
|
|
736
|
+
}
|
|
713
737
|
this.clearTimer(store, schedule.id);
|
|
714
738
|
store.appendEvent(schedule, "schedule.deleted");
|
|
715
739
|
store.delete(schedule.id);
|
|
@@ -807,8 +831,9 @@ export class ScheduledRunManager {
|
|
|
807
831
|
await this.launch(store, schedule, planned, "timer", true);
|
|
808
832
|
}
|
|
809
833
|
|
|
810
|
-
private async launch(store: ScheduleStore, schedule: ScheduleRecord, planned: number, dueReason: ScheduleRunRecord["dueReason"], advance: boolean): Promise<ScheduleRunRecord> {
|
|
834
|
+
private async launch(store: ScheduleStore, schedule: ScheduleRecord, planned: number, dueReason: ScheduleRunRecord["dueReason"], advance: boolean, quiet?: boolean): Promise<ScheduleRunRecord> {
|
|
811
835
|
const now = this.now();
|
|
836
|
+
const nextRunAtBeforeClaim = schedule.trigger.nextRunAt;
|
|
812
837
|
const run: ScheduleRunRecord = { schemaVersion: 1, id: this.randomId(), scheduleId: schedule.id, plannedAt: timestamp(planned), dueReason, state: "running", startedAt: timestamp(now) };
|
|
813
838
|
if (schedule.activeRunId) {
|
|
814
839
|
run.state = "skipped";
|
|
@@ -849,7 +874,7 @@ export class ScheduledRunManager {
|
|
|
849
874
|
store.write(schedule);
|
|
850
875
|
store.writeRun(schedule, run, "schedule.run.started");
|
|
851
876
|
try {
|
|
852
|
-
const result = await this.deps.launch(executionParams(schedule), this.requireContext(store), new AbortController().signal);
|
|
877
|
+
const result = await this.deps.launch(executionParams(schedule, dueReason === "manual" ? quiet === true : schedule.quiet === true), this.requireContext(store), new AbortController().signal);
|
|
853
878
|
const asyncId = result.details?.asyncId ?? result.details?.runId;
|
|
854
879
|
if (result.isError || !asyncId) throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
|
|
855
880
|
run.asyncId = asyncId;
|
|
@@ -862,12 +887,14 @@ export class ScheduledRunManager {
|
|
|
862
887
|
run.state = "failed_launch";
|
|
863
888
|
run.completedAt = timestamp(this.now());
|
|
864
889
|
run.error = error instanceof Error ? error.message : String(error);
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
890
|
+
const latest = store.get(schedule.id);
|
|
891
|
+
latest.activeRunId = undefined;
|
|
892
|
+
if (!advance && nextRunAtBeforeClaim) latest.trigger.nextRunAt = nextRunAtBeforeClaim;
|
|
893
|
+
latest.updatedAt = timestamp(this.now());
|
|
894
|
+
store.write(latest);
|
|
895
|
+
store.writeRun(latest, run, "schedule.run.failed");
|
|
869
896
|
fs.rmSync(lockPath, { force: true });
|
|
870
|
-
this.arm(
|
|
897
|
+
this.arm(latest, store);
|
|
871
898
|
return run;
|
|
872
899
|
}
|
|
873
900
|
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { readStatus } from "../../shared/utils.ts";
|
|
15
15
|
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
16
16
|
import { redactSecretValues } from "../shared/permissions.ts";
|
|
17
|
+
import type { SteerDeliveryMode } from "./control-channel.ts";
|
|
17
18
|
|
|
18
19
|
export const MAX_STEERING_REQUESTS = 20;
|
|
19
20
|
export const STEERING_MESSAGE_PREVIEW_LIMIT = 160;
|
|
@@ -22,6 +23,21 @@ export function steeringMessagePreview(message: string): string {
|
|
|
22
23
|
return previewDisplayText(redactSecretValues(message), STEERING_MESSAGE_PREVIEW_LIMIT);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** FIFO match of one accepted steer to an emitted user message; equal text claims the oldest entry. */
|
|
27
|
+
export function takeMatchingAcceptedSteer<T extends { text: string }>(accepted: T[], messageText: string): T | undefined {
|
|
28
|
+
const index = accepted.findIndex((entry) => entry.text === messageText);
|
|
29
|
+
if (index < 0) return undefined;
|
|
30
|
+
const [entry] = accepted.splice(index, 1);
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Settlement reason for one accepted request that never got a matching user `message_end`. */
|
|
35
|
+
export function unconsumedSteerReason(mode?: SteerDeliveryMode): string {
|
|
36
|
+
return mode === "follow_up"
|
|
37
|
+
? "child completed before consuming follow-up"
|
|
38
|
+
: "child completed before consuming steering";
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
export function steeringReceipt(message: string, receipt: string): string {
|
|
26
42
|
const preview = steeringMessagePreview(message);
|
|
27
43
|
const longestFence = Math.max(2, ...[...preview.matchAll(/`{3,}/g)].map((match) => match[0]!.length));
|
|
@@ -93,7 +109,9 @@ export function updateSteeringTarget(
|
|
|
93
109
|
if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
|
|
94
110
|
return target;
|
|
95
111
|
}
|
|
96
|
-
|
|
112
|
+
const wasPending = target.state === "routed" || target.state === "queued";
|
|
113
|
+
const nowPending = state === "routed" || state === "queued";
|
|
114
|
+
if (wasPending && !nowPending) status.pending = Math.max(0, status.pending - 1);
|
|
97
115
|
target.state = state;
|
|
98
116
|
if (state === "routed") target.routedAt = now;
|
|
99
117
|
if (state === "delivered") {
|
|
@@ -108,7 +126,7 @@ export function updateSteeringTarget(
|
|
|
108
126
|
if (state === "recovered") target.recoveredAt = now;
|
|
109
127
|
if (fields.reason) target.reason = fields.reason;
|
|
110
128
|
if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
|
|
111
|
-
incrementStateCount(status, state);
|
|
129
|
+
if (!wasPending || !nowPending) incrementStateCount(status, state);
|
|
112
130
|
return target;
|
|
113
131
|
}
|
|
114
132
|
|