pi-crew 0.11.4 → 0.11.6
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 +109 -0
- package/README.md +2 -2
- package/dist/index.mjs +498 -492
- package/docs/actions-reference.md +22 -0
- package/docs/dynamic-workflows.md +37 -0
- package/index.ts +9 -5
- package/package.json +18 -14
- package/scripts/analyze-run.mjs +101 -14
- package/skills/model-routing-context/SKILL.md +4 -3
- package/skills/real-test-pi-crew/SKILL.md +10 -7
- package/src/config/env-vars.ts +6 -0
- package/src/extension/crew-vibes/index.ts +29 -10
- package/src/extension/pi-api.ts +1 -0
- package/src/extension/registration/lifecycle-handlers.ts +65 -3
- package/src/extension/team-tool/dispatch/index.ts +4 -3
- package/src/extension/team-tool/dispatch/status.ts +7 -3
- package/src/extension/team-tool/inspect.ts +90 -0
- package/src/extension/team-tool/respond.ts +54 -0
- package/src/extension/team-tool/status.ts +10 -0
- package/src/extension/team-tool.ts +393 -3
- package/src/extension/webhook-notify.ts +23 -1
- package/src/observability/exporters/otlp-exporter.ts +93 -11
- package/src/prompt/prompt-runtime.ts +13 -4
- package/src/runtime/agent-control.ts +1 -15
- package/src/runtime/background-runner.ts +3 -0
- package/src/runtime/broker/crew-broker.ts +12 -103
- package/src/runtime/broker/protocol/request-parsers.ts +9 -1
- package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
- package/src/runtime/dispatch-batch.ts +3 -1
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
- package/src/runtime/live-session/live-agent-manager.ts +35 -0
- package/src/runtime/live-session/live-session-runtime.ts +34 -5
- package/src/runtime/mcp-proxy.ts +110 -7
- package/src/runtime/model/pi-args.ts +14 -11
- package/src/runtime/model/provider-extensions.ts +67 -1
- package/src/runtime/recovery/crash-recovery.ts +6 -1
- package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
- package/src/runtime/stale-reconciler.ts +55 -0
- package/src/runtime/task-output-context.ts +11 -1
- package/src/runtime/task-runner/child-executor.ts +130 -4
- package/src/runtime/task-runner/output-splitter.ts +48 -5
- package/src/runtime/task-runner/post-execution.ts +2 -2
- package/src/runtime/task-runner/pre-execution.ts +37 -24
- package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
- package/src/schema/team-tool-schema.ts +27 -1
- package/src/state/contracts.ts +16 -0
- package/src/state/crew-init.ts +114 -0
- package/src/state/stores/active-run-registry.ts +13 -6
- package/src/state/types.ts +0 -8
- package/src/ui/goal-flag.ts +44 -0
- package/src/ui/live-run-sidebar.ts +5 -1
- package/src/ui/pi-ui-compat.ts +20 -3
- package/src/ui/run-dashboard.ts +2 -1
- package/src/ui/widget/widget-model.ts +6 -2
- package/src/utils/ip-literal.ts +145 -0
- package/src/workflows/workflow-config.ts +6 -0
|
@@ -621,10 +621,19 @@ function findAskResponse(manifest: TeamRunManifest, questionId: string): Mailbox
|
|
|
621
621
|
* report — best-effort wait.resolve on EVERY terminal path (answered /
|
|
622
622
|
* timed-out / aborted). A rejected resolve leaves the task parked; the
|
|
623
623
|
* scheduler's TTL leak-guard (stale-reconciler) is the backstop, so the
|
|
624
|
-
* failure is logged, never thrown.
|
|
625
|
-
|
|
624
|
+
* failure is logged, never thrown.
|
|
625
|
+
* G5 (deep-review 2026-10-01): the terminal outcome rides the resolve frame
|
|
626
|
+
* so the broker can emit `task.respond_delivered` ONLY when the worker
|
|
627
|
+
* actually picked up the leader's mailbox response ("answered") — a
|
|
628
|
+
* timed-out/aborted park was NOT a delivery and must not masquerade as one. */
|
|
629
|
+
async function resolvePark(
|
|
630
|
+
client: AskBrokerClientSurface,
|
|
631
|
+
taskId: string,
|
|
632
|
+
questionId: string,
|
|
633
|
+
outcome: "answered" | "timed-out" | "aborted",
|
|
634
|
+
): Promise<void> {
|
|
626
635
|
try {
|
|
627
|
-
const res = await client.request("wait.resolve", { to: taskId, questionId });
|
|
636
|
+
const res = await client.request("wait.resolve", { to: taskId, questionId, outcome });
|
|
628
637
|
if (!res.ok) {
|
|
629
638
|
logInternalError(
|
|
630
639
|
"prompt-runtime.ask-wait-resolve",
|
|
@@ -774,7 +783,7 @@ export function createAskTool(deps: AskToolDeps = {}): AskToolDefinition {
|
|
|
774
783
|
}
|
|
775
784
|
const waitedMs = now() - startedAt;
|
|
776
785
|
// Terminal report (ADR item 8): best-effort un-park on EVERY path.
|
|
777
|
-
await resolvePark(client, taskId, questionId);
|
|
786
|
+
await resolvePark(client, taskId, questionId, terminal);
|
|
778
787
|
if (terminal === "answered" && answer) {
|
|
779
788
|
return {
|
|
780
789
|
content: [{ type: "text" as const, text: renderAskAnswer(questionId, answer.body) }],
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
3
|
-
import type {
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
3
|
import { appendTaskAttentionEvent } from "./attention-events.ts";
|
|
5
4
|
import { upsertCrewAgent } from "./crew-agent-records.ts";
|
|
6
5
|
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
@@ -200,16 +199,3 @@ function resetConsecutiveToolFailures(manifest: TeamRunManifest, agent: CrewAgen
|
|
|
200
199
|
};
|
|
201
200
|
upsertCrewAgent(manifest, updated);
|
|
202
201
|
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Reserve a control channel for a task before spawning its worker.
|
|
206
|
-
* This ensures cancel/steer requests can be queued immediately
|
|
207
|
-
* while the worker is still starting up.
|
|
208
|
-
*/
|
|
209
|
-
export function reserveControlChannel(taskId: string, runId: string): ControlReservation {
|
|
210
|
-
return {
|
|
211
|
-
reservedAt: new Date().toISOString(),
|
|
212
|
-
controllerId: `ctrl:${taskId}:${randomUUID()}`,
|
|
213
|
-
acceptsControlEvents: true,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
@@ -544,6 +544,9 @@ export async function executeBackgroundRun(
|
|
|
544
544
|
workflow: wf as import("../workflows/workflow-config.ts").DynamicWorkflowConfig,
|
|
545
545
|
signal,
|
|
546
546
|
tokenBudget: wf.maxTokenBudget,
|
|
547
|
+
// SDD-3 W-C G13: workflow-config override for the agent-call cap
|
|
548
|
+
// (unset → ctx default; a run is always bounded).
|
|
549
|
+
maxAgentCalls: wf.maxAgentCalls,
|
|
547
550
|
});
|
|
548
551
|
await saveRunManifestAsync(dwfResult.manifest);
|
|
549
552
|
earlyResult = dwfResult;
|
|
@@ -54,12 +54,12 @@ import {
|
|
|
54
54
|
isRequestObject,
|
|
55
55
|
parseMsgSendParams,
|
|
56
56
|
parseWaitRequestParams,
|
|
57
|
-
parseWaitResolveParams,
|
|
58
57
|
safeStringify,
|
|
59
58
|
WAIT_REQUEST_TIMEOUT_SEC_DEFAULT,
|
|
60
59
|
WAIT_REQUEST_TIMEOUT_SEC_MAX,
|
|
61
60
|
} from "./protocol/request-parsers.ts";
|
|
62
61
|
import { recordWaitPolicyRejection, waitAuthError } from "./protocol/wait-auth.ts";
|
|
62
|
+
import { handleWaitResolve } from "./protocol/wait-resolve.ts";
|
|
63
63
|
import { pushWaitingToForegroundWaiter } from "./wait-push.ts";
|
|
64
64
|
import { WaitStatusCache } from "./wait-status-cache.ts";
|
|
65
65
|
|
|
@@ -1883,110 +1883,19 @@ export class CrewBroker {
|
|
|
1883
1883
|
});
|
|
1884
1884
|
}
|
|
1885
1885
|
|
|
1886
|
-
/** WP-2/R2:
|
|
1887
|
-
*
|
|
1888
|
-
* auth + state transition + ask.answered event ONLY (ADR item 6/8):
|
|
1889
|
-
* answer DELIVERY is the mailbox respond path (step 6), not this
|
|
1890
|
-
* method. A questionId mismatch (or a task that is not parked) is
|
|
1891
|
-
* rejected WITHOUT clearing anything — fail-closed. */
|
|
1886
|
+
/** WP-2/R2/R3: wait.resolve handler lives in ./protocol/wait-resolve.ts
|
|
1887
|
+
* (moved out for the wc-gate M4 §5 — pure move, no behavior change). */
|
|
1892
1888
|
private async handleWaitResolve(conn: ServerConnection, id: string, params: unknown): Promise<void> {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const parsed = parseWaitResolveParams(params);
|
|
1903
|
-
if (!parsed) {
|
|
1904
|
-
this.sendError(conn, id, "bad-params", "wait.resolve: invalid params");
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
// Server-side identity enforcement (same rule as wait.request).
|
|
1908
|
-
if (parsed.to !== conn.taskId) {
|
|
1909
|
-
this.sendError(conn, id, "forbidden", "wait.resolve: 'to' must match the authenticated task");
|
|
1910
|
-
return;
|
|
1911
|
-
}
|
|
1912
|
-
const cwd = this.options.cwd;
|
|
1913
|
-
if (!cwd) {
|
|
1914
|
-
this.sendError(conn, id, "no-manifest", "broker has no cwd configured");
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
let loaded: NonNullable<ReturnType<typeof loadRunManifestById>>;
|
|
1918
|
-
try {
|
|
1919
|
-
const l = loadRunManifestById(cwd, conn.runId);
|
|
1920
|
-
if (!l) {
|
|
1921
|
-
this.sendError(conn, id, "no-manifest", `run '${conn.runId}' not found`);
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
loaded = l;
|
|
1925
|
-
} catch (err) {
|
|
1926
|
-
this.sendError(conn, id, "no-manifest", (err as Error).message);
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
if (this.options.waitMethodsEnabled !== true) {
|
|
1930
|
-
recordWaitPolicyRejection(loaded.manifest, conn.taskId, "wait.resolve");
|
|
1931
|
-
this.sendError(
|
|
1932
|
-
conn,
|
|
1933
|
-
id,
|
|
1934
|
-
"policy-disabled",
|
|
1935
|
-
"wait.resolve is disabled: broker.waitMethodsEnabled=false (fail-closed; policy.action recorded in events.jsonl)",
|
|
1936
|
-
);
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
const runId = conn.runId;
|
|
1940
|
-
const taskId = conn.taskId;
|
|
1941
|
-
const outcome = withRunLockSync(loaded.manifest, () => {
|
|
1942
|
-
const fresh = loadRunManifestById(loaded.manifest.cwd, runId);
|
|
1943
|
-
if (!fresh) return { code: "no-manifest" as const, message: `run '${runId}' not found` };
|
|
1944
|
-
const task = fresh.tasks.find((t) => t.id === taskId);
|
|
1945
|
-
if (!task) return { code: "no-task" as const, message: `task '${taskId}' not found` };
|
|
1946
|
-
if (task.status !== "waiting" || task.waiting?.questionId !== parsed.questionId) {
|
|
1947
|
-
return {
|
|
1948
|
-
code: "bad-params" as const,
|
|
1949
|
-
message: `wait.resolve: no parked question '${parsed.questionId}' on task '${taskId}'`,
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
const updatedTasks = fresh.tasks.map((t) => (t.id === taskId ? { ...t, status: "running" as const, waiting: undefined } : t));
|
|
1953
|
-
// waitState is a single run-level slot: clear it ONLY when it points
|
|
1954
|
-
// at this exact question — never clobber another task's newer park.
|
|
1955
|
-
const waitState = fresh.manifest.waitState;
|
|
1956
|
-
const clearWaitState = waitState !== undefined && waitState.taskId === taskId && waitState.questionId === parsed.questionId;
|
|
1957
|
-
const updatedManifest = {
|
|
1958
|
-
...fresh.manifest,
|
|
1959
|
-
...(clearWaitState ? { waitState: undefined } : {}),
|
|
1960
|
-
updatedAt: new Date().toISOString(),
|
|
1961
|
-
};
|
|
1962
|
-
saveRunTasks(updatedManifest, updatedTasks);
|
|
1963
|
-
saveRunManifest(updatedManifest);
|
|
1964
|
-
return { code: "ok" as const, message: "" };
|
|
1965
|
-
});
|
|
1966
|
-
if (outcome.code !== "ok") {
|
|
1967
|
-
this.sendError(conn, id, outcome.code, outcome.message);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
const eventsPath = loaded.manifest.eventsPath;
|
|
1971
|
-
void appendEventAsync(eventsPath, {
|
|
1972
|
-
type: "ask.answered",
|
|
1973
|
-
runId,
|
|
1974
|
-
taskId,
|
|
1975
|
-
message: `Question ${parsed.questionId} answered; task resumed.`,
|
|
1976
|
-
data: { questionId: parsed.questionId },
|
|
1977
|
-
}).catch((err) =>
|
|
1978
|
-
logInternalError("crew-broker.wait.ask-answered-event", err instanceof Error ? err : new Error(String(err)), `runId=${runId}`),
|
|
1979
|
-
);
|
|
1980
|
-
void appendEventAsync(eventsPath, {
|
|
1981
|
-
type: "task.resumed",
|
|
1982
|
-
runId,
|
|
1983
|
-
taskId,
|
|
1984
|
-
message: `Task resumed after ask answer (question ${parsed.questionId}).`,
|
|
1985
|
-
data: { questionId: parsed.questionId },
|
|
1986
|
-
}).catch((err) =>
|
|
1987
|
-
logInternalError("crew-broker.wait.task-resumed-event", err instanceof Error ? err : new Error(String(err)), `runId=${runId}`),
|
|
1889
|
+
await handleWaitResolve(
|
|
1890
|
+
conn,
|
|
1891
|
+
id,
|
|
1892
|
+
params,
|
|
1893
|
+
{
|
|
1894
|
+
sendError: (c, i, code, msg) => this.sendError(c, i, code, msg),
|
|
1895
|
+
sendResult: (c, i, r) => this.sendResult(c, i, r),
|
|
1896
|
+
},
|
|
1897
|
+
{ cwd: this.options.cwd, waitMethodsEnabled: this.options.waitMethodsEnabled },
|
|
1988
1898
|
);
|
|
1989
|
-
this.sendResult(conn, id, { ok: true, taskId, questionId: parsed.questionId });
|
|
1990
1899
|
}
|
|
1991
1900
|
}
|
|
1992
1901
|
|
|
@@ -172,6 +172,13 @@ export function parseWaitRequestParams(value: unknown): WaitRequestParams | unde
|
|
|
172
172
|
export interface WaitResolveParams {
|
|
173
173
|
to: string;
|
|
174
174
|
questionId: string;
|
|
175
|
+
/** G5 (deep-review 2026-10-01): the parked worker's terminal outcome —
|
|
176
|
+
* "answered" = it PICKED UP the leader's mailbox response for this
|
|
177
|
+
* questionId (the only outcome that triggers the broker's
|
|
178
|
+
* task.respond_delivered ack event); "timed-out"/"aborted" resolve the
|
|
179
|
+
* park WITHOUT a delivery claim. Optional + fail-closed: legacy workers
|
|
180
|
+
* that omit it never claim a delivery (no event is emitted). */
|
|
181
|
+
outcome?: "answered" | "timed-out" | "aborted";
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
export function parseWaitResolveParams(value: unknown): WaitResolveParams | undefined {
|
|
@@ -179,5 +186,6 @@ export function parseWaitResolveParams(value: unknown): WaitResolveParams | unde
|
|
|
179
186
|
const v = value as Record<string, unknown>;
|
|
180
187
|
if (typeof v.to !== "string" || v.to.length === 0 || v.to.length > 256) return undefined;
|
|
181
188
|
if (typeof v.questionId !== "string" || v.questionId.length === 0 || v.questionId.length > 128) return undefined;
|
|
182
|
-
|
|
189
|
+
if (v.outcome !== undefined && v.outcome !== "answered" && v.outcome !== "timed-out" && v.outcome !== "aborted") return undefined;
|
|
190
|
+
return { to: v.to, questionId: v.questionId, ...(v.outcome !== undefined ? { outcome: v.outcome } : {}) };
|
|
183
191
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-resolve.ts — WP-2/R2 `wait.resolve` broker handler.
|
|
3
|
+
*
|
|
4
|
+
* Moved from crew-broker.ts (wc-gate M4 §5, GH #61 release audit 2026-10-02)
|
|
5
|
+
* — pure move, no behavior change. Terminal report of the parked `ask` tool:
|
|
6
|
+
* flips the task waiting→running and clears the park coordination state.
|
|
7
|
+
* Scoped to auth + state transition + ask.answered/task.resumed events +
|
|
8
|
+
* the G5 respond_delivered ack ONLY (ADR item 6/8): answer DELIVERY is the
|
|
9
|
+
* mailbox respond path (step 6), not this handler. A questionId mismatch
|
|
10
|
+
* (or a task that is not parked) is rejected WITHOUT clearing anything —
|
|
11
|
+
* fail-closed.
|
|
12
|
+
*
|
|
13
|
+
* Top-level function so the class method is a 1-line delegation
|
|
14
|
+
* (same pattern as msg-inbox.ts).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
18
|
+
import { appendEventAsync } from "../../../state/event-log/event-log.ts";
|
|
19
|
+
import { loadRunManifestById, saveRunManifest, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
20
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
21
|
+
import type { ServerConnection } from "./connection-state.ts";
|
|
22
|
+
import { parseWaitResolveParams } from "./request-parsers.ts";
|
|
23
|
+
import { recordWaitPolicyRejection } from "./wait-auth.ts";
|
|
24
|
+
|
|
25
|
+
export interface WaitResolveHelpers {
|
|
26
|
+
sendError(conn: ServerConnection, id: string, code: string, message: string): void;
|
|
27
|
+
sendResult(conn: ServerConnection, id: string, result: unknown): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WaitResolveOptions {
|
|
31
|
+
/** Broker cwd (run state root). Undefined → no-manifest error. */
|
|
32
|
+
cwd?: string;
|
|
33
|
+
/** Fail-closed gate: wait.* methods disabled unless explicitly true. */
|
|
34
|
+
waitMethodsEnabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleWaitResolve(
|
|
38
|
+
conn: ServerConnection,
|
|
39
|
+
id: string,
|
|
40
|
+
params: unknown,
|
|
41
|
+
helpers: WaitResolveHelpers,
|
|
42
|
+
options: WaitResolveOptions,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
if (!conn.runId || !conn.taskId) {
|
|
45
|
+
helpers.sendError(conn, id, "auth", "not authed");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseWaitResolveParams(params);
|
|
49
|
+
if (!parsed) {
|
|
50
|
+
helpers.sendError(conn, id, "bad-params", "wait.resolve: invalid params");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Server-side identity enforcement (same rule as wait.request).
|
|
54
|
+
if (parsed.to !== conn.taskId) {
|
|
55
|
+
helpers.sendError(conn, id, "forbidden", "wait.resolve: 'to' must match the authenticated task");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!options.cwd) {
|
|
59
|
+
helpers.sendError(conn, id, "no-manifest", "broker has no cwd configured");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let loaded: NonNullable<ReturnType<typeof loadRunManifestById>>;
|
|
63
|
+
try {
|
|
64
|
+
const l = loadRunManifestById(options.cwd, conn.runId);
|
|
65
|
+
if (!l) {
|
|
66
|
+
helpers.sendError(conn, id, "no-manifest", `run '${conn.runId}' not found`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
loaded = l;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
helpers.sendError(conn, id, "no-manifest", (err as Error).message);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.waitMethodsEnabled !== true) {
|
|
75
|
+
recordWaitPolicyRejection(loaded.manifest, conn.taskId, "wait.resolve");
|
|
76
|
+
helpers.sendError(
|
|
77
|
+
conn,
|
|
78
|
+
id,
|
|
79
|
+
"policy-disabled",
|
|
80
|
+
"wait.resolve is disabled: broker.waitMethodsEnabled=false (fail-closed; policy.action recorded in events.jsonl)",
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const runId = conn.runId;
|
|
85
|
+
const taskId = conn.taskId;
|
|
86
|
+
const outcome = withRunLockSync(loaded.manifest, () => {
|
|
87
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, runId);
|
|
88
|
+
if (!fresh) return { code: "no-manifest" as const, message: `run '${runId}' not found` };
|
|
89
|
+
const task = fresh.tasks.find((t) => t.id === taskId);
|
|
90
|
+
if (!task) return { code: "no-task" as const, message: `task '${taskId}' not found` };
|
|
91
|
+
if (task.status !== "waiting" || task.waiting?.questionId !== parsed.questionId) {
|
|
92
|
+
return {
|
|
93
|
+
code: "bad-params" as const,
|
|
94
|
+
message: `wait.resolve: no parked question '${parsed.questionId}' on task '${taskId}'`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const updatedTasks = fresh.tasks.map((t) => (t.id === taskId ? { ...t, status: "running" as const, waiting: undefined } : t));
|
|
98
|
+
// G5 (deep-review 2026-10-01): capture the liveness pid (heartbeat-first,
|
|
99
|
+
// checkpoint-fallback — the same source stale-reconciler uses) before
|
|
100
|
+
// the flip, so respond_delivered records the same worker-pid evidence
|
|
101
|
+
// its respond_missed twin does root-side.
|
|
102
|
+
const workerPid = task.heartbeat?.pid ?? task.checkpoint?.childPid;
|
|
103
|
+
// waitState is a single run-level slot: clear it ONLY when it points
|
|
104
|
+
// at this exact question — never clobber another task's newer park.
|
|
105
|
+
const waitState = fresh.manifest.waitState;
|
|
106
|
+
const clearWaitState = waitState !== undefined && waitState.taskId === taskId && waitState.questionId === parsed.questionId;
|
|
107
|
+
const updatedManifest = {
|
|
108
|
+
...fresh.manifest,
|
|
109
|
+
...(clearWaitState ? { waitState: undefined } : {}),
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
saveRunTasks(updatedManifest, updatedTasks);
|
|
113
|
+
saveRunManifest(updatedManifest);
|
|
114
|
+
return { code: "ok" as const, message: "", workerPid };
|
|
115
|
+
});
|
|
116
|
+
if (outcome.code !== "ok") {
|
|
117
|
+
helpers.sendError(conn, id, outcome.code, outcome.message);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const eventsPath = loaded.manifest.eventsPath;
|
|
121
|
+
void appendEventAsync(eventsPath, {
|
|
122
|
+
type: "ask.answered",
|
|
123
|
+
runId,
|
|
124
|
+
taskId,
|
|
125
|
+
message: `Question ${parsed.questionId} answered; task resumed.`,
|
|
126
|
+
data: { questionId: parsed.questionId },
|
|
127
|
+
}).catch((err) =>
|
|
128
|
+
logInternalError("crew-broker.wait.ask-answered-event", err instanceof Error ? err : new Error(String(err)), `runId=${runId}`),
|
|
129
|
+
);
|
|
130
|
+
void appendEventAsync(eventsPath, {
|
|
131
|
+
type: "task.resumed",
|
|
132
|
+
runId,
|
|
133
|
+
taskId,
|
|
134
|
+
message: `Task resumed after ask answer (question ${parsed.questionId}).`,
|
|
135
|
+
data: { questionId: parsed.questionId },
|
|
136
|
+
}).catch((err) =>
|
|
137
|
+
logInternalError("crew-broker.wait.task-resumed-event", err instanceof Error ? err : new Error(String(err)), `runId=${runId}`),
|
|
138
|
+
);
|
|
139
|
+
// G5 (deep-review 2026-10-01): respond-delivery ack. wait.resolve fires on
|
|
140
|
+
// EVERY terminal path (answered/timed-out/aborted) — only the worker-claimed
|
|
141
|
+
// outcome "answered" is a DELIVERY: the parked ask tool found the
|
|
142
|
+
// questionId-tagged mailbox response and is resolving the park WITH the
|
|
143
|
+
// answer in hand (its findAskResponse exact-equality match). A
|
|
144
|
+
// timed-out/aborted resolve — or a legacy worker that sends no outcome —
|
|
145
|
+
// never claims a delivery: fail-closed, no event. This ack is the leader's
|
|
146
|
+
// only confirmation that a mailbox respond reached a live worker instead
|
|
147
|
+
// of falling into the void; the root-side respond_missed twin covers the
|
|
148
|
+
// dead-worker branch at write time (extension/team-tool/respond.ts).
|
|
149
|
+
if (parsed.outcome === "answered") {
|
|
150
|
+
void appendEventAsync(eventsPath, {
|
|
151
|
+
type: "task.respond_delivered",
|
|
152
|
+
runId,
|
|
153
|
+
taskId,
|
|
154
|
+
message: `Leader respond delivered: worker acknowledged the mailbox response for question ${parsed.questionId}.`,
|
|
155
|
+
data: {
|
|
156
|
+
questionId: parsed.questionId,
|
|
157
|
+
...(outcome.workerPid !== undefined ? { workerPid: outcome.workerPid } : {}),
|
|
158
|
+
},
|
|
159
|
+
}).catch((err) =>
|
|
160
|
+
logInternalError(
|
|
161
|
+
"crew-broker.wait.respond-delivered-event",
|
|
162
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
163
|
+
`runId=${runId}`,
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
helpers.sendResult(conn, id, { ok: true, taskId, questionId: parsed.questionId });
|
|
168
|
+
}
|
|
@@ -357,10 +357,11 @@ export function prepareSpawnContext(
|
|
|
357
357
|
// F4/S-1: the RAW (unredacted) snapshot must NEVER land in artifactsRoot —
|
|
358
358
|
// point it at a temp dir; the worker reads it then writeArtifact()
|
|
359
359
|
// (redact+atomic) is the ONLY writer into artifactsRoot.
|
|
360
|
-
// R3-1:
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// spawn). createSafeTempDir auto-tracks the dir for
|
|
360
|
+
// R3-1: since G3 (spill-always) buildPiWorkerArgs creates a tempDir for
|
|
361
|
+
// EVERY spawn (task.md) — the `??` guard stays as pure defense against
|
|
362
|
+
// future call shapes (resolveRealContainedPath(undefined) would
|
|
363
|
+
// TypeError and crash spawn). createSafeTempDir auto-tracks the dir for
|
|
364
|
+
// cleanupAllTrackedTempDirs.
|
|
364
365
|
const scratchTempDir = built.tempDir ?? createSafeTempDir(getPiTempBase(), "pi-crew-scratchpad-");
|
|
365
366
|
built.env.PI_CREW_SCRATCHPAD_SNAPSHOT = resolveRealContainedPath(scratchTempDir, `${input.agentId}.snapshot.json`);
|
|
366
367
|
// Phase 2 crash-resume (D1/D1b/D2): locate the latest snapshot artifact of a
|
|
@@ -625,6 +625,8 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
|
|
|
625
625
|
const { workflow, input, runtimeKind, runController } = ctx;
|
|
626
626
|
|
|
627
627
|
// 2.2 caller migration: batch progress is high-frequency informational (M7 wire).
|
|
628
|
+
// .catch REQUIRED — buffered-flush rejections reject queued promises (see
|
|
629
|
+
// child-executor.ts:530 note, CI 2026-10-03).
|
|
628
630
|
void appendEventBuffered(ctx.manifest.eventsPath, {
|
|
629
631
|
type: "task.progress",
|
|
630
632
|
runId: ctx.manifest.runId,
|
|
@@ -640,7 +642,7 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
|
|
|
640
642
|
defaultConcurrency: concurrency.defaultConcurrency,
|
|
641
643
|
concurrencyReason: approvalPending ? `${concurrency.reason};plan-approval-read-only` : concurrency.reason,
|
|
642
644
|
},
|
|
643
|
-
});
|
|
645
|
+
}).catch((error) => logInternalError("dispatch-batch.ready-batch-progress", error, `runId=${ctx.manifest.runId}`));
|
|
644
646
|
// Execute before_task_start hooks for the batch — P1-10: run hooks in
|
|
645
647
|
// parallel (each may be a subprocess), then apply skip mutations in order.
|
|
646
648
|
const beforeTaskStartReports = await Promise.all(
|