pi-crew 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/agents/analyst.md +1 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/docs/next-upgrade-roadmap.md +733 -0
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +11 -9
- package/src/extension/registration/commands.ts +32 -25
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +8 -6
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +62 -43
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +33 -4
- package/src/extension/team-tool/context.ts +5 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +91 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +37 -17
- package/src/extension/team-tool/run.ts +52 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +8 -1
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +59 -53
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +457 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +38 -6
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +46 -25
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +76 -0
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +88 -87
- package/src/runtime/live-agent-manager.ts +103 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +309 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +22 -6
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +105 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +78 -15
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +8 -1
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +44 -2
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-config.ts +1 -0
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +4 -3
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +10 -2
- package/src/ui/snapshot-types.ts +2 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +4 -2
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
export interface ProgressEventSummary {
|
|
2
|
-
eventType: string;
|
|
3
|
-
currentTool?: string;
|
|
4
|
-
toolCount?: number;
|
|
5
|
-
tokens?: number;
|
|
6
|
-
turns?: number;
|
|
7
|
-
activityState?: string;
|
|
8
|
-
lastActivityAt?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ProgressEventCoalesceDecision {
|
|
12
|
-
shouldAppend: boolean;
|
|
13
|
-
reason: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProgressEventCoalesceInput {
|
|
17
|
-
previous?: ProgressEventSummary;
|
|
18
|
-
next: ProgressEventSummary;
|
|
19
|
-
nowMs: number;
|
|
20
|
-
lastAppendMs?: number;
|
|
21
|
-
minIntervalMs: number;
|
|
22
|
-
force?: boolean;
|
|
23
|
-
tokenThreshold?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_TOKEN_THRESHOLD = 256;
|
|
27
|
-
|
|
28
|
-
function numericIncrease(previous: number | undefined, next: number | undefined): number {
|
|
29
|
-
return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
|
|
33
|
-
if (input.force) return { shouldAppend: true, reason: "force" };
|
|
34
|
-
if (!input.previous) return { shouldAppend: true, reason: "first" };
|
|
35
|
-
if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
|
|
36
|
-
if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
|
|
37
|
-
if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
|
|
38
|
-
if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
|
|
39
|
-
const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
|
|
40
|
-
if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
|
|
41
|
-
if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
|
|
42
|
-
return { shouldAppend: false, reason: "coalesced" };
|
|
43
|
-
}
|
|
1
|
+
export interface ProgressEventSummary {
|
|
2
|
+
eventType: string;
|
|
3
|
+
currentTool?: string;
|
|
4
|
+
toolCount?: number;
|
|
5
|
+
tokens?: number;
|
|
6
|
+
turns?: number;
|
|
7
|
+
activityState?: string;
|
|
8
|
+
lastActivityAt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProgressEventCoalesceDecision {
|
|
12
|
+
shouldAppend: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProgressEventCoalesceInput {
|
|
17
|
+
previous?: ProgressEventSummary;
|
|
18
|
+
next: ProgressEventSummary;
|
|
19
|
+
nowMs: number;
|
|
20
|
+
lastAppendMs?: number;
|
|
21
|
+
minIntervalMs: number;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
tokenThreshold?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TOKEN_THRESHOLD = 256;
|
|
27
|
+
|
|
28
|
+
function numericIncrease(previous: number | undefined, next: number | undefined): number {
|
|
29
|
+
return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
|
|
33
|
+
if (input.force) return { shouldAppend: true, reason: "force" };
|
|
34
|
+
if (!input.previous) return { shouldAppend: true, reason: "first" };
|
|
35
|
+
if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
|
|
36
|
+
if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
|
|
37
|
+
if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
|
|
38
|
+
if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
|
|
39
|
+
const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
|
|
40
|
+
if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
|
|
41
|
+
if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
|
|
42
|
+
return { shouldAppend: false, reason: "coalesced" };
|
|
43
|
+
}
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import type { PolicyDecision, PolicyDecisionReason } from "../state/types.ts";
|
|
2
|
-
|
|
3
|
-
export type FailureScenario = "trust_prompt_unresolved" | "prompt_misdelivery" | "stale_branch" | "compile_red_cross_crate" | "mcp_handshake_failure" | "partial_plugin_startup" | "provider_failure" | "task_failed" | "worker_stale" | "green_unsatisfied";
|
|
4
|
-
export type RecoveryStep = "accept_trust_prompt" | "redirect_prompt_to_agent" | "rebase_branch" | "clean_build" | "retry_mcp_handshake" | "restart_plugin" | "restart_worker" | "rerun_task" | "collect_verification_evidence" | "escalate_to_human";
|
|
5
|
-
export type RecoveryResultState = "planned" | "skipped" | "escalation_required";
|
|
6
|
-
|
|
7
|
-
export interface RecoveryRecipe {
|
|
8
|
-
scenario: FailureScenario;
|
|
9
|
-
steps: RecoveryStep[];
|
|
10
|
-
maxAttempts: number;
|
|
11
|
-
escalationPolicy: "alert_human" | "log_and_continue" | "abort";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface RecoveryLedgerEntry {
|
|
15
|
-
scenario: FailureScenario;
|
|
16
|
-
taskId?: string;
|
|
17
|
-
decisionReason: PolicyDecisionReason;
|
|
18
|
-
attempt: number;
|
|
19
|
-
state: RecoveryResultState;
|
|
20
|
-
steps: RecoveryStep[];
|
|
21
|
-
message: string;
|
|
22
|
-
createdAt: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RecoveryLedger {
|
|
26
|
-
entries: RecoveryLedgerEntry[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function scenarioForPolicyReason(reason: PolicyDecisionReason): FailureScenario {
|
|
30
|
-
switch (reason) {
|
|
31
|
-
case "branch_stale": return "stale_branch";
|
|
32
|
-
case "worker_stale": return "worker_stale";
|
|
33
|
-
case "green_unsatisfied": return "green_unsatisfied";
|
|
34
|
-
case "task_failed": return "task_failed";
|
|
35
|
-
default: return "provider_failure";
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function recipeFor(scenario: FailureScenario): RecoveryRecipe {
|
|
40
|
-
switch (scenario) {
|
|
41
|
-
case "trust_prompt_unresolved": return { scenario, steps: ["accept_trust_prompt"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
42
|
-
case "prompt_misdelivery": return { scenario, steps: ["redirect_prompt_to_agent"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
43
|
-
case "stale_branch": return { scenario, steps: ["rebase_branch", "clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
44
|
-
case "compile_red_cross_crate": return { scenario, steps: ["clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
45
|
-
case "mcp_handshake_failure": return { scenario, steps: ["retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "abort" };
|
|
46
|
-
case "partial_plugin_startup": return { scenario, steps: ["restart_plugin", "retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "log_and_continue" };
|
|
47
|
-
case "worker_stale": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
48
|
-
case "green_unsatisfied": return { scenario, steps: ["collect_verification_evidence"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
49
|
-
case "task_failed": return { scenario, steps: ["rerun_task"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
50
|
-
case "provider_failure": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function buildRecoveryLedger(decisions: PolicyDecision[], previous: RecoveryLedger = { entries: [] }): RecoveryLedger {
|
|
55
|
-
const entries = [...previous.entries];
|
|
56
|
-
for (const item of decisions) {
|
|
57
|
-
if (!["retry", "escalate", "block"].includes(item.action)) continue;
|
|
58
|
-
const scenario = scenarioForPolicyReason(item.reason);
|
|
59
|
-
const recipe = recipeFor(scenario);
|
|
60
|
-
const priorAttempts = entries.filter((entry) => entry.scenario === scenario && entry.taskId === item.taskId).length;
|
|
61
|
-
const attempt = priorAttempts + 1;
|
|
62
|
-
entries.push({
|
|
63
|
-
scenario,
|
|
64
|
-
taskId: item.taskId,
|
|
65
|
-
decisionReason: item.reason,
|
|
66
|
-
attempt,
|
|
67
|
-
state: attempt <= recipe.maxAttempts && item.action !== "block" ? "planned" : "escalation_required",
|
|
68
|
-
steps: attempt <= recipe.maxAttempts ? recipe.steps : ["escalate_to_human"],
|
|
69
|
-
message: item.message,
|
|
70
|
-
createdAt: new Date().toISOString(),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return { entries };
|
|
74
|
-
}
|
|
1
|
+
import type { PolicyDecision, PolicyDecisionReason } from "../state/types.ts";
|
|
2
|
+
|
|
3
|
+
export type FailureScenario = "trust_prompt_unresolved" | "prompt_misdelivery" | "stale_branch" | "compile_red_cross_crate" | "mcp_handshake_failure" | "partial_plugin_startup" | "provider_failure" | "task_failed" | "worker_stale" | "green_unsatisfied";
|
|
4
|
+
export type RecoveryStep = "accept_trust_prompt" | "redirect_prompt_to_agent" | "rebase_branch" | "clean_build" | "retry_mcp_handshake" | "restart_plugin" | "restart_worker" | "rerun_task" | "collect_verification_evidence" | "escalate_to_human";
|
|
5
|
+
export type RecoveryResultState = "planned" | "skipped" | "escalation_required";
|
|
6
|
+
|
|
7
|
+
export interface RecoveryRecipe {
|
|
8
|
+
scenario: FailureScenario;
|
|
9
|
+
steps: RecoveryStep[];
|
|
10
|
+
maxAttempts: number;
|
|
11
|
+
escalationPolicy: "alert_human" | "log_and_continue" | "abort";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecoveryLedgerEntry {
|
|
15
|
+
scenario: FailureScenario;
|
|
16
|
+
taskId?: string;
|
|
17
|
+
decisionReason: PolicyDecisionReason;
|
|
18
|
+
attempt: number;
|
|
19
|
+
state: RecoveryResultState;
|
|
20
|
+
steps: RecoveryStep[];
|
|
21
|
+
message: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RecoveryLedger {
|
|
26
|
+
entries: RecoveryLedgerEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function scenarioForPolicyReason(reason: PolicyDecisionReason): FailureScenario {
|
|
30
|
+
switch (reason) {
|
|
31
|
+
case "branch_stale": return "stale_branch";
|
|
32
|
+
case "worker_stale": return "worker_stale";
|
|
33
|
+
case "green_unsatisfied": return "green_unsatisfied";
|
|
34
|
+
case "task_failed": return "task_failed";
|
|
35
|
+
default: return "provider_failure";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function recipeFor(scenario: FailureScenario): RecoveryRecipe {
|
|
40
|
+
switch (scenario) {
|
|
41
|
+
case "trust_prompt_unresolved": return { scenario, steps: ["accept_trust_prompt"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
42
|
+
case "prompt_misdelivery": return { scenario, steps: ["redirect_prompt_to_agent"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
43
|
+
case "stale_branch": return { scenario, steps: ["rebase_branch", "clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
44
|
+
case "compile_red_cross_crate": return { scenario, steps: ["clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
45
|
+
case "mcp_handshake_failure": return { scenario, steps: ["retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "abort" };
|
|
46
|
+
case "partial_plugin_startup": return { scenario, steps: ["restart_plugin", "retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "log_and_continue" };
|
|
47
|
+
case "worker_stale": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
48
|
+
case "green_unsatisfied": return { scenario, steps: ["collect_verification_evidence"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
49
|
+
case "task_failed": return { scenario, steps: ["rerun_task"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
50
|
+
case "provider_failure": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRecoveryLedger(decisions: PolicyDecision[], previous: RecoveryLedger = { entries: [] }): RecoveryLedger {
|
|
55
|
+
const entries = [...previous.entries];
|
|
56
|
+
for (const item of decisions) {
|
|
57
|
+
if (!["retry", "escalate", "block"].includes(item.action)) continue;
|
|
58
|
+
const scenario = scenarioForPolicyReason(item.reason);
|
|
59
|
+
const recipe = recipeFor(scenario);
|
|
60
|
+
const priorAttempts = entries.filter((entry) => entry.scenario === scenario && entry.taskId === item.taskId).length;
|
|
61
|
+
const attempt = priorAttempts + 1;
|
|
62
|
+
entries.push({
|
|
63
|
+
scenario,
|
|
64
|
+
taskId: item.taskId,
|
|
65
|
+
decisionReason: item.reason,
|
|
66
|
+
attempt,
|
|
67
|
+
state: attempt <= recipe.maxAttempts && item.action !== "block" ? "planned" : "escalation_required",
|
|
68
|
+
steps: attempt <= recipe.maxAttempts ? recipe.steps : ["escalate_to_human"],
|
|
69
|
+
message: item.message,
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { entries };
|
|
74
|
+
}
|
|
@@ -1,64 +1,81 @@
|
|
|
1
|
-
import { sleep } from "../utils/sleep.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
import { sleep } from "../utils/sleep.ts";
|
|
2
|
+
import { throwIfCancelled } from "./cancellation.ts";
|
|
3
|
+
|
|
4
|
+
export interface RetryPolicy {
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
backoffMs: number;
|
|
7
|
+
jitterRatio: number;
|
|
8
|
+
exponentialFactor: number;
|
|
9
|
+
retryableErrors?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RetryAttemptInfo {
|
|
13
|
+
attempt: number;
|
|
14
|
+
attemptId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RetryHooks {
|
|
18
|
+
onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number, info: RetryAttemptInfo) => void;
|
|
19
|
+
onRetryGivenUp?: (attempts: number, error: Error, info: RetryAttemptInfo) => void;
|
|
20
|
+
attemptId?: (attempt: number) => string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
|
|
25
|
+
|
|
26
|
+
function asError(error: unknown): Error {
|
|
27
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function globToRegex(pattern: string): RegExp {
|
|
31
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
32
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRetryable(error: Error, policy: RetryPolicy): boolean {
|
|
36
|
+
const patterns = policy.retryableErrors ?? [];
|
|
37
|
+
if (!patterns.length) return true;
|
|
38
|
+
return patterns.some((pattern) => globToRegex(pattern).test(error.message));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
|
|
42
|
+
const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
|
|
43
|
+
const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
|
|
44
|
+
return Math.max(0, base + jitter);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function retryAttemptInfo(attempt: number, hooks: RetryHooks): RetryAttemptInfo {
|
|
48
|
+
return { attempt, attemptId: hooks.attemptId?.(attempt) ?? `retry_attempt_${attempt}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeWithRetry<T>(fn: (attempt: number, info: RetryAttemptInfo) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
|
|
52
|
+
const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
|
|
53
|
+
let lastError: Error | undefined;
|
|
54
|
+
for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
|
|
55
|
+
throwIfCancelled(hooks.signal);
|
|
56
|
+
const info = retryAttemptInfo(attempt, hooks);
|
|
57
|
+
try {
|
|
58
|
+
return await fn(attempt, info);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
lastError = asError(error);
|
|
61
|
+
// Never retry if aborted — sleep() would immediately reject on every attempt.
|
|
62
|
+
if (hooks.signal?.aborted) {
|
|
63
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
|
|
67
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
const delay = calculateRetryDelay(attempt, normalized);
|
|
71
|
+
hooks.onAttemptFailed?.(attempt, lastError, delay, info);
|
|
72
|
+
try {
|
|
73
|
+
await sleep(delay, hooks.signal);
|
|
74
|
+
} catch (sleepError) {
|
|
75
|
+
if (hooks.signal?.aborted) throwIfCancelled(hooks.signal);
|
|
76
|
+
throw sleepError;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError ?? new Error("Retry failed without error.");
|
|
81
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
-
|
|
3
|
-
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
-
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
-
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
-
|
|
7
|
-
export interface PermissionCheckResult {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
mode: RolePermissionMode;
|
|
10
|
-
reason?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
-
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
-
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
-
return "workspace_write";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
-
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
-
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\b(?:npm|pnpm|yarn|bun)\s+(install|add|ci|remove)\b|\bgit\s+(commit|push|merge|rebase|reset|checkout|clean)\b/.test(command);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
-
const mode = permissionForRole(role);
|
|
26
|
-
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
-
return { allowed: true, mode };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
-
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
-
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
-
const mode = permissionForRole(role);
|
|
37
|
-
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
-
return { allowed: true, mode };
|
|
39
|
-
}
|
|
1
|
+
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
+
|
|
3
|
+
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
+
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
+
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
+
|
|
7
|
+
export interface PermissionCheckResult {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
mode: RolePermissionMode;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
+
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
+
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
+
return "workspace_write";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
+
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
+
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\b(?:npm|pnpm|yarn|bun)\s+(install|add|ci|remove)\b|\bgit\s+(commit|push|merge|rebase|reset|checkout|clean)\b/.test(command);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
+
const mode = permissionForRole(role);
|
|
26
|
+
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
+
return { allowed: true, mode };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
+
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
+
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
+
const mode = permissionForRole(role);
|
|
37
|
+
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
+
return { allowed: true, mode };
|
|
39
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
|
+
import type { RuntimeResolutionState } from "../state/types.ts";
|
|
2
3
|
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
4
|
|
|
4
5
|
export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
|
|
5
6
|
|
|
7
|
+
export type CrewRuntimeSafety = "trusted" | "explicit_dry_run" | "blocked";
|
|
8
|
+
|
|
6
9
|
export interface CrewRuntimeCapabilities {
|
|
7
10
|
kind: CrewRuntimeKind;
|
|
8
11
|
requestedMode: CrewRuntimeMode;
|
|
@@ -13,6 +16,19 @@ export interface CrewRuntimeCapabilities {
|
|
|
13
16
|
liveToolActivity: boolean;
|
|
14
17
|
transcript: boolean;
|
|
15
18
|
reason?: string;
|
|
19
|
+
safety: CrewRuntimeSafety;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runtimeResolutionState(runtime: CrewRuntimeCapabilities, resolvedAt = new Date().toISOString()): RuntimeResolutionState {
|
|
23
|
+
return {
|
|
24
|
+
kind: runtime.kind,
|
|
25
|
+
requestedMode: runtime.requestedMode,
|
|
26
|
+
safety: runtime.safety,
|
|
27
|
+
available: runtime.available,
|
|
28
|
+
...(runtime.fallback ? { fallback: runtime.fallback } : {}),
|
|
29
|
+
...(runtime.reason ? { reason: runtime.reason } : {}),
|
|
30
|
+
resolvedAt,
|
|
31
|
+
};
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJS.ProcessEnv = process.env): Promise<{ available: boolean; reason?: string }> {
|
|
@@ -52,8 +68,8 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
|
|
|
52
68
|
export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
|
|
53
69
|
const requestedMode = config.runtime?.mode ?? "auto";
|
|
54
70
|
const workersDisabled = config.executeWorkers === false || env.PI_CREW_EXECUTE_WORKERS === "0" || env.PI_TEAMS_EXECUTE_WORKERS === "0";
|
|
55
|
-
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
|
|
56
|
-
if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.");
|
|
71
|
+
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode, undefined, "explicit_dry_run");
|
|
72
|
+
if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.", "blocked");
|
|
57
73
|
if (requestedMode === "child-process") return childCaps(requestedMode);
|
|
58
74
|
if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
|
|
59
75
|
const live = await isLiveSessionRuntimeAvailable(1500, env);
|
|
@@ -64,14 +80,14 @@ export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.Proc
|
|
|
64
80
|
return childCaps(requestedMode);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|
|
68
|
-
return { kind: "scaffold", requestedMode, available:
|
|
83
|
+
function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string, safety: CrewRuntimeSafety = "explicit_dry_run"): CrewRuntimeCapabilities {
|
|
84
|
+
return { kind: "scaffold", requestedMode, available: safety !== "blocked", steer: false, resume: false, liveToolActivity: false, transcript: false, safety, ...(reason ? { reason } : {}) };
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
function childCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|
|
72
|
-
return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, ...(reason ? { reason } : {}) };
|
|
88
|
+
return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, safety: "trusted", ...(reason ? { reason } : {}) };
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
function liveCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
|
|
76
|
-
return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true };
|
|
92
|
+
return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true, safety: "trusted" };
|
|
77
93
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Try to register a cleanup function with Pi's session resource cleanup API (v0.72+).
|
|
6
|
-
* Falls back to returning undefined if the API is not available.
|
|
7
|
-
*
|
|
8
|
-
* The returned function (if defined) can be called to unregister the cleanup.
|
|
9
|
-
*/
|
|
10
|
-
export function tryRegisterSessionCleanup(pi: ExtensionAPI, cleanup: () => void): (() => void) | undefined {
|
|
11
|
-
const api = pi as unknown as Record<string, unknown>;
|
|
12
|
-
const registerFn = api["registerSessionResourceCleanup"];
|
|
13
|
-
if (typeof registerFn === "function") {
|
|
14
|
-
try {
|
|
15
|
-
const unregister = (registerFn as (fn: () => void) => (() => void) | void)(cleanup);
|
|
16
|
-
if (typeof unregister === "function") return unregister;
|
|
17
|
-
// API returned void — cleanup is registered but cannot be unregistered
|
|
18
|
-
return undefined;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
logInternalError("session-resources.register", error);
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Try to register a cleanup function with Pi's session resource cleanup API (v0.72+).
|
|
6
|
+
* Falls back to returning undefined if the API is not available.
|
|
7
|
+
*
|
|
8
|
+
* The returned function (if defined) can be called to unregister the cleanup.
|
|
9
|
+
*/
|
|
10
|
+
export function tryRegisterSessionCleanup(pi: ExtensionAPI, cleanup: () => void): (() => void) | undefined {
|
|
11
|
+
const api = pi as unknown as Record<string, unknown>;
|
|
12
|
+
const registerFn = api["registerSessionResourceCleanup"];
|
|
13
|
+
if (typeof registerFn === "function") {
|
|
14
|
+
try {
|
|
15
|
+
const unregister = (registerFn as (fn: () => void) => (() => void) | void)(cleanup);
|
|
16
|
+
if (typeof unregister === "function") return unregister;
|
|
17
|
+
// API returned void — cleanup is registered but cannot be unregistered
|
|
18
|
+
return undefined;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logInternalError("session-resources.register", error);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|