pi-amq 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/extension/guidance.d.ts +25 -0
- package/dist/extension/guidance.d.ts.map +1 -0
- package/dist/extension/guidance.js +121 -0
- package/dist/extension/guidance.js.map +1 -0
- package/dist/extension/guide.d.ts +30 -0
- package/dist/extension/guide.d.ts.map +1 -0
- package/dist/extension/guide.js +128 -0
- package/dist/extension/guide.js.map +1 -0
- package/dist/extension/index.d.ts +4 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/index.js +6 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/runtime.d.ts +48 -0
- package/dist/extension/runtime.d.ts.map +1 -0
- package/dist/extension/runtime.js +1095 -0
- package/dist/extension/runtime.js.map +1 -0
- package/dist/extension/tools.d.ts +12 -0
- package/dist/extension/tools.d.ts.map +1 -0
- package/dist/extension/tools.js +198 -0
- package/dist/extension/tools.js.map +1 -0
- package/dist/extension/worker-runtime.d.ts +34 -0
- package/dist/extension/worker-runtime.d.ts.map +1 -0
- package/dist/extension/worker-runtime.js +974 -0
- package/dist/extension/worker-runtime.js.map +1 -0
- package/dist/extension/worker-tools.d.ts +140 -0
- package/dist/extension/worker-tools.d.ts.map +1 -0
- package/dist/extension/worker-tools.js +217 -0
- package/dist/extension/worker-tools.js.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts +44 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.js +199 -0
- package/dist/integration/confirmed-bind-onboarding.js.map +1 -0
- package/dist/integration/fleet-view.d.ts +66 -0
- package/dist/integration/fleet-view.d.ts.map +1 -0
- package/dist/integration/fleet-view.js +238 -0
- package/dist/integration/fleet-view.js.map +1 -0
- package/dist/integration/fresh-session-dispatcher.d.ts +66 -0
- package/dist/integration/fresh-session-dispatcher.d.ts.map +1 -0
- package/dist/integration/fresh-session-dispatcher.js +103 -0
- package/dist/integration/fresh-session-dispatcher.js.map +1 -0
- package/dist/integration/launch-preview.d.ts +31 -0
- package/dist/integration/launch-preview.d.ts.map +1 -0
- package/dist/integration/launch-preview.js +325 -0
- package/dist/integration/launch-preview.js.map +1 -0
- package/dist/integration/transport-port.d.ts +39 -0
- package/dist/integration/transport-port.d.ts.map +1 -0
- package/dist/integration/transport-port.js +2 -0
- package/dist/integration/transport-port.js.map +1 -0
- package/dist/integration/worker-completion-notifier.d.ts +22 -0
- package/dist/integration/worker-completion-notifier.d.ts.map +1 -0
- package/dist/integration/worker-completion-notifier.js +81 -0
- package/dist/integration/worker-completion-notifier.js.map +1 -0
- package/dist/transport/amq-cli-adapter.d.ts +41 -0
- package/dist/transport/amq-cli-adapter.d.ts.map +1 -0
- package/dist/transport/amq-cli-adapter.js +291 -0
- package/dist/transport/amq-cli-adapter.js.map +1 -0
- package/dist/transport/amq-message.d.ts +47 -0
- package/dist/transport/amq-message.d.ts.map +1 -0
- package/dist/transport/amq-message.js +173 -0
- package/dist/transport/amq-message.js.map +1 -0
- package/dist/transport/amq-operations.d.ts +105 -0
- package/dist/transport/amq-operations.d.ts.map +1 -0
- package/dist/transport/amq-operations.js +320 -0
- package/dist/transport/amq-operations.js.map +1 -0
- package/dist/transport/atomic-json.d.ts +18 -0
- package/dist/transport/atomic-json.d.ts.map +1 -0
- package/dist/transport/atomic-json.js +90 -0
- package/dist/transport/atomic-json.js.map +1 -0
- package/dist/transport/binding-resolver.d.ts +46 -0
- package/dist/transport/binding-resolver.d.ts.map +1 -0
- package/dist/transport/binding-resolver.js +389 -0
- package/dist/transport/binding-resolver.js.map +1 -0
- package/dist/transport/bridge-journal.d.ts +100 -0
- package/dist/transport/bridge-journal.d.ts.map +1 -0
- package/dist/transport/bridge-journal.js +459 -0
- package/dist/transport/bridge-journal.js.map +1 -0
- package/dist/transport/dlq-retry-audit.d.ts +28 -0
- package/dist/transport/dlq-retry-audit.d.ts.map +1 -0
- package/dist/transport/dlq-retry-audit.js +66 -0
- package/dist/transport/dlq-retry-audit.js.map +1 -0
- package/dist/transport/inbox-pump.d.ts +81 -0
- package/dist/transport/inbox-pump.d.ts.map +1 -0
- package/dist/transport/inbox-pump.js +558 -0
- package/dist/transport/inbox-pump.js.map +1 -0
- package/dist/transport/index.d.ts +12 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +12 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/presence-projector.d.ts +29 -0
- package/dist/transport/presence-projector.d.ts.map +1 -0
- package/dist/transport/presence-projector.js +73 -0
- package/dist/transport/presence-projector.js.map +1 -0
- package/dist/transport/priority-delivery-scheduler.d.ts +85 -0
- package/dist/transport/priority-delivery-scheduler.d.ts.map +1 -0
- package/dist/transport/priority-delivery-scheduler.js +210 -0
- package/dist/transport/priority-delivery-scheduler.js.map +1 -0
- package/dist/transport/receiver-lease.d.ts +102 -0
- package/dist/transport/receiver-lease.d.ts.map +1 -0
- package/dist/transport/receiver-lease.js +778 -0
- package/dist/transport/receiver-lease.js.map +1 -0
- package/dist/transport/runtime-status.d.ts +48 -0
- package/dist/transport/runtime-status.d.ts.map +1 -0
- package/dist/transport/runtime-status.js +71 -0
- package/dist/transport/runtime-status.js.map +1 -0
- package/dist/workflow/atomic-storage.d.ts +12 -0
- package/dist/workflow/atomic-storage.d.ts.map +1 -0
- package/dist/workflow/atomic-storage.js +104 -0
- package/dist/workflow/atomic-storage.js.map +1 -0
- package/dist/workflow/budget.d.ts +18 -0
- package/dist/workflow/budget.d.ts.map +1 -0
- package/dist/workflow/budget.js +74 -0
- package/dist/workflow/budget.js.map +1 -0
- package/dist/workflow/index.d.ts +17 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +17 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/lifecycle-projection.d.ts +18 -0
- package/dist/workflow/lifecycle-projection.d.ts.map +1 -0
- package/dist/workflow/lifecycle-projection.js +22 -0
- package/dist/workflow/lifecycle-projection.js.map +1 -0
- package/dist/workflow/model-fallback.d.ts +2 -0
- package/dist/workflow/model-fallback.d.ts.map +1 -0
- package/dist/workflow/model-fallback.js +17 -0
- package/dist/workflow/model-fallback.js.map +1 -0
- package/dist/workflow/pi-session-fork.d.ts +18 -0
- package/dist/workflow/pi-session-fork.d.ts.map +1 -0
- package/dist/workflow/pi-session-fork.js +80 -0
- package/dist/workflow/pi-session-fork.js.map +1 -0
- package/dist/workflow/run-artifacts.d.ts +5 -0
- package/dist/workflow/run-artifacts.d.ts.map +1 -0
- package/dist/workflow/run-artifacts.js +25 -0
- package/dist/workflow/run-artifacts.js.map +1 -0
- package/dist/workflow/run-cleanup.d.ts +70 -0
- package/dist/workflow/run-cleanup.d.ts.map +1 -0
- package/dist/workflow/run-cleanup.js +222 -0
- package/dist/workflow/run-cleanup.js.map +1 -0
- package/dist/workflow/run-gate.d.ts +43 -0
- package/dist/workflow/run-gate.d.ts.map +1 -0
- package/dist/workflow/run-gate.js +70 -0
- package/dist/workflow/run-gate.js.map +1 -0
- package/dist/workflow/run-store.d.ts +208 -0
- package/dist/workflow/run-store.d.ts.map +1 -0
- package/dist/workflow/run-store.js +2965 -0
- package/dist/workflow/run-store.js.map +1 -0
- package/dist/workflow/spawn-confirmation.d.ts +44 -0
- package/dist/workflow/spawn-confirmation.d.ts.map +1 -0
- package/dist/workflow/spawn-confirmation.js +228 -0
- package/dist/workflow/spawn-confirmation.js.map +1 -0
- package/dist/workflow/system-process-inspector.d.ts +9 -0
- package/dist/workflow/system-process-inspector.d.ts.map +1 -0
- package/dist/workflow/system-process-inspector.js +129 -0
- package/dist/workflow/system-process-inspector.js.map +1 -0
- package/dist/workflow/worker-child-extension.d.ts +3 -0
- package/dist/workflow/worker-child-extension.d.ts.map +1 -0
- package/dist/workflow/worker-child-extension.js +182 -0
- package/dist/workflow/worker-child-extension.js.map +1 -0
- package/dist/workflow/worker-gate.d.ts +74 -0
- package/dist/workflow/worker-gate.d.ts.map +1 -0
- package/dist/workflow/worker-gate.js +165 -0
- package/dist/workflow/worker-gate.js.map +1 -0
- package/dist/workflow/worker-launch.d.ts +65 -0
- package/dist/workflow/worker-launch.d.ts.map +1 -0
- package/dist/workflow/worker-launch.js +338 -0
- package/dist/workflow/worker-launch.js.map +1 -0
- package/dist/workflow/worker-model-catalog.d.ts +36 -0
- package/dist/workflow/worker-model-catalog.d.ts.map +1 -0
- package/dist/workflow/worker-model-catalog.js +105 -0
- package/dist/workflow/worker-model-catalog.js.map +1 -0
- package/dist/workflow/worker-process-supervisor.d.ts +110 -0
- package/dist/workflow/worker-process-supervisor.d.ts.map +1 -0
- package/dist/workflow/worker-process-supervisor.js +1188 -0
- package/dist/workflow/worker-process-supervisor.js.map +1 -0
- package/dist/workflow/worker-prompt-capsule.d.ts +32 -0
- package/dist/workflow/worker-prompt-capsule.d.ts.map +1 -0
- package/dist/workflow/worker-prompt-capsule.js +52 -0
- package/dist/workflow/worker-prompt-capsule.js.map +1 -0
- package/dist/workflow/worker-role-presets.d.ts +38 -0
- package/dist/workflow/worker-role-presets.d.ts.map +1 -0
- package/dist/workflow/worker-role-presets.js +201 -0
- package/dist/workflow/worker-role-presets.js.map +1 -0
- package/dist/workflow/workflow-doctor.d.ts +57 -0
- package/dist/workflow/workflow-doctor.d.ts.map +1 -0
- package/dist/workflow/workflow-doctor.js +289 -0
- package/dist/workflow/workflow-doctor.js.map +1 -0
- package/dist/workflow/workflow-lease.d.ts +125 -0
- package/dist/workflow/workflow-lease.d.ts.map +1 -0
- package/dist/workflow/workflow-lease.js +689 -0
- package/dist/workflow/workflow-lease.js.map +1 -0
- package/dist/workflow/workflow-types.d.ts +335 -0
- package/dist/workflow/workflow-types.d.ts.map +1 -0
- package/dist/workflow/workflow-types.js +11 -0
- package/dist/workflow/workflow-types.js.map +1 -0
- package/dist/workflow/workspace-paths.d.ts +4 -0
- package/dist/workflow/workspace-paths.d.ts.map +1 -0
- package/dist/workflow/workspace-paths.js +22 -0
- package/dist/workflow/workspace-paths.js.map +1 -0
- package/docs/conformance.md +116 -0
- package/docs/demo-transcript.md +34 -0
- package/docs/design.md +1481 -0
- package/docs/operator-guide.md +100 -0
- package/extension/guidance.ts +162 -0
- package/extension/guide.ts +155 -0
- package/extension/index.ts +8 -0
- package/extension/runtime.ts +1224 -0
- package/extension/tools.ts +221 -0
- package/extension/worker-runtime.ts +1150 -0
- package/extension/worker-tools.ts +276 -0
- package/integration/confirmed-bind-onboarding.ts +296 -0
- package/integration/fleet-view.ts +286 -0
- package/integration/fresh-session-dispatcher.ts +166 -0
- package/integration/launch-preview.ts +396 -0
- package/integration/transport-port.ts +29 -0
- package/integration/worker-completion-notifier.ts +98 -0
- package/package.json +55 -0
- package/roles/oracle.md +9 -0
- package/roles/researcher.md +8 -0
- package/roles/reviewer.md +9 -0
- package/roles/scout.md +8 -0
- package/roles/worker.md +10 -0
- package/scripts/demo-two-terminal.sh +59 -0
- package/skills/coordination-patterns/SKILL.md +41 -0
- package/skills/coordination-patterns/references/supervised-worker-fan-out.md +12 -0
- package/skills/coordination-patterns/references/today-mail-team.md +36 -0
- package/transport/amq-cli-adapter.ts +381 -0
- package/transport/amq-message.ts +218 -0
- package/transport/amq-operations.ts +482 -0
- package/transport/atomic-json.ts +115 -0
- package/transport/binding-resolver.ts +565 -0
- package/transport/bridge-journal.ts +588 -0
- package/transport/dlq-retry-audit.ts +97 -0
- package/transport/inbox-pump.ts +729 -0
- package/transport/index.ts +11 -0
- package/transport/presence-projector.ts +104 -0
- package/transport/priority-delivery-scheduler.ts +328 -0
- package/transport/receiver-lease.ts +945 -0
- package/transport/runtime-status.ts +112 -0
- package/workflow/.gitkeep +1 -0
- package/workflow/atomic-storage.ts +120 -0
- package/workflow/budget.ts +90 -0
- package/workflow/index.ts +16 -0
- package/workflow/lifecycle-projection.ts +37 -0
- package/workflow/model-fallback.ts +17 -0
- package/workflow/pi-session-fork.ts +96 -0
- package/workflow/run-artifacts.ts +36 -0
- package/workflow/run-cleanup.ts +315 -0
- package/workflow/run-gate.ts +122 -0
- package/workflow/run-store.ts +3890 -0
- package/workflow/spawn-confirmation.ts +305 -0
- package/workflow/system-process-inspector.ts +142 -0
- package/workflow/worker-child-extension.ts +193 -0
- package/workflow/worker-gate.ts +238 -0
- package/workflow/worker-launch.ts +485 -0
- package/workflow/worker-model-catalog.ts +165 -0
- package/workflow/worker-process-supervisor.ts +1504 -0
- package/workflow/worker-prompt-capsule.ts +81 -0
- package/workflow/worker-role-presets.ts +241 -0
- package/workflow/workflow-doctor.ts +400 -0
- package/workflow/workflow-lease.ts +905 -0
- package/workflow/workflow-types.ts +367 -0
- package/workflow/workspace-paths.ts +32 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { basename, isAbsolute, join, normalize } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { RunRecordDiagnostic } from "./run-store.js";
|
|
5
|
+
import type {
|
|
6
|
+
ProcessIdentity,
|
|
7
|
+
ProcessIdentityInspector,
|
|
8
|
+
ProcessInspection,
|
|
9
|
+
WorkerSessionLeaseDiagnostic,
|
|
10
|
+
WorkflowLeaseOwnerV1,
|
|
11
|
+
} from "./workflow-lease.js";
|
|
12
|
+
import type { RunProjectionV2, WorkerAttemptProjection, WorkerProjection } from "./workflow-types.js";
|
|
13
|
+
|
|
14
|
+
export interface WorkflowDoctorCommand {
|
|
15
|
+
readonly kind: "exec" | "pi";
|
|
16
|
+
readonly argv: readonly string[];
|
|
17
|
+
readonly purpose: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkflowDoctorFinding {
|
|
21
|
+
readonly severity: "error" | "warning";
|
|
22
|
+
readonly code:
|
|
23
|
+
| "corrupt-run-record"
|
|
24
|
+
| "corrupt-session-lease"
|
|
25
|
+
| "orphan-process"
|
|
26
|
+
| "ledger-process-missing"
|
|
27
|
+
| "stale-session-lease"
|
|
28
|
+
| "abandoned-session-lease"
|
|
29
|
+
| "dangling-session";
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly paths: readonly string[];
|
|
32
|
+
readonly commands: readonly WorkflowDoctorCommand[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WorkflowDoctorReport {
|
|
36
|
+
readonly schema: 1;
|
|
37
|
+
readonly checkedAt: string;
|
|
38
|
+
readonly status: "ok" | "attention";
|
|
39
|
+
readonly summary: {
|
|
40
|
+
readonly healthyRuns: number;
|
|
41
|
+
readonly corruptRuns: number;
|
|
42
|
+
readonly workerProcessesChecked: number;
|
|
43
|
+
readonly orphanProcesses: number;
|
|
44
|
+
readonly staleOrAbandonedLeases: number;
|
|
45
|
+
readonly danglingSessions: number;
|
|
46
|
+
};
|
|
47
|
+
readonly diskUsage: {
|
|
48
|
+
readonly totalBytes: number;
|
|
49
|
+
readonly byArea: readonly { readonly name: string; readonly path: string; readonly bytes: number }[];
|
|
50
|
+
};
|
|
51
|
+
readonly findings: readonly WorkflowDoctorFinding[];
|
|
52
|
+
readonly suggestedCommands: readonly WorkflowDoctorCommand[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface RunDiagnosticCatalog {
|
|
56
|
+
diagnoseRunRecords(): Promise<readonly RunRecordDiagnostic[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface LeaseDiagnosticCatalog {
|
|
60
|
+
diagnoseWorkerSessionLeases(): Promise<readonly WorkerSessionLeaseDiagnostic[]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorkflowDoctorOptions {
|
|
64
|
+
readonly namespaceDirectory: string;
|
|
65
|
+
readonly runs: RunDiagnosticCatalog;
|
|
66
|
+
readonly leases: LeaseDiagnosticCatalog;
|
|
67
|
+
readonly inspector: ProcessIdentityInspector;
|
|
68
|
+
readonly now?: () => Date;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AttemptReference {
|
|
72
|
+
readonly run: RunProjectionV2;
|
|
73
|
+
readonly worker: WorkerProjection;
|
|
74
|
+
readonly attempt: WorkerAttemptProjection;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class WorkflowDoctor {
|
|
78
|
+
readonly #namespaceDirectory: string;
|
|
79
|
+
readonly #runs: RunDiagnosticCatalog;
|
|
80
|
+
readonly #leases: LeaseDiagnosticCatalog;
|
|
81
|
+
readonly #inspector: ProcessIdentityInspector;
|
|
82
|
+
readonly #now: () => Date;
|
|
83
|
+
|
|
84
|
+
constructor(options: WorkflowDoctorOptions) {
|
|
85
|
+
if (!isAbsolute(options.namespaceDirectory)) {
|
|
86
|
+
throw new TypeError("Workflow doctor namespace must be absolute");
|
|
87
|
+
}
|
|
88
|
+
this.#namespaceDirectory = normalize(options.namespaceDirectory);
|
|
89
|
+
this.#runs = options.runs;
|
|
90
|
+
this.#leases = options.leases;
|
|
91
|
+
this.#inspector = options.inspector;
|
|
92
|
+
this.#now = options.now ?? (() => new Date());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async inspect(): Promise<WorkflowDoctorReport> {
|
|
96
|
+
const records = await this.#runs.diagnoseRunRecords();
|
|
97
|
+
const healthy = records.filter((record) => record.kind === "healthy");
|
|
98
|
+
const findings: WorkflowDoctorFinding[] = [];
|
|
99
|
+
const orphanProcessIds = new Set<number>();
|
|
100
|
+
for (const record of records) {
|
|
101
|
+
if (record.kind !== "corrupt") continue;
|
|
102
|
+
findings.push({
|
|
103
|
+
severity: "error",
|
|
104
|
+
code: "corrupt-run-record",
|
|
105
|
+
message: record.diagnostic,
|
|
106
|
+
paths: [record.directory],
|
|
107
|
+
commands: [quarantineCommand(this.#namespaceDirectory, "run-record", record.directory)],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const attempts = healthy.flatMap((record) => attemptsIn(record.projection));
|
|
112
|
+
let workerProcessesChecked = 0;
|
|
113
|
+
for (const reference of attempts) {
|
|
114
|
+
const pid = reference.attempt.pidStartEvidence.pid;
|
|
115
|
+
if (pid === null) continue;
|
|
116
|
+
workerProcessesChecked += 1;
|
|
117
|
+
const inspection = await this.#inspector.inspect(pid);
|
|
118
|
+
const matches = inspection.kind === "live"
|
|
119
|
+
&& processIdentityMatchesAttempt(inspection.identity, reference.attempt);
|
|
120
|
+
if (matches && reference.attempt.pidStartEvidence.observedExitAt !== null) {
|
|
121
|
+
orphanProcessIds.add(pid);
|
|
122
|
+
findings.push(orphanProcessFinding(
|
|
123
|
+
pid,
|
|
124
|
+
reference.attempt.session.path,
|
|
125
|
+
`Process ${pid} still matches terminal ledger attempt ${reference.attempt.attemptId}`,
|
|
126
|
+
));
|
|
127
|
+
} else if (!matches && reference.attempt.pidStartEvidence.observedExitAt === null) {
|
|
128
|
+
findings.push({
|
|
129
|
+
severity: "warning",
|
|
130
|
+
code: "ledger-process-missing",
|
|
131
|
+
message: processMismatchDiagnostic(pid, inspection, reference.attempt),
|
|
132
|
+
paths: [reference.attempt.session.path],
|
|
133
|
+
commands: [inspectProcessCommand(pid)],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let currentIdentity: ProcessIdentity | undefined;
|
|
139
|
+
try {
|
|
140
|
+
currentIdentity = await this.#inspector.current();
|
|
141
|
+
} catch {
|
|
142
|
+
// Individual records stay visible as abandoned/unverifiable below.
|
|
143
|
+
}
|
|
144
|
+
const leaseRecords = await this.#leases.diagnoseWorkerSessionLeases();
|
|
145
|
+
for (const record of leaseRecords) {
|
|
146
|
+
if (record.projection.kind === "empty") continue;
|
|
147
|
+
if (record.projection.kind === "corrupt") {
|
|
148
|
+
findings.push({
|
|
149
|
+
severity: "error",
|
|
150
|
+
code: "corrupt-session-lease",
|
|
151
|
+
message: record.projection.diagnostic,
|
|
152
|
+
paths: [record.path],
|
|
153
|
+
commands: [quarantineCommand(this.#namespaceDirectory, "session-lease", record.path)],
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const owner = record.projection.owner;
|
|
158
|
+
const reference = attempts.find((candidate) => leaseOwnerReferences(owner, candidate));
|
|
159
|
+
const inspection = await inspectLeaseOwner(owner, currentIdentity, this.#inspector);
|
|
160
|
+
const live = inspection.kind === "live" && processIdentityMatchesOwner(inspection.identity, owner);
|
|
161
|
+
if (live && (reference === undefined || !leaseOwnerConsistentWithLedger(owner, reference))) {
|
|
162
|
+
if (!orphanProcessIds.has(owner.pid)) {
|
|
163
|
+
orphanProcessIds.add(owner.pid);
|
|
164
|
+
findings.push(orphanProcessFinding(
|
|
165
|
+
owner.pid,
|
|
166
|
+
record.path,
|
|
167
|
+
reference === undefined
|
|
168
|
+
? `Live lease process ${owner.pid} has no matching readable ledger attempt`
|
|
169
|
+
: `Live lease process ${owner.pid} conflicts with ledger attempt ${reference.attempt.attemptId}`,
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
} else if (!live && inspection.kind !== "unknown") {
|
|
173
|
+
findings.push({
|
|
174
|
+
severity: "warning",
|
|
175
|
+
code: "stale-session-lease",
|
|
176
|
+
message: leaseMismatchDiagnostic(owner, inspection),
|
|
177
|
+
paths: [record.path, ...(owner.sessionPath ? [owner.sessionPath] : [])],
|
|
178
|
+
commands: [quarantineCommand(this.#namespaceDirectory, "session-lease", record.path)],
|
|
179
|
+
});
|
|
180
|
+
} else if (inspection.kind === "unknown") {
|
|
181
|
+
findings.push({
|
|
182
|
+
severity: "warning",
|
|
183
|
+
code: "abandoned-session-lease",
|
|
184
|
+
message: inspection.diagnostic,
|
|
185
|
+
paths: [record.path, ...(owner.sessionPath ? [owner.sessionPath] : [])],
|
|
186
|
+
commands: [inspectProcessCommand(owner.pid)],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const referencedSessions = new Set(attempts.map((reference) => normalize(reference.attempt.session.path)));
|
|
192
|
+
for (const path of await sessionDirectories(this.#namespaceDirectory)) {
|
|
193
|
+
if (referencedSessions.has(path)) continue;
|
|
194
|
+
findings.push({
|
|
195
|
+
severity: "warning",
|
|
196
|
+
code: "dangling-session",
|
|
197
|
+
message: records.some((record) => record.kind === "corrupt")
|
|
198
|
+
? "Session is not referenced by a readable run; corrupt run records make ownership uncertain"
|
|
199
|
+
: "Session is not referenced by any workflow run",
|
|
200
|
+
paths: [path],
|
|
201
|
+
commands: [quarantineCommand(this.#namespaceDirectory, "session", path)],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
findings.sort((left, right) =>
|
|
206
|
+
left.code.localeCompare(right.code) || left.paths[0]!.localeCompare(right.paths[0]!));
|
|
207
|
+
const byArea = await topLevelDiskUsage(this.#namespaceDirectory);
|
|
208
|
+
return {
|
|
209
|
+
schema: 1,
|
|
210
|
+
checkedAt: this.#now().toISOString(),
|
|
211
|
+
status: findings.length === 0 ? "ok" : "attention",
|
|
212
|
+
summary: {
|
|
213
|
+
healthyRuns: healthy.length,
|
|
214
|
+
corruptRuns: records.length - healthy.length,
|
|
215
|
+
workerProcessesChecked,
|
|
216
|
+
orphanProcesses: orphanProcessIds.size,
|
|
217
|
+
staleOrAbandonedLeases: findings.filter((finding) =>
|
|
218
|
+
finding.code === "stale-session-lease"
|
|
219
|
+
|| finding.code === "abandoned-session-lease"
|
|
220
|
+
|| finding.code === "corrupt-session-lease").length,
|
|
221
|
+
danglingSessions: findings.filter((finding) => finding.code === "dangling-session").length,
|
|
222
|
+
},
|
|
223
|
+
diskUsage: {
|
|
224
|
+
totalBytes: byArea.reduce((sum, area) => sum + area.bytes, 0),
|
|
225
|
+
byArea,
|
|
226
|
+
},
|
|
227
|
+
findings,
|
|
228
|
+
suggestedCommands: [{
|
|
229
|
+
kind: "pi",
|
|
230
|
+
argv: ["/pi-amq-cleanup", "--older-than", "7d"],
|
|
231
|
+
purpose: "Preview explicit cleanup of old terminal workflow state",
|
|
232
|
+
}],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function attemptsIn(run: RunProjectionV2): AttemptReference[] {
|
|
238
|
+
return Object.values(run.workers).flatMap((worker) =>
|
|
239
|
+
worker.attempts.map((attempt) => ({ run, worker, attempt })));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function leaseOwnerReferences(owner: WorkflowLeaseOwnerV1, reference: AttemptReference): boolean {
|
|
243
|
+
return owner.runId === reference.run.runId
|
|
244
|
+
&& owner.workerId === reference.worker.workerId
|
|
245
|
+
&& owner.attemptId === reference.attempt.attemptId
|
|
246
|
+
&& owner.workerAttemptGeneration === reference.attempt.workerAttemptGeneration
|
|
247
|
+
&& owner.sessionPath === reference.attempt.session.path;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function leaseOwnerConsistentWithLedger(
|
|
251
|
+
owner: WorkflowLeaseOwnerV1,
|
|
252
|
+
reference: AttemptReference,
|
|
253
|
+
): boolean {
|
|
254
|
+
if (owner.stage === "reserved") {
|
|
255
|
+
return reference.attempt.state === "pending"
|
|
256
|
+
&& reference.attempt.pidStartEvidence.pid === null
|
|
257
|
+
&& reference.attempt.session.leaseGeneration === null;
|
|
258
|
+
}
|
|
259
|
+
return reference.attempt.pidStartEvidence.observedExitAt === null
|
|
260
|
+
&& reference.attempt.session.leaseGeneration === owner.generation
|
|
261
|
+
&& processIdentityMatchesAttempt({
|
|
262
|
+
pid: owner.pid,
|
|
263
|
+
processStartIdentity: owner.processStartIdentity,
|
|
264
|
+
bootIdentity: owner.bootIdentity,
|
|
265
|
+
executable: owner.executable,
|
|
266
|
+
hostname: owner.hostname,
|
|
267
|
+
}, reference.attempt);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function inspectLeaseOwner(
|
|
271
|
+
owner: WorkflowLeaseOwnerV1,
|
|
272
|
+
current: ProcessIdentity | undefined,
|
|
273
|
+
inspector: ProcessIdentityInspector,
|
|
274
|
+
): Promise<ProcessInspection> {
|
|
275
|
+
if (!current) return { kind: "unknown", diagnostic: "Current process identity is unavailable" };
|
|
276
|
+
if (owner.hostname !== current.hostname) {
|
|
277
|
+
return { kind: "unknown", diagnostic: `Lease belongs to remote host ${owner.hostname}` };
|
|
278
|
+
}
|
|
279
|
+
return inspector.inspect(owner.pid);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function processIdentityMatchesAttempt(
|
|
283
|
+
identity: ProcessIdentity,
|
|
284
|
+
attempt: WorkerAttemptProjection,
|
|
285
|
+
): boolean {
|
|
286
|
+
const recorded = attempt.pidStartEvidence;
|
|
287
|
+
return identity.pid === recorded.pid
|
|
288
|
+
&& identity.processStartIdentity === recorded.processStartIdentity
|
|
289
|
+
&& identity.bootIdentity === recorded.bootIdentity
|
|
290
|
+
&& identity.executable === recorded.executable
|
|
291
|
+
&& identity.hostname === recorded.hostname;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function processIdentityMatchesOwner(identity: ProcessIdentity, owner: WorkflowLeaseOwnerV1): boolean {
|
|
295
|
+
return identity.pid === owner.pid
|
|
296
|
+
&& identity.processStartIdentity === owner.processStartIdentity
|
|
297
|
+
&& identity.bootIdentity === owner.bootIdentity
|
|
298
|
+
&& identity.executable === owner.executable
|
|
299
|
+
&& identity.hostname === owner.hostname;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function processMismatchDiagnostic(
|
|
303
|
+
pid: number,
|
|
304
|
+
inspection: ProcessInspection,
|
|
305
|
+
attempt: WorkerAttemptProjection,
|
|
306
|
+
): string {
|
|
307
|
+
if (inspection.kind === "absent") return `Ledger attempt ${attempt.attemptId} has no process ${pid}`;
|
|
308
|
+
if (inspection.kind === "unknown") {
|
|
309
|
+
return `Ledger attempt ${attempt.attemptId} process ${pid} is unverifiable: ${inspection.diagnostic}`;
|
|
310
|
+
}
|
|
311
|
+
return `Ledger attempt ${attempt.attemptId} PID ${pid} now has different process identity`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function leaseMismatchDiagnostic(owner: WorkflowLeaseOwnerV1, inspection: ProcessInspection): string {
|
|
315
|
+
return inspection.kind === "absent"
|
|
316
|
+
? `Lease owner process ${owner.pid} is absent`
|
|
317
|
+
: `Lease owner PID ${owner.pid} now has different process identity`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function orphanProcessFinding(
|
|
321
|
+
pid: number,
|
|
322
|
+
path: string,
|
|
323
|
+
message: string,
|
|
324
|
+
): WorkflowDoctorFinding {
|
|
325
|
+
return {
|
|
326
|
+
severity: "error",
|
|
327
|
+
code: "orphan-process",
|
|
328
|
+
message,
|
|
329
|
+
paths: [path],
|
|
330
|
+
commands: [inspectProcessCommand(pid)],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function inspectProcessCommand(pid: number): WorkflowDoctorCommand {
|
|
335
|
+
return {
|
|
336
|
+
kind: "exec",
|
|
337
|
+
argv: ["ps", "-p", String(pid), "-o", "pid=,lstart=,comm="],
|
|
338
|
+
purpose: "Re-inspect PID identity before any operator action",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function quarantineCommand(
|
|
343
|
+
namespaceDirectory: string,
|
|
344
|
+
prefix: string,
|
|
345
|
+
path: string,
|
|
346
|
+
): WorkflowDoctorCommand {
|
|
347
|
+
return {
|
|
348
|
+
kind: "exec",
|
|
349
|
+
argv: ["mv", "--", path, join(namespaceDirectory, "quarantine", `doctor-${prefix}-${basename(path)}`)],
|
|
350
|
+
purpose: "Quarantine the record for manual recovery instead of deleting it",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function sessionDirectories(namespaceDirectory: string): Promise<readonly string[]> {
|
|
355
|
+
const parent = join(namespaceDirectory, "worker-sessions");
|
|
356
|
+
try {
|
|
357
|
+
return (await readdir(parent, { withFileTypes: true }))
|
|
358
|
+
.filter((entry) => !entry.name.startsWith("."))
|
|
359
|
+
.map((entry) => normalize(join(parent, entry.name)))
|
|
360
|
+
.sort();
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function topLevelDiskUsage(
|
|
368
|
+
namespaceDirectory: string,
|
|
369
|
+
): Promise<readonly { readonly name: string; readonly path: string; readonly bytes: number }[]> {
|
|
370
|
+
let entries: string[];
|
|
371
|
+
try {
|
|
372
|
+
entries = await readdir(namespaceDirectory);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
const areas = await Promise.all(entries.filter((entry) => !entry.startsWith(".")).sort().map(async (name) => {
|
|
378
|
+
const path = join(namespaceDirectory, name);
|
|
379
|
+
return { name, path, bytes: await pathBytes(path) };
|
|
380
|
+
}));
|
|
381
|
+
return areas;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function pathBytes(path: string): Promise<number> {
|
|
385
|
+
let info;
|
|
386
|
+
try {
|
|
387
|
+
info = await lstat(path);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (isNodeError(error) && error.code === "ENOENT") return 0;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
if (!info.isDirectory()) return info.size;
|
|
393
|
+
let total = info.size;
|
|
394
|
+
for (const entry of await readdir(path)) total += await pathBytes(join(path, entry));
|
|
395
|
+
return total;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
399
|
+
return error instanceof Error && "code" in error;
|
|
400
|
+
}
|