pi-crew 0.2.3 → 0.2.5
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 +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- 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/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- 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/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- 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/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
package/src/state/locks.ts
CHANGED
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { TeamRunManifest } from "./types.ts";
|
|
4
|
-
import { DEFAULT_LOCKS } from "../config/defaults.ts";
|
|
5
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
6
|
-
|
|
7
|
-
export interface RunLockOptions {
|
|
8
|
-
staleMs?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const DEFAULT_STALE_MS = DEFAULT_LOCKS.staleMs;
|
|
12
|
-
|
|
13
|
-
function lockPath(manifest: TeamRunManifest): string {
|
|
14
|
-
return path.join(manifest.stateRoot, "run.lock");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function parseCreatedAtFromLock(raw: string): number | undefined {
|
|
20
|
-
try {
|
|
21
|
-
const payload = JSON.parse(raw) as unknown;
|
|
22
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
23
|
-
const candidate = payload as { createdAt?: unknown };
|
|
24
|
-
if (typeof candidate.createdAt !== "string") return undefined;
|
|
25
|
-
const parsed = Date.parse(candidate.createdAt);
|
|
26
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
27
|
-
} catch {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isLockStale(filePath: string, staleMs: number): boolean {
|
|
33
|
-
try {
|
|
34
|
-
const stat = fs.statSync(filePath);
|
|
35
|
-
let createdAt = parseCreatedAtFromLock(fs.readFileSync(filePath, "utf-8"));
|
|
36
|
-
if (createdAt === undefined) createdAt = stat.mtimeMs;
|
|
37
|
-
return Date.now() - createdAt > staleMs;
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeLockFile(filePath: string): void {
|
|
44
|
-
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
|
|
45
|
-
try {
|
|
46
|
-
fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
|
|
47
|
-
} finally {
|
|
48
|
-
fs.closeSync(fd);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function acquireLockWithRetry(filePath: string, staleMs: number): void {
|
|
53
|
-
let attempt = 0;
|
|
54
|
-
const deadline = Date.now() + staleMs * 2;
|
|
55
|
-
while (true) {
|
|
56
|
-
try {
|
|
57
|
-
writeLockFile(filePath);
|
|
58
|
-
return;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
61
|
-
if (code !== "EEXIST") throw error;
|
|
62
|
-
if (Date.now() > deadline) {
|
|
63
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
64
|
-
}
|
|
65
|
-
// If lock is not stale, fail fast (sync should not wait for active locks)
|
|
66
|
-
if (!isLockStale(filePath, staleMs)) {
|
|
67
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
68
|
-
}
|
|
69
|
-
// Lock is stale — try to clear it, but don't bail on rmSync error — let loop retry
|
|
70
|
-
try {
|
|
71
|
-
fs.rmSync(filePath, { force: true });
|
|
72
|
-
} catch { /* race — let loop retry */ }
|
|
73
|
-
sleepSync(Math.min(250, 25 * 2 ** attempt));
|
|
74
|
-
attempt++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function sleep(ms: number): Promise<void> {
|
|
80
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function readLockStateAsync(filePath: string, staleMs: number): void {
|
|
84
|
-
try {
|
|
85
|
-
if (isLockStale(filePath, staleMs)) fs.rmSync(filePath, { force: true });
|
|
86
|
-
} catch {
|
|
87
|
-
// Ignore stale-check races.
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function acquireLockWithRetryAsync(filePath: string, staleMs: number): Promise<void> {
|
|
92
|
-
let attempt = 0;
|
|
93
|
-
const deadline = Date.now() + staleMs * 2;
|
|
94
|
-
while (true) {
|
|
95
|
-
try {
|
|
96
|
-
writeLockFile(filePath);
|
|
97
|
-
return;
|
|
98
|
-
} catch (error) {
|
|
99
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
100
|
-
if (code !== "EEXIST") throw error;
|
|
101
|
-
if (Date.now() > deadline) {
|
|
102
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
103
|
-
}
|
|
104
|
-
// If lock is not stale, fail fast (async should not wait for active locks)
|
|
105
|
-
if (!isLockStale(filePath, staleMs)) {
|
|
106
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
107
|
-
}
|
|
108
|
-
readLockStateAsync(filePath, staleMs);
|
|
109
|
-
const delay = Math.min(250, 25 * 2 ** attempt);
|
|
110
|
-
await sleep(delay);
|
|
111
|
-
attempt++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function withRunLockSync<T>(manifest: TeamRunManifest, fn: () => T, options: RunLockOptions = {}): T {
|
|
117
|
-
const filePath = lockPath(manifest);
|
|
118
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
119
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
120
|
-
acquireLockWithRetry(filePath, staleMs);
|
|
121
|
-
try {
|
|
122
|
-
return fn();
|
|
123
|
-
} finally {
|
|
124
|
-
try {
|
|
125
|
-
fs.rmSync(filePath, { force: true });
|
|
126
|
-
} catch {
|
|
127
|
-
// Best-effort lock cleanup.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function withRunLock<T>(manifest: TeamRunManifest, fn: () => Promise<T>, options: RunLockOptions = {}): Promise<T> {
|
|
133
|
-
const filePath = lockPath(manifest);
|
|
134
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
135
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
136
|
-
await acquireLockWithRetryAsync(filePath, staleMs);
|
|
137
|
-
try {
|
|
138
|
-
return await fn();
|
|
139
|
-
} finally {
|
|
140
|
-
try {
|
|
141
|
-
fs.rmSync(filePath, { force: true });
|
|
142
|
-
} catch {
|
|
143
|
-
// Best-effort lock cleanup.
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest } from "./types.ts";
|
|
4
|
+
import { DEFAULT_LOCKS } from "../config/defaults.ts";
|
|
5
|
+
import { sleepSync } from "../utils/sleep.ts";
|
|
6
|
+
|
|
7
|
+
export interface RunLockOptions {
|
|
8
|
+
staleMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STALE_MS = DEFAULT_LOCKS.staleMs;
|
|
12
|
+
|
|
13
|
+
function lockPath(manifest: TeamRunManifest): string {
|
|
14
|
+
return path.join(manifest.stateRoot, "run.lock");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
function parseCreatedAtFromLock(raw: string): number | undefined {
|
|
20
|
+
try {
|
|
21
|
+
const payload = JSON.parse(raw) as unknown;
|
|
22
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
23
|
+
const candidate = payload as { createdAt?: unknown };
|
|
24
|
+
if (typeof candidate.createdAt !== "string") return undefined;
|
|
25
|
+
const parsed = Date.parse(candidate.createdAt);
|
|
26
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isLockStale(filePath: string, staleMs: number): boolean {
|
|
33
|
+
try {
|
|
34
|
+
const stat = fs.statSync(filePath);
|
|
35
|
+
let createdAt = parseCreatedAtFromLock(fs.readFileSync(filePath, "utf-8"));
|
|
36
|
+
if (createdAt === undefined) createdAt = stat.mtimeMs;
|
|
37
|
+
return Date.now() - createdAt > staleMs;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeLockFile(filePath: string): void {
|
|
44
|
+
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
|
|
45
|
+
try {
|
|
46
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
|
|
47
|
+
} finally {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function acquireLockWithRetry(filePath: string, staleMs: number): void {
|
|
53
|
+
let attempt = 0;
|
|
54
|
+
const deadline = Date.now() + staleMs * 2;
|
|
55
|
+
while (true) {
|
|
56
|
+
try {
|
|
57
|
+
writeLockFile(filePath);
|
|
58
|
+
return;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
61
|
+
if (code !== "EEXIST") throw error;
|
|
62
|
+
if (Date.now() > deadline) {
|
|
63
|
+
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
64
|
+
}
|
|
65
|
+
// If lock is not stale, fail fast (sync should not wait for active locks)
|
|
66
|
+
if (!isLockStale(filePath, staleMs)) {
|
|
67
|
+
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
68
|
+
}
|
|
69
|
+
// Lock is stale — try to clear it, but don't bail on rmSync error — let loop retry
|
|
70
|
+
try {
|
|
71
|
+
fs.rmSync(filePath, { force: true });
|
|
72
|
+
} catch { /* race — let loop retry */ }
|
|
73
|
+
sleepSync(Math.min(250, 25 * 2 ** attempt));
|
|
74
|
+
attempt++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sleep(ms: number): Promise<void> {
|
|
80
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readLockStateAsync(filePath: string, staleMs: number): void {
|
|
84
|
+
try {
|
|
85
|
+
if (isLockStale(filePath, staleMs)) fs.rmSync(filePath, { force: true });
|
|
86
|
+
} catch {
|
|
87
|
+
// Ignore stale-check races.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function acquireLockWithRetryAsync(filePath: string, staleMs: number): Promise<void> {
|
|
92
|
+
let attempt = 0;
|
|
93
|
+
const deadline = Date.now() + staleMs * 2;
|
|
94
|
+
while (true) {
|
|
95
|
+
try {
|
|
96
|
+
writeLockFile(filePath);
|
|
97
|
+
return;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
100
|
+
if (code !== "EEXIST") throw error;
|
|
101
|
+
if (Date.now() > deadline) {
|
|
102
|
+
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
103
|
+
}
|
|
104
|
+
// If lock is not stale, fail fast (async should not wait for active locks)
|
|
105
|
+
if (!isLockStale(filePath, staleMs)) {
|
|
106
|
+
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
107
|
+
}
|
|
108
|
+
readLockStateAsync(filePath, staleMs);
|
|
109
|
+
const delay = Math.min(250, 25 * 2 ** attempt);
|
|
110
|
+
await sleep(delay);
|
|
111
|
+
attempt++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function withRunLockSync<T>(manifest: TeamRunManifest, fn: () => T, options: RunLockOptions = {}): T {
|
|
117
|
+
const filePath = lockPath(manifest);
|
|
118
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
119
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
120
|
+
acquireLockWithRetry(filePath, staleMs);
|
|
121
|
+
try {
|
|
122
|
+
return fn();
|
|
123
|
+
} finally {
|
|
124
|
+
try {
|
|
125
|
+
fs.rmSync(filePath, { force: true });
|
|
126
|
+
} catch {
|
|
127
|
+
// Best-effort lock cleanup.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function withRunLock<T>(manifest: TeamRunManifest, fn: () => Promise<T>, options: RunLockOptions = {}): Promise<T> {
|
|
133
|
+
const filePath = lockPath(manifest);
|
|
134
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
135
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
136
|
+
await acquireLockWithRetryAsync(filePath, staleMs);
|
|
137
|
+
try {
|
|
138
|
+
return await fn();
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
fs.rmSync(filePath, { force: true });
|
|
142
|
+
} catch {
|
|
143
|
+
// Best-effort lock cleanup.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|