pi-crew 0.10.3 → 0.10.4
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/AGENTS.md +2 -1
- package/README.md +5 -1
- package/dist/index.mjs +10685 -6882
- package/docs/architecture.md +4 -4
- package/docs/commands-reference.md +3 -0
- package/docs/publishing.md +15 -3
- package/install.mjs +90 -39
- package/package.json +8 -3
- package/scripts/README.md +4 -3
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +1 -0
- package/skills/real-test-pi-crew/SKILL.md +153 -6
- package/src/config/migration-validator.ts +113 -0
- package/src/extension/cross-extension-rpc.ts +3 -7
- package/src/extension/register.ts +13 -0
- package/src/extension/registration/observability.ts +3 -7
- package/src/extension/registration/subagent-tools.ts +3 -7
- package/src/extension/registration/team-tool.ts +3 -7
- package/src/extension/registration/ui.ts +3 -8
- package/src/extension/registration/viewers.ts +3 -10
- package/src/extension/team-manager-command.ts +3 -7
- package/src/extension/team-tool/api/agent-control.ts +17 -10
- package/src/extension/team-tool/api/heartbeat.ts +4 -3
- package/src/extension/team-tool/api/mailbox.ts +33 -20
- package/src/extension/team-tool/api/plan-approval.ts +5 -5
- package/src/extension/team-tool/api/task-claims.ts +8 -7
- package/src/extension/team-tool/cancel.ts +6 -0
- package/src/extension/team-tool/handle-settings.ts +4 -1
- package/src/extension/team-tool/run.ts +3 -7
- package/src/extension/team-tool/status.ts +5 -0
- package/src/extension/team-tool.ts +6 -14
- package/src/hooks/registry.ts +3 -0
- package/src/prompt/scratchpad-lifecycle.ts +3 -3
- package/src/runtime/background-runner.ts +30 -35
- package/src/runtime/broker/crew-broker.ts +112 -441
- package/src/runtime/broker/delegate/delegate-event.ts +37 -0
- package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
- package/src/runtime/broker/protocol/connection-state.ts +103 -0
- package/src/runtime/broker/protocol/events-replay.ts +68 -0
- package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
- package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
- package/src/runtime/broker/protocol/request-parsers.ts +175 -0
- package/src/runtime/broker/protocol/wait-auth.ts +46 -0
- package/src/runtime/child-pi/child-pi.ts +15 -0
- package/src/runtime/finalize-run.ts +15 -7
- package/src/runtime/foreground-control.ts +19 -6
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
- package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
- package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
- package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
- package/src/runtime/model/pi-args.ts +6 -1
- package/src/runtime/plan-replan.ts +3 -0
- package/src/runtime/stale-reconciler.ts +28 -3
- package/src/runtime/supervisor-contact.ts +3 -0
- package/src/runtime/task-runner/child-executor.ts +33 -0
- package/src/runtime/team-runner.ts +3 -3
- package/src/state/stores/ownership-map.ts +5 -4
- package/src/state/stores/plan-store.ts +12 -0
- package/src/state/stores/state-store.ts +5 -0
- package/src/ui/powerbar-publisher.ts +3 -7
- package/src/ui/run-action-dispatcher.ts +7 -10
- package/src/ui/settings-overlay.ts +4 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migration-validator.ts — WI-5.6 (M5 spec §5).
|
|
3
|
+
*
|
|
4
|
+
* Validates a parsed config against deprecated/removed env keys and
|
|
5
|
+
* returns an advisory list (not a failure). Per spec acceptance:
|
|
6
|
+
* "Migration validator: test case cũ config + key deprecated →
|
|
7
|
+
* warning không fail."
|
|
8
|
+
*
|
|
9
|
+
* Key principles (additive-only, READ-ONLY — the validator never mutates
|
|
10
|
+
* env or config):
|
|
11
|
+
* - Deprecated: emit warning (severity "deprecated"), keep the value,
|
|
12
|
+
* do not fail.
|
|
13
|
+
* - Removed/dead/reverted: emit warning (severity "removed"), do not
|
|
14
|
+
* fail, do NOT delete the key (cleanup is a separate concern).
|
|
15
|
+
* - Keys not in the registry are not scanned (registry-driven scan).
|
|
16
|
+
*
|
|
17
|
+
* Returns an advisory list; never throws.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { CREW_ENV_VARS } from "./env-vars.ts";
|
|
21
|
+
|
|
22
|
+
export interface ValidationWarning {
|
|
23
|
+
scope: "env-var" | "config-key";
|
|
24
|
+
name: string;
|
|
25
|
+
severity: "deprecated" | "removed";
|
|
26
|
+
message: string;
|
|
27
|
+
/** Optional policy note from the registry entry. */
|
|
28
|
+
policy?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EnvValidationResult {
|
|
32
|
+
warnings: ValidationWarning[];
|
|
33
|
+
/** Fast-bool for callers that don't need detail. */
|
|
34
|
+
hasWarnings: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Validate `process.env` against the registry. Returns warnings; never
|
|
38
|
+
* throws. */
|
|
39
|
+
export function validateEnv(env: NodeJS.ProcessEnv = process.env): EnvValidationResult {
|
|
40
|
+
const warnings: ValidationWarning[] = [];
|
|
41
|
+
if (!env || typeof env !== "object") return { warnings, hasWarnings: false };
|
|
42
|
+
for (const [name, spec] of Object.entries(CREW_ENV_VARS)) {
|
|
43
|
+
let value: string | undefined;
|
|
44
|
+
try {
|
|
45
|
+
value = (env as Record<string, string | undefined>)[name];
|
|
46
|
+
} catch {
|
|
47
|
+
value = undefined;
|
|
48
|
+
}
|
|
49
|
+
if (value === undefined) continue;
|
|
50
|
+
if (spec.deprecated !== undefined) {
|
|
51
|
+
// Distinguish DEAD/REMOVED vs DEPRECATED-but-working.
|
|
52
|
+
const lower = spec.deprecated.toLowerCase();
|
|
53
|
+
if (lower === "dead" || lower === "removed" || lower.startsWith("reverted")) {
|
|
54
|
+
warnings.push({
|
|
55
|
+
scope: "env-var",
|
|
56
|
+
name,
|
|
57
|
+
severity: "removed",
|
|
58
|
+
message: `${name} is ${spec.deprecated}; setting it has no effect`,
|
|
59
|
+
policy: spec.deprecated,
|
|
60
|
+
});
|
|
61
|
+
// Note: we DO NOT delete env[name] here; that's a separate
|
|
62
|
+
// cleanup concern. The validator is read-only.
|
|
63
|
+
} else {
|
|
64
|
+
warnings.push({
|
|
65
|
+
scope: "env-var",
|
|
66
|
+
name,
|
|
67
|
+
severity: "deprecated",
|
|
68
|
+
message: `${name} is deprecated (${spec.deprecated}); prefer the canonical name`,
|
|
69
|
+
policy: spec.deprecated,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { warnings, hasWarnings: warnings.length > 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validate a parsed config object against the env-var registry.
|
|
78
|
+
* Useful for: a config file that hardcodes a removed key. */
|
|
79
|
+
export function validateConfigAgainstEnvRegistry(config: Record<string, unknown>): EnvValidationResult {
|
|
80
|
+
const warnings: ValidationWarning[] = [];
|
|
81
|
+
if (!config || typeof config !== "object") return { warnings, hasWarnings: false };
|
|
82
|
+
for (const [name, spec] of Object.entries(CREW_ENV_VARS)) {
|
|
83
|
+
let present = false;
|
|
84
|
+
try {
|
|
85
|
+
present = name in config;
|
|
86
|
+
} catch {
|
|
87
|
+
present = false;
|
|
88
|
+
}
|
|
89
|
+
if (present) {
|
|
90
|
+
if (spec.deprecated !== undefined) {
|
|
91
|
+
const lower = spec.deprecated.toLowerCase();
|
|
92
|
+
if (lower === "dead" || lower === "removed" || lower.startsWith("reverted")) {
|
|
93
|
+
warnings.push({
|
|
94
|
+
scope: "config-key",
|
|
95
|
+
name,
|
|
96
|
+
severity: "removed",
|
|
97
|
+
message: `${name} config key is ${spec.deprecated}`,
|
|
98
|
+
policy: spec.deprecated,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
warnings.push({
|
|
102
|
+
scope: "config-key",
|
|
103
|
+
name,
|
|
104
|
+
severity: "deprecated",
|
|
105
|
+
message: `${name} config key is deprecated (${spec.deprecated})`,
|
|
106
|
+
policy: spec.deprecated,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { warnings, hasWarnings: warnings.length > 0 };
|
|
113
|
+
}
|
|
@@ -5,17 +5,13 @@ import { withHmacVerification } from "./rpc-hmac.ts";
|
|
|
5
5
|
// Lazy-loaded to avoid pulling team-tool.ts (and its entire runtime chain) into module load.
|
|
6
6
|
import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
|
|
7
7
|
|
|
8
|
-
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
9
8
|
async function handleTeamTool(
|
|
10
9
|
params: Parameters<typeof HandleTeamToolFn>[0],
|
|
11
10
|
ctx: Parameters<typeof HandleTeamToolFn>[1],
|
|
12
11
|
): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
17
|
-
}
|
|
18
|
-
return _cachedHandleTeamTool(params, ctx);
|
|
12
|
+
// LAZY: avoid pulling team-tool.ts (and its entire runtime chain) into module load.
|
|
13
|
+
const mod = await import("./team-tool.ts");
|
|
14
|
+
return mod.handleTeamTool(params, ctx);
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-session/live-control-realtime.ts";
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { loadConfig } from "../config/config.ts";
|
|
21
|
+
import { validateEnv } from "../config/migration-validator.ts";
|
|
21
22
|
import { startRuntimeWarmup } from "../runtime/model/runtime-warmup.ts";
|
|
22
23
|
import { primePeerDep } from "../runtime/peer-dep.ts";
|
|
23
24
|
import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
|
|
@@ -60,6 +61,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
60
61
|
// full crash chain — /crew-view mid-run crash).
|
|
61
62
|
installChildProcessAbortShield();
|
|
62
63
|
|
|
64
|
+
// WI-5.6 wiring (review remediation 2026-09-10): surface deprecated /
|
|
65
|
+
// removed / reverted PI_CREW_* env keys as ONE consolidated startup
|
|
66
|
+
// warning — warn, never fail (spec M5 acceptance). validateEnv is a pure
|
|
67
|
+
// scanner over the env-registry (env-vars.ts) and never throws.
|
|
68
|
+
const envWarnings = validateEnv(process.env);
|
|
69
|
+
if (envWarnings.warnings.length > 0) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[pi-crew] ${envWarnings.warnings.length} deprecated env var(s) in use:\n` +
|
|
72
|
+
envWarnings.warnings.map((w) => ` ${w.name}: ${w.message}`).join("\n"),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
startRuntimeWarmup();
|
|
64
77
|
primePeerDep().catch(() => undefined);
|
|
65
78
|
deployBundledThemes();
|
|
@@ -75,14 +75,10 @@ export interface ObservabilityDeps {
|
|
|
75
75
|
}>;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
let _cachedOTLPExporter: OTLPExporterCtor | undefined;
|
|
79
78
|
async function importOTLPExporter(): Promise<OTLPExporterCtor> {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
_cachedOTLPExporter = mod.OTLPExporter as unknown as OTLPExporterCtor;
|
|
84
|
-
}
|
|
85
|
-
return _cachedOTLPExporter;
|
|
79
|
+
// LAZY: opt-in OTLP metric export — load only when otlp.enabled=true.
|
|
80
|
+
const mod = await import("../../observability/exporters/otlp-exporter.ts");
|
|
81
|
+
return mod.OTLPExporter as unknown as OTLPExporterCtor;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
/**
|
|
@@ -6,17 +6,13 @@ import { withSessionId } from "../team-tool/context.ts";
|
|
|
6
6
|
// Lazy-loaded: team-tool.ts pulls in entire runtime chain.
|
|
7
7
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
8
8
|
|
|
9
|
-
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
10
9
|
async function handleTeamTool(
|
|
11
10
|
params: Parameters<typeof HandleTeamToolFn>[0],
|
|
12
11
|
ctx: Parameters<typeof HandleTeamToolFn>[1],
|
|
13
12
|
): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
18
|
-
}
|
|
19
|
-
return _cachedHandleTeamTool(params, ctx);
|
|
13
|
+
// LAZY: team-tool.ts pulls in entire runtime chain.
|
|
14
|
+
const mod = await import("../team-tool.ts");
|
|
15
|
+
return mod.handleTeamTool(params, ctx);
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -18,17 +18,13 @@ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
|
18
18
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
19
19
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
20
20
|
|
|
21
|
-
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
22
21
|
async function handleTeamTool(
|
|
23
22
|
params: Parameters<typeof HandleTeamToolFn>[0],
|
|
24
23
|
ctx: Parameters<typeof HandleTeamToolFn>[1],
|
|
25
24
|
): Promise<ReturnType<typeof HandleTeamToolFn>> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
30
|
-
}
|
|
31
|
-
return _cachedHandleTeamTool(params, ctx);
|
|
25
|
+
// LAZY: team-tool.ts imports many modules — defer until first use.
|
|
26
|
+
const mod = await import("../team-tool.ts");
|
|
27
|
+
return mod.handleTeamTool(params, ctx);
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
@@ -27,15 +27,10 @@ import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
|
27
27
|
import { stopCrewWidget, updateCrewWidget } from "../../ui/widget/index.ts";
|
|
28
28
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
29
29
|
|
|
30
|
-
/** Cached live-run-sidebar constructor (lazy-loaded on first overlay open). */
|
|
31
|
-
let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
|
|
32
30
|
async function importLiveRunSidebar(): Promise<typeof LiveRunSidebarType> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_cachedLiveRunSidebar = mod.LiveRunSidebar;
|
|
37
|
-
}
|
|
38
|
-
return _cachedLiveRunSidebar;
|
|
31
|
+
// LAZY: defer LiveRunSidebar import until the user opens a sidebar overlay.
|
|
32
|
+
const mod = await import("../../ui/live-run-sidebar.ts");
|
|
33
|
+
return mod.LiveRunSidebar;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
/** Mutable state owned by register.ts. */
|
|
@@ -8,17 +8,10 @@ import { asCrewTheme } from "../../ui/theme-adapter.ts";
|
|
|
8
8
|
// Lazy-loaded: DurableTranscriptViewer is 658ms — only needed for /crew transcript command
|
|
9
9
|
import type { DurableTranscriptViewer as DurableTranscriptViewerType } from "../../ui/transcript-viewer.ts";
|
|
10
10
|
|
|
11
|
-
let _cachedViewer: typeof DurableTranscriptViewerType | undefined;
|
|
12
|
-
let _viewerPromise: Promise<typeof DurableTranscriptViewerType> | undefined;
|
|
13
11
|
async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
_cachedViewer = mod.DurableTranscriptViewer;
|
|
18
|
-
return mod.DurableTranscriptViewer;
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
return _viewerPromise;
|
|
12
|
+
// LAZY: DurableTranscriptViewer is 658ms — only needed for /crew transcript.
|
|
13
|
+
const mod = await import("../../ui/transcript-viewer.ts");
|
|
14
|
+
return mod.DurableTranscriptViewer;
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
export async function selectAgentTask(
|
|
@@ -3,17 +3,13 @@ import { listRuns } from "./run-index.ts";
|
|
|
3
3
|
// Lazy-loaded: team-tool.ts pulls in entire runtime chain.
|
|
4
4
|
import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
|
|
5
5
|
|
|
6
|
-
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
7
6
|
async function handleTeamTool(
|
|
8
7
|
params: Parameters<typeof HandleTeamToolFn>[0],
|
|
9
8
|
ctx: Parameters<typeof HandleTeamToolFn>[1],
|
|
10
9
|
): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
15
|
-
}
|
|
16
|
-
return _cachedHandleTeamTool(params, ctx);
|
|
10
|
+
// LAZY: team-tool.ts pulls in the entire runtime chain.
|
|
11
|
+
const mod = await import("./team-tool.ts");
|
|
12
|
+
return mod.handleTeamTool(params, ctx);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
import { isToolError, textFromToolResult } from "./tool-result.ts";
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
appendSteeringMessage,
|
|
23
23
|
appendSteeringMessageAsync,
|
|
24
24
|
} from "../../../state/coordination/mailbox.ts";
|
|
25
|
-
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
25
|
+
import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
|
|
26
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
26
27
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
27
28
|
|
|
28
29
|
export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
|
|
@@ -54,13 +55,19 @@ export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
|
|
|
54
55
|
priority: "normal",
|
|
55
56
|
data: { source: "nudge-agent" },
|
|
56
57
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
// Read-your-writes (CI 2026-09-11, phase8 integration): the nudge caller
|
|
59
|
+
// reads events.jsonl synchronously right after dispatch — sync appendEvent.
|
|
60
|
+
try {
|
|
61
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
62
|
+
type: "agent.nudged",
|
|
63
|
+
runId: loaded.manifest.runId,
|
|
64
|
+
taskId: agent.taskId,
|
|
65
|
+
message: messageText,
|
|
66
|
+
data: { agentId: agent.id, mailboxMessageId: message.id },
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
logInternalError("api.agent-control.append", e, "type=agent.nudged");
|
|
70
|
+
}
|
|
64
71
|
ctx.events?.emit?.("crew.mailbox.message", {
|
|
65
72
|
runId: loaded.manifest.runId,
|
|
66
73
|
id: message.id,
|
|
@@ -314,7 +321,7 @@ export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
|
|
|
314
321
|
: undefined;
|
|
315
322
|
publishLiveControlRealtime(request);
|
|
316
323
|
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
317
|
-
|
|
324
|
+
appendEventBuffered(loaded.manifest.eventsPath, {
|
|
318
325
|
type: "agent.control.queued",
|
|
319
326
|
runId: loaded.manifest.runId,
|
|
320
327
|
taskId: agent.taskId,
|
|
@@ -324,7 +331,7 @@ export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
|
|
|
324
331
|
mailboxMessageId: mailboxMessage?.id,
|
|
325
332
|
realtime: true,
|
|
326
333
|
},
|
|
327
|
-
});
|
|
334
|
+
}).catch((e) => logInternalError("api.agent-control.buffered", e, "type=agent.control.queued"));
|
|
328
335
|
return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
|
|
329
336
|
action: "api",
|
|
330
337
|
status: "ok",
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
|
|
9
9
|
import { isTerminalTaskStatus } from "../../../state/contracts.ts";
|
|
10
10
|
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { appendEventBuffered } from "../../../state/event-log/event-log.ts";
|
|
12
12
|
import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
13
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
13
14
|
import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
|
|
14
15
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
15
16
|
|
|
@@ -79,12 +80,12 @@ export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
|
79
80
|
);
|
|
80
81
|
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? { ...item, heartbeat } : item));
|
|
81
82
|
saveRunTasks(fresh.manifest, tasks);
|
|
82
|
-
|
|
83
|
+
appendEventBuffered(fresh.manifest.eventsPath, {
|
|
83
84
|
type: "worker.heartbeat",
|
|
84
85
|
runId: fresh.manifest.runId,
|
|
85
86
|
taskId: freshTask.id,
|
|
86
87
|
data: { ...heartbeat },
|
|
87
|
-
});
|
|
88
|
+
}).catch((e) => logInternalError("api.heartbeat.buffered", e, "type=worker.heartbeat"));
|
|
88
89
|
return result(JSON.stringify(heartbeat, null, 2), {
|
|
89
90
|
action: "api",
|
|
90
91
|
status: "ok",
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
readMailboxMessage,
|
|
17
17
|
validateMailbox,
|
|
18
18
|
} from "../../../state/coordination/mailbox.ts";
|
|
19
|
-
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
19
|
+
import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
|
|
20
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
20
21
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
21
22
|
|
|
22
23
|
export const handleReadMailbox: ApiOperationHandler = (hctx) => {
|
|
@@ -129,11 +130,11 @@ export const handleSendMessage: ApiOperationHandler = (hctx) => {
|
|
|
129
130
|
// run-lock callback. Sync append (byte-identical to pre-extract
|
|
130
131
|
// api.ts) so consumers reading eventsPath immediately after the
|
|
131
132
|
// call see the event — async fire-and-forget would race.
|
|
132
|
-
|
|
133
|
+
appendEventBuffered(loaded.manifest.eventsPath, {
|
|
133
134
|
type: "mailbox.message",
|
|
134
135
|
runId: loaded.manifest.runId,
|
|
135
136
|
data: { id: message.id, direction, from, to },
|
|
136
|
-
});
|
|
137
|
+
}).catch((e) => logInternalError("api.mailbox.buffered", e, "type=mailbox.message"));
|
|
137
138
|
ctx.events?.emit?.("crew.mailbox.message", {
|
|
138
139
|
runId: loaded.manifest.runId,
|
|
139
140
|
id: message.id,
|
|
@@ -185,26 +186,38 @@ export const handleAckMessage: ApiOperationHandler = (hctx) => {
|
|
|
185
186
|
return withRunLockSync(loaded.manifest, () => {
|
|
186
187
|
const message = readMailboxMessage(loaded.manifest, messageId);
|
|
187
188
|
const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
|
|
189
|
+
// Read-your-writes (CI 2026-09-11, phase4 integration): the ack API
|
|
190
|
+
// returns and callers/tests read events.jsonl synchronously right after —
|
|
191
|
+
// buffered append flushes later (bufferMs window), so the events were
|
|
192
|
+
// missing at read. Sync appendEvent (base behavior, b6eba80f pattern).
|
|
193
|
+
try {
|
|
194
194
|
appendEvent(loaded.manifest.eventsPath, {
|
|
195
|
-
type: "
|
|
195
|
+
type: "mailbox.acknowledged",
|
|
196
196
|
runId: loaded.manifest.runId,
|
|
197
|
-
|
|
198
|
-
data: {
|
|
199
|
-
requestId: message.data.requestId,
|
|
200
|
-
messageId,
|
|
201
|
-
batchId: message.data.batchId,
|
|
202
|
-
partial: message.data.partial,
|
|
203
|
-
acknowledgedAt: delivery.updatedAt,
|
|
204
|
-
acknowledgedBy: "leader",
|
|
205
|
-
},
|
|
206
|
-
metadata: { provenance: "api" },
|
|
197
|
+
data: { messageId },
|
|
207
198
|
});
|
|
199
|
+
} catch (e) {
|
|
200
|
+
logInternalError("api.mailbox.append", e, "type=mailbox.acknowledged");
|
|
201
|
+
}
|
|
202
|
+
if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
|
|
203
|
+
try {
|
|
204
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
205
|
+
type: "agent.group_join.acknowledged",
|
|
206
|
+
runId: loaded.manifest.runId,
|
|
207
|
+
message: "Group join delivery acknowledged via mailbox ack.",
|
|
208
|
+
data: {
|
|
209
|
+
requestId: message.data.requestId,
|
|
210
|
+
messageId,
|
|
211
|
+
batchId: message.data.batchId,
|
|
212
|
+
partial: message.data.partial,
|
|
213
|
+
acknowledgedAt: delivery.updatedAt,
|
|
214
|
+
acknowledgedBy: "leader",
|
|
215
|
+
},
|
|
216
|
+
metadata: { provenance: "api" },
|
|
217
|
+
});
|
|
218
|
+
} catch (e) {
|
|
219
|
+
logInternalError("api.mailbox.append", e, "type=agent.group_join.acknowledged");
|
|
220
|
+
}
|
|
208
221
|
}
|
|
209
222
|
ctx.events?.emit?.("crew.mailbox.acknowledged", {
|
|
210
223
|
runId: loaded.manifest.runId,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { terminateLiveAgentsForRun } from "../../../runtime/live-session/live-agent-manager.ts";
|
|
9
9
|
import { currentCrewRole, permissionForRole } from "../../../runtime/role-permission.ts";
|
|
10
10
|
import { withRunLock } from "../../../state/coordination/locks.ts";
|
|
11
|
-
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
11
|
+
import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
|
|
12
12
|
import { getCurrentPlanRecord, setPlanApproval } from "../../../state/stores/plan-store.ts";
|
|
13
13
|
import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../../../state/stores/state-store.ts";
|
|
14
14
|
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
@@ -68,13 +68,13 @@ export const handleApprovePlan: ApiOperationHandler = async (hctx) => {
|
|
|
68
68
|
// plan id+version. Pre-v2 runs without a PlanRecord skip silently.
|
|
69
69
|
const currentRecord = getCurrentPlanRecord(manifest);
|
|
70
70
|
if (currentRecord) setPlanApproval(manifest, { status: "approved", planVersion: currentRecord.version, by: "api" });
|
|
71
|
-
|
|
71
|
+
appendEventBuffered(manifest.eventsPath, {
|
|
72
72
|
type: "plan.approved",
|
|
73
73
|
runId: manifest.runId,
|
|
74
74
|
taskId: approval.planTaskId,
|
|
75
75
|
message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.",
|
|
76
76
|
metadata: { provenance: "api" },
|
|
77
|
-
});
|
|
77
|
+
}).catch((e) => logInternalError("api.plan-approval.buffered", e, "type=plan.approved"));
|
|
78
78
|
return result(JSON.stringify(manifest.planApproval, null, 2), {
|
|
79
79
|
action: "api",
|
|
80
80
|
status: "ok",
|
|
@@ -150,13 +150,13 @@ export const handleCancelPlan: ApiOperationHandler = async (hctx) => {
|
|
|
150
150
|
const denyRecord = getCurrentPlanRecord(manifest);
|
|
151
151
|
if (denyRecord) setPlanApproval(manifest, { status: "rejected", planVersion: denyRecord.version, by: "api" });
|
|
152
152
|
saveRunTasks(manifest, tasks);
|
|
153
|
-
|
|
153
|
+
appendEventBuffered(manifest.eventsPath, {
|
|
154
154
|
type: "plan.cancelled",
|
|
155
155
|
runId: manifest.runId,
|
|
156
156
|
taskId: approval.planTaskId,
|
|
157
157
|
message: "Adaptive implementation plan was cancelled.",
|
|
158
158
|
metadata: { provenance: "api" },
|
|
159
|
-
});
|
|
159
|
+
}).catch((e) => logInternalError("api.plan-approval.buffered", e, "type=plan.cancelled"));
|
|
160
160
|
manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
|
|
161
161
|
void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch((error) =>
|
|
162
162
|
logInternalError("team-tool.cancel-plan.terminate", error, `runId=${manifest.runId}`),
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
import { canTransitionTaskStatus, isTeamTaskStatus } from "../../../state/contracts.ts";
|
|
17
17
|
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
18
18
|
import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../../state/coordination/task-claims.ts";
|
|
19
|
-
import {
|
|
19
|
+
import { appendEventBuffered } from "../../../state/event-log/event-log.ts";
|
|
20
20
|
import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
21
21
|
import type { TeamTaskState } from "../../../state/types.ts";
|
|
22
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
22
23
|
import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
|
|
23
24
|
import type { ApiHandlerContext, ApiOperationHandler } from "./handler-context.ts";
|
|
24
25
|
|
|
@@ -92,7 +93,7 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
|
|
|
92
93
|
const updatedTask = claimTask(freshTask, owner);
|
|
93
94
|
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
94
95
|
saveRunTasks(fresh.manifest, tasks);
|
|
95
|
-
|
|
96
|
+
appendEventBuffered(fresh.manifest.eventsPath, {
|
|
96
97
|
type: "task.claimed",
|
|
97
98
|
runId: fresh.manifest.runId,
|
|
98
99
|
taskId: freshTask.id,
|
|
@@ -101,7 +102,7 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
|
|
|
101
102
|
token: "[REDACTED]",
|
|
102
103
|
leasedUntil: updatedTask.claim?.leasedUntil,
|
|
103
104
|
},
|
|
104
|
-
});
|
|
105
|
+
}).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.claimed"));
|
|
105
106
|
return result(JSON.stringify(updatedTask.claim, null, 2), {
|
|
106
107
|
action: "api",
|
|
107
108
|
status: "ok",
|
|
@@ -162,12 +163,12 @@ export const handleReleaseTaskClaim: ApiOperationHandler = (ctx) => {
|
|
|
162
163
|
const updatedTask = releaseTaskClaim(freshTask, owner, token);
|
|
163
164
|
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
164
165
|
saveRunTasks(fresh.manifest, tasks);
|
|
165
|
-
|
|
166
|
+
appendEventBuffered(fresh.manifest.eventsPath, {
|
|
166
167
|
type: "task.claim_released",
|
|
167
168
|
runId: fresh.manifest.runId,
|
|
168
169
|
taskId: freshTask.id,
|
|
169
170
|
data: { owner },
|
|
170
|
-
});
|
|
171
|
+
}).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.claim_released"));
|
|
171
172
|
return result(JSON.stringify(updatedTask, null, 2), {
|
|
172
173
|
action: "api",
|
|
173
174
|
status: "ok",
|
|
@@ -241,12 +242,12 @@ export const handleTransitionTaskStatus: ApiOperationHandler = (ctx) => {
|
|
|
241
242
|
const updatedTask = transitionClaimedTaskStatus(freshTask, owner, token, to);
|
|
242
243
|
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
243
244
|
saveRunTasks(fresh.manifest, tasks);
|
|
244
|
-
|
|
245
|
+
appendEventBuffered(fresh.manifest.eventsPath, {
|
|
245
246
|
type: "task.status_transitioned",
|
|
246
247
|
runId: fresh.manifest.runId,
|
|
247
248
|
taskId: freshTask.id,
|
|
248
249
|
data: { owner, status: to },
|
|
249
|
-
});
|
|
250
|
+
}).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.status_transitioned"));
|
|
250
251
|
return result(JSON.stringify(updatedTask, null, 2), {
|
|
251
252
|
action: "api",
|
|
252
253
|
status: "ok",
|
|
@@ -392,6 +392,12 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
392
392
|
}
|
|
393
393
|
ctx.abortForegroundRun?.(fresh.manifest.runId);
|
|
394
394
|
for (const taskId of abortResult.abortedIds) {
|
|
395
|
+
// REVIEW FIX (2026-09-10): reverted M2b buffered conversion. Although
|
|
396
|
+
// task.cancelled is a TERMINAL type (buffered terminal path bypasses
|
|
397
|
+
// the 20ms buffer), that path still resolves via a microtask chain
|
|
398
|
+
// (flushPromise.then(appendEvent)) — NOT same-tick. Cancel tests and
|
|
399
|
+
// the immediately-following updateRunStatus reader need same-tick
|
|
400
|
+
// durability, so plain sync appendEvent (base behavior).
|
|
395
401
|
appendEvent(fresh.manifest.eventsPath, {
|
|
396
402
|
type: "task.cancelled",
|
|
397
403
|
runId: fresh.manifest.runId,
|
|
@@ -37,7 +37,10 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
37
37
|
"ui.dashboardPlacement": "center",
|
|
38
38
|
"ui.dashboardWidth": 72,
|
|
39
39
|
"ui.autoOpenDashboard": false,
|
|
40
|
-
|
|
40
|
+
// G17 sync (2026-09-10 review): canonical DEFAULT_UI.widgetPlacement =
|
|
41
|
+
// "bottom" (defaults.ts / install.mjs / project-init) — was drifted
|
|
42
|
+
// "aboveEditor" in this duplicated EFFECTIVE_DEFAULTS map.
|
|
43
|
+
"ui.widgetPlacement": "bottom",
|
|
41
44
|
"autonomous.enabled": true,
|
|
42
45
|
"autonomous.injectPolicy": true,
|
|
43
46
|
"autonomous.preferAsyncForLongTasks": false,
|
|
@@ -18,14 +18,10 @@ import { logInternalError } from "../../utils/internal-error.ts";
|
|
|
18
18
|
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
19
19
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
20
20
|
|
|
21
|
-
let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
|
|
22
21
|
async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
_cachedExecuteTeamRun = mod.executeTeamRun;
|
|
27
|
-
}
|
|
28
|
-
return _cachedExecuteTeamRun(...args);
|
|
22
|
+
// LAZY: heavy runtime — defer 1.4s import cost until team run actually executes.
|
|
23
|
+
const mod = await import("../../runtime/team-runner.ts");
|
|
24
|
+
return mod.executeTeamRun(...args);
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
@@ -93,6 +93,8 @@ export function transitionStaleAsyncUnderLock(
|
|
|
93
93
|
// async.stale (2026-08-10): sync append (byte-identical to pre-extract
|
|
94
94
|
// api.ts) so callers reading eventsPath immediately after status see
|
|
95
95
|
// the event — async fire-and-forget would race.
|
|
96
|
+
// REVIEW FIX (2026-09-10): reverted the M2b buffered conversion — the
|
|
97
|
+
// same-poll read-back contract below requires same-tick durability.
|
|
96
98
|
appendEvent(failed.eventsPath, {
|
|
97
99
|
type: "async.stale",
|
|
98
100
|
runId: failed.runId,
|
|
@@ -192,6 +194,9 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
192
194
|
const timedOut = ack === "pending" && ackTimeoutMs !== undefined && Number.isFinite(ageMs) && ageMs > ackTimeoutMs;
|
|
193
195
|
if (timedOut && !ackTimeoutRequestIds.has(requestId)) {
|
|
194
196
|
// ack_timeout: sync append (byte-identical to pre-extract api.ts).
|
|
197
|
+
// REVIEW FIX (2026-09-10): reverted the M2b buffered conversion — the
|
|
198
|
+
// dedupe set below is rebuilt from readEventsCursor in THIS invocation,
|
|
199
|
+
// so a buffered write would re-emit duplicates on sub-20ms re-polls.
|
|
195
200
|
appendEvent(manifest.eventsPath, {
|
|
196
201
|
type: "agent.group_join.ack_timeout",
|
|
197
202
|
runId: manifest.runId,
|