@vimhead.dev/norn-cli 0.1.0-tip.0
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 +13 -0
- package/assets/README.md +157 -0
- package/assets/docs/README.md +23 -0
- package/assets/docs/agents.md +64 -0
- package/assets/docs/cli.md +183 -0
- package/assets/docs/composition.md +76 -0
- package/assets/docs/persistence.md +56 -0
- package/assets/docs/projects.md +75 -0
- package/assets/docs/recovery.md +56 -0
- package/assets/docs/resources.md +61 -0
- package/assets/docs/workflows.md +57 -0
- package/assets/examples/agent-then-analysis/README.md +103 -0
- package/assets/examples/agent-then-analysis/input.json +5 -0
- package/assets/examples/agent-then-analysis/norn.project.json +4 -0
- package/assets/examples/agent-then-analysis/plugin.ts +89 -0
- package/assets/examples/coordinating-multiple-agents/README.md +56 -0
- package/assets/examples/coordinating-multiple-agents/input.json +10 -0
- package/assets/examples/coordinating-multiple-agents/norn.project.json +4 -0
- package/assets/examples/coordinating-multiple-agents/plugin.ts +102 -0
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +52 -0
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +153 -0
- package/assets/examples/minimal-workflow/README.md +71 -0
- package/assets/examples/minimal-workflow/norn.project.json +4 -0
- package/assets/examples/minimal-workflow/plugin.ts +29 -0
- package/assets/examples/shared-state/README.md +19 -0
- package/assets/examples/shared-state/input.json +1 -0
- package/assets/examples/shared-state/norn.project.json +4 -0
- package/assets/examples/shared-state/plugin.ts +47 -0
- package/assets/examples/worktree-development-loop/README.md +66 -0
- package/assets/examples/worktree-development-loop/index.ts +1 -0
- package/assets/examples/worktree-development-loop/manifest.ts +26 -0
- package/assets/examples/worktree-development-loop/norn.project.json +9 -0
- package/assets/examples/worktree-development-loop/plugin.ts +27 -0
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -0
- package/assets/examples/worktree-development-loop/state.ts +23 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +22 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +14 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +54 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +28 -0
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +53 -0
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +10 -0
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +23 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +12 -0
- package/assets/package.json +1 -0
- package/assets/packages/cli/src/build-info.ts +36 -0
- package/assets/packages/cli/src/bun/cli.ts +16 -0
- package/assets/packages/cli/src/cli.ts +1135 -0
- package/assets/packages/cli/src/client.ts +167 -0
- package/assets/packages/cli/src/documentation-intro.ts +30 -0
- package/assets/packages/cli/src/documentation.ts +149 -0
- package/assets/packages/cli/src/generated-build-info.ts +12 -0
- package/assets/packages/cli/src/internal/agent-directory.ts +5 -0
- package/assets/packages/cli/src/internal/agent-response-tool.ts +96 -0
- package/assets/packages/cli/src/internal/agents.ts +365 -0
- package/assets/packages/cli/src/internal/artifacts.ts +26 -0
- package/assets/packages/cli/src/internal/commands.ts +180 -0
- package/assets/packages/cli/src/internal/documentation-bundle.ts +49 -0
- package/assets/packages/cli/src/internal/engine.ts +501 -0
- package/assets/packages/cli/src/internal/errors.ts +39 -0
- package/assets/packages/cli/src/internal/file-names.ts +3 -0
- package/assets/packages/cli/src/internal/launch-request.ts +94 -0
- package/assets/packages/cli/src/internal/logs.ts +41 -0
- package/assets/packages/cli/src/internal/metrics.ts +356 -0
- package/assets/packages/cli/src/internal/pi-assets.ts +95 -0
- package/assets/packages/cli/src/internal/resource-bindings.ts +35 -0
- package/assets/packages/cli/src/internal/run-lease.ts +158 -0
- package/assets/packages/cli/src/internal/run-log.ts +59 -0
- package/assets/packages/cli/src/internal/run-names.ts +36 -0
- package/assets/packages/cli/src/internal/run-resources.ts +23 -0
- package/assets/packages/cli/src/internal/run-state.ts +380 -0
- package/assets/packages/cli/src/internal/run-store.ts +323 -0
- package/assets/packages/cli/src/internal/run.ts +133 -0
- package/assets/packages/cli/src/internal/state-store.ts +75 -0
- package/assets/packages/cli/src/internal/usage.ts +70 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +176 -0
- package/assets/packages/cli/src/plugin-loader.ts +412 -0
- package/assets/packages/cli/src/resources.ts +67 -0
- package/assets/packages/core/src/agent-protocol.ts +1 -0
- package/assets/packages/core/src/atomic-files.ts +24 -0
- package/assets/packages/core/src/errors.ts +3 -0
- package/assets/packages/sdk/src/agent-resource-adapter.ts +11 -0
- package/assets/packages/sdk/src/api.ts +821 -0
- package/assets/packages/sdk/src/files.ts +136 -0
- package/assets/packages/sdk/src/index.ts +6 -0
- package/assets/packages/sdk/src/resources.ts +20 -0
- package/assets/packages/sdk/src/schema.ts +48 -0
- package/assets/packages/sdk/src/seer/config.ts +62 -0
- package/assets/packages/sdk/src/seer/index.ts +7 -0
- package/assets/packages/sdk/src/state-adapter.ts +75 -0
- package/assets/setup/providers.md +128 -0
- package/assets/setup/releases.md +76 -0
- package/assets/tests/workflow-ref.test.ts +113 -0
- package/bin/norn.mjs +10 -0
- package/dist/build-info.d.ts +30 -0
- package/dist/build-info.js +6 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1032 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +118 -0
- package/dist/documentation-intro.d.ts +5 -0
- package/dist/documentation-intro.js +29 -0
- package/dist/documentation.d.ts +33 -0
- package/dist/documentation.js +132 -0
- package/dist/generated-build-info.d.ts +10 -0
- package/dist/generated-build-info.js +14 -0
- package/dist/internal/agent-directory.d.ts +4 -0
- package/dist/internal/agent-directory.js +8 -0
- package/dist/internal/agent-response-tool.d.ts +21 -0
- package/dist/internal/agent-response-tool.js +79 -0
- package/dist/internal/agents.d.ts +29 -0
- package/dist/internal/agents.js +336 -0
- package/dist/internal/artifacts.d.ts +10 -0
- package/dist/internal/artifacts.js +29 -0
- package/dist/internal/commands.d.ts +18 -0
- package/dist/internal/commands.js +147 -0
- package/dist/internal/documentation-bundle.d.ts +16 -0
- package/dist/internal/documentation-bundle.js +42 -0
- package/dist/internal/engine.d.ts +44 -0
- package/dist/internal/engine.js +399 -0
- package/dist/internal/errors.d.ts +14 -0
- package/dist/internal/errors.js +38 -0
- package/dist/internal/file-names.d.ts +1 -0
- package/dist/internal/file-names.js +7 -0
- package/dist/internal/launch-request.d.ts +33 -0
- package/dist/internal/launch-request.js +110 -0
- package/dist/internal/logs.d.ts +16 -0
- package/dist/internal/logs.js +38 -0
- package/dist/internal/metrics.d.ts +19 -0
- package/dist/internal/metrics.js +282 -0
- package/dist/internal/pi-assets.d.ts +13 -0
- package/dist/internal/pi-assets.js +94 -0
- package/dist/internal/resource-bindings.d.ts +13 -0
- package/dist/internal/resource-bindings.js +34 -0
- package/dist/internal/run-lease.d.ts +32 -0
- package/dist/internal/run-lease.js +166 -0
- package/dist/internal/run-log.d.ts +30 -0
- package/dist/internal/run-log.js +71 -0
- package/dist/internal/run-names.d.ts +1 -0
- package/dist/internal/run-names.js +144 -0
- package/dist/internal/run-resources.d.ts +6 -0
- package/dist/internal/run-resources.js +26 -0
- package/dist/internal/run-state.d.ts +95 -0
- package/dist/internal/run-state.js +323 -0
- package/dist/internal/run-store.d.ts +35 -0
- package/dist/internal/run-store.js +314 -0
- package/dist/internal/run.d.ts +51 -0
- package/dist/internal/run.js +101 -0
- package/dist/internal/state-store.d.ts +22 -0
- package/dist/internal/state-store.js +97 -0
- package/dist/internal/usage.d.ts +5 -0
- package/dist/internal/usage.js +70 -0
- package/dist/internal/workflow-registry.d.ts +35 -0
- package/dist/internal/workflow-registry.js +129 -0
- package/dist/plugin-loader.d.ts +55 -0
- package/dist/plugin-loader.js +353 -0
- package/dist/resources.d.ts +11 -0
- package/dist/resources.js +98 -0
- package/package.json +52 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NornRunMetrics, NornRunStatus } from "@vimhead.dev/norn";
|
|
2
|
+
type NornRunManifestEvent = Record<string, unknown> & {
|
|
3
|
+
readonly at?: string;
|
|
4
|
+
readonly type?: string;
|
|
5
|
+
};
|
|
6
|
+
type NornRunStateSnapshot = {
|
|
7
|
+
readonly status?: NornRunStatus;
|
|
8
|
+
readonly startedAt?: string;
|
|
9
|
+
readonly updatedAt?: string;
|
|
10
|
+
readonly outcome?: {
|
|
11
|
+
readonly completedAt?: string;
|
|
12
|
+
} | null;
|
|
13
|
+
readonly failed?: {
|
|
14
|
+
readonly failedAt?: string;
|
|
15
|
+
} | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function readRunMetrics(runRoot: string): Promise<NornRunMetrics>;
|
|
18
|
+
export declare function calculateRunMetrics(events: readonly NornRunManifestEvent[], state: NornRunStateSnapshot, now: Date): NornRunMetrics;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// src/internal/metrics.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runCurrentRoot } from "./run-store.js";
|
|
5
|
+
import { RUN_STATE_FILE_NAME } from "./run-state.js";
|
|
6
|
+
|
|
7
|
+
// ../core/src/errors.ts
|
|
8
|
+
function isNodeError(error) {
|
|
9
|
+
return error instanceof Error && "code" in error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/internal/metrics.ts
|
|
13
|
+
import { addAgentUsage, agentUsageFromValue, emptyAgentUsage } from "./usage.js";
|
|
14
|
+
var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
15
|
+
async function readRunMetrics(runRoot) {
|
|
16
|
+
const currentRoot = runCurrentRoot(runRoot);
|
|
17
|
+
const [state, events] = await Promise.all([
|
|
18
|
+
readRunStateSnapshot(currentRoot),
|
|
19
|
+
readManifestEvents(join(currentRoot, "manifest.json"))
|
|
20
|
+
]);
|
|
21
|
+
return calculateRunMetrics(events, state, /* @__PURE__ */ new Date());
|
|
22
|
+
}
|
|
23
|
+
function calculateRunMetrics(events, state, now) {
|
|
24
|
+
const startedAt = state.startedAt ?? firstEventTime(events) ?? now.toISOString();
|
|
25
|
+
const endedAt = runEndedAt(state);
|
|
26
|
+
const startedAtMs = timestampMs(startedAt) ?? now.getTime();
|
|
27
|
+
const endedAtMs = timestampMs(endedAt) ?? now.getTime();
|
|
28
|
+
const workflows = [];
|
|
29
|
+
let openWorkflow;
|
|
30
|
+
let gateStartedAtMs;
|
|
31
|
+
let gateWaitMs = 0;
|
|
32
|
+
let agentUsage = emptyAgentUsage();
|
|
33
|
+
for (const event of events) {
|
|
34
|
+
const eventAtMs = timestampMs(event.at);
|
|
35
|
+
if (event.type === "run.interrupted" && eventAtMs !== void 0) gateStartedAtMs = eventAtMs;
|
|
36
|
+
else if (event.type === "run.resumed" && eventAtMs !== void 0 && gateStartedAtMs !== void 0) {
|
|
37
|
+
gateWaitMs += Math.max(0, eventAtMs - gateStartedAtMs);
|
|
38
|
+
gateStartedAtMs = void 0;
|
|
39
|
+
}
|
|
40
|
+
if (event.type === "workflow.started") {
|
|
41
|
+
openWorkflow = {
|
|
42
|
+
workflowId: stringField(event.workflowId, "unknown"),
|
|
43
|
+
startedAt: isoTime(event.at, now),
|
|
44
|
+
startedAtMs: eventAtMs ?? now.getTime(),
|
|
45
|
+
agents: [],
|
|
46
|
+
commands: [],
|
|
47
|
+
openAgents: [],
|
|
48
|
+
openCommands: []
|
|
49
|
+
};
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (event.type === "agent.started") {
|
|
53
|
+
if (openWorkflow) openWorkflow = startAgentMetrics(openWorkflow, event, now);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (event.type === "agent.completed" || event.type === "agent.failed") {
|
|
57
|
+
const usage = agentUsageFromEvent(event);
|
|
58
|
+
agentUsage = addAgentUsage(agentUsage, usage);
|
|
59
|
+
if (openWorkflow) openWorkflow = finishAgentMetrics(openWorkflow, event, usage, now);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (event.type === "command.started") {
|
|
63
|
+
if (openWorkflow) openWorkflow = startCommandMetrics(openWorkflow, event, now);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "command.completed" || event.type === "command.failed") {
|
|
67
|
+
if (openWorkflow) openWorkflow = finishCommandMetrics(openWorkflow, event, now);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const status = workflowTerminalStatus(event.type);
|
|
71
|
+
if (status) {
|
|
72
|
+
const durationMs = numberField(event.durationMs) || (openWorkflow && eventAtMs !== void 0 ? Math.max(0, eventAtMs - openWorkflow.startedAtMs) : 0);
|
|
73
|
+
const workflowId = status === "transitioned" ? stringField(event.fromWorkflowId, openWorkflow?.workflowId ?? "unknown") : stringField(event.workflowId, openWorkflow?.workflowId ?? "unknown");
|
|
74
|
+
const workflow = openWorkflow ?? createSyntheticWorkflowMetrics(workflowId, startedAtMs, endedAtMs, eventAtMs, durationMs);
|
|
75
|
+
const childStatus = status === "failed" ? "failed" : "completed";
|
|
76
|
+
workflows.push(closeWorkflowMetrics(closeOpenChildMetrics(workflow, eventAtMs ?? now.getTime(), event.at, childStatus), workflows.length + 1, status, durationMs, event.at));
|
|
77
|
+
openWorkflow = void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (gateStartedAtMs !== void 0) gateWaitMs += Math.max(0, endedAtMs - gateStartedAtMs);
|
|
81
|
+
if (openWorkflow) {
|
|
82
|
+
workflows.push(closeWorkflowMetrics(closeOpenChildMetrics(openWorkflow, endedAtMs, endedAt, "running"), workflows.length + 1, "running", Math.max(0, endedAtMs - openWorkflow.startedAtMs), endedAt));
|
|
83
|
+
}
|
|
84
|
+
const workflowsMs = workflows.reduce((sum, workflow) => sum + workflow.wallMs, 0);
|
|
85
|
+
const agentsMs = workflows.reduce((sum, workflow) => sum + workflow.agentsMs, 0);
|
|
86
|
+
const commandsMs = workflows.reduce((sum, workflow) => sum + workflow.commandsMs, 0);
|
|
87
|
+
const wallMs = Math.max(0, endedAtMs - startedAtMs);
|
|
88
|
+
return {
|
|
89
|
+
status: state.status ?? "running",
|
|
90
|
+
startedAt,
|
|
91
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
92
|
+
wallMs,
|
|
93
|
+
activeMs: Math.max(0, wallMs - gateWaitMs),
|
|
94
|
+
gateWaitMs,
|
|
95
|
+
workflowsMs,
|
|
96
|
+
workflowOwnMs: Math.max(0, workflowsMs - agentsMs - commandsMs),
|
|
97
|
+
agentsMs,
|
|
98
|
+
commandsMs,
|
|
99
|
+
agentUsage,
|
|
100
|
+
workflows
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function readRunStateSnapshot(currentRoot) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(await readFile(join(currentRoot, RUN_STATE_FILE_NAME), "utf8"));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
108
|
+
return JSON.parse(await readFile(join(currentRoot, LEGACY_RUN_STATE_FILE_NAME), "utf8"));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function readManifestEvents(path) {
|
|
112
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
113
|
+
return manifest.events ?? [];
|
|
114
|
+
}
|
|
115
|
+
function createSyntheticWorkflowMetrics(workflowId, runStartedAtMs, runEndedAtMs, eventAtMs, durationMs) {
|
|
116
|
+
const startedAtMs = eventAtMs === void 0 ? runEndedAtMs : Math.max(runStartedAtMs, eventAtMs - durationMs);
|
|
117
|
+
return {
|
|
118
|
+
workflowId,
|
|
119
|
+
startedAt: isoTimeFromMs(startedAtMs),
|
|
120
|
+
startedAtMs,
|
|
121
|
+
agents: [],
|
|
122
|
+
commands: [],
|
|
123
|
+
openAgents: [],
|
|
124
|
+
openCommands: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function startAgentMetrics(workflow, event, now) {
|
|
128
|
+
return {
|
|
129
|
+
...workflow,
|
|
130
|
+
openAgents: [...workflow.openAgents, {
|
|
131
|
+
label: stringField(event.label, "unknown"),
|
|
132
|
+
startedAt: isoTime(event.at, now),
|
|
133
|
+
startedAtMs: timestampMs(event.at) ?? now.getTime()
|
|
134
|
+
}]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function finishAgentMetrics(workflow, event, usage, now) {
|
|
138
|
+
const [agent, openAgents] = takeOpenChild(workflow.openAgents, stringField(event.label, "unknown"));
|
|
139
|
+
const closedAgent = closeAgentMetrics(event, workflow.agents.length + 1, usage, now, agent);
|
|
140
|
+
return { ...workflow, agents: [...workflow.agents, closedAgent], openAgents };
|
|
141
|
+
}
|
|
142
|
+
function startCommandMetrics(workflow, event, now) {
|
|
143
|
+
return {
|
|
144
|
+
...workflow,
|
|
145
|
+
openCommands: [...workflow.openCommands, {
|
|
146
|
+
label: stringField(event.label, "unknown"),
|
|
147
|
+
startedAt: isoTime(event.at, now),
|
|
148
|
+
startedAtMs: timestampMs(event.at) ?? now.getTime()
|
|
149
|
+
}]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function finishCommandMetrics(workflow, event, now) {
|
|
153
|
+
const [command, openCommands] = takeOpenChild(workflow.openCommands, stringField(event.label, "unknown"));
|
|
154
|
+
const closedCommand = closeCommandMetrics(event, workflow.commands.length + 1, now, command);
|
|
155
|
+
return { ...workflow, commands: [...workflow.commands, closedCommand], openCommands };
|
|
156
|
+
}
|
|
157
|
+
function closeOpenChildMetrics(workflow, endedAtMs, endedAt, status) {
|
|
158
|
+
return {
|
|
159
|
+
...workflow,
|
|
160
|
+
agents: [
|
|
161
|
+
...workflow.agents,
|
|
162
|
+
...workflow.openAgents.map((agent, offset) => ({
|
|
163
|
+
index: workflow.agents.length + offset + 1,
|
|
164
|
+
label: agent.label,
|
|
165
|
+
status,
|
|
166
|
+
startedAt: agent.startedAt,
|
|
167
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
168
|
+
wallMs: Math.max(0, endedAtMs - agent.startedAtMs),
|
|
169
|
+
usage: emptyAgentUsage()
|
|
170
|
+
}))
|
|
171
|
+
],
|
|
172
|
+
commands: [
|
|
173
|
+
...workflow.commands,
|
|
174
|
+
...workflow.openCommands.map((command, offset) => ({
|
|
175
|
+
index: workflow.commands.length + offset + 1,
|
|
176
|
+
label: command.label,
|
|
177
|
+
status,
|
|
178
|
+
startedAt: command.startedAt,
|
|
179
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
180
|
+
wallMs: Math.max(0, endedAtMs - command.startedAtMs)
|
|
181
|
+
}))
|
|
182
|
+
],
|
|
183
|
+
openAgents: [],
|
|
184
|
+
openCommands: []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function closeWorkflowMetrics(workflow, index, status, wallMs, endedAt) {
|
|
188
|
+
const agentsMs = workflow.agents.reduce((sum, agent) => sum + agent.wallMs, 0);
|
|
189
|
+
const commandsMs = workflow.commands.reduce((sum, command) => sum + command.wallMs, 0);
|
|
190
|
+
return {
|
|
191
|
+
index,
|
|
192
|
+
workflowId: workflow.workflowId,
|
|
193
|
+
status,
|
|
194
|
+
startedAt: workflow.startedAt,
|
|
195
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
196
|
+
wallMs,
|
|
197
|
+
ownMs: Math.max(0, wallMs - agentsMs - commandsMs),
|
|
198
|
+
agentsMs,
|
|
199
|
+
commandsMs,
|
|
200
|
+
agentUsage: workflow.agents.reduce((sum, agent) => addAgentUsage(sum, agent.usage), emptyAgentUsage()),
|
|
201
|
+
agents: workflow.agents,
|
|
202
|
+
commands: workflow.commands
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function closeAgentMetrics(event, index, usage, now, openAgent) {
|
|
206
|
+
const endedAtMs = timestampMs(event.at) ?? now.getTime();
|
|
207
|
+
const wallMs = numberField(event.durationMs) || (openAgent ? Math.max(0, endedAtMs - openAgent.startedAtMs) : 0);
|
|
208
|
+
return {
|
|
209
|
+
index,
|
|
210
|
+
label: stringField(event.label, openAgent?.label ?? "unknown"),
|
|
211
|
+
status: event.type === "agent.failed" ? "failed" : "completed",
|
|
212
|
+
startedAt: openAgent?.startedAt ?? isoTimeFromMs(Math.max(0, endedAtMs - wallMs)),
|
|
213
|
+
endedAt: isoTime(event.at, now),
|
|
214
|
+
wallMs,
|
|
215
|
+
attempts: optionalNumberField(event.attempts),
|
|
216
|
+
usage
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function closeCommandMetrics(event, index, now, openCommand) {
|
|
220
|
+
const endedAtMs = timestampMs(event.at) ?? now.getTime();
|
|
221
|
+
const wallMs = numberField(event.durationMs) || (openCommand ? Math.max(0, endedAtMs - openCommand.startedAtMs) : 0);
|
|
222
|
+
return {
|
|
223
|
+
index,
|
|
224
|
+
label: stringField(event.label, openCommand?.label ?? "unknown"),
|
|
225
|
+
status: event.type === "command.failed" ? "failed" : "completed",
|
|
226
|
+
startedAt: openCommand?.startedAt ?? isoTimeFromMs(Math.max(0, endedAtMs - wallMs)),
|
|
227
|
+
endedAt: isoTime(event.at, now),
|
|
228
|
+
wallMs,
|
|
229
|
+
...event.type === "command.completed" ? { exitCode: nullableNumberField(event.exitCode), killed: booleanField(event.killed) } : {}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function takeOpenChild(children, label) {
|
|
233
|
+
const index = children.findIndex((child) => child.label === label);
|
|
234
|
+
if (index === -1) return [void 0, children];
|
|
235
|
+
return [children[index], [...children.slice(0, index), ...children.slice(index + 1)]];
|
|
236
|
+
}
|
|
237
|
+
function workflowTerminalStatus(type) {
|
|
238
|
+
if (type === "workflow.completed") return "completed";
|
|
239
|
+
if (type === "workflow.failed") return "failed";
|
|
240
|
+
if (type === "workflow.transitioned") return "transitioned";
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
function runEndedAt(state) {
|
|
244
|
+
if (state.status === "completed") return state.outcome?.completedAt ?? state.updatedAt;
|
|
245
|
+
if (state.status === "failed") return state.failed?.failedAt ?? state.outcome?.completedAt ?? state.updatedAt;
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
function agentUsageFromEvent(event) {
|
|
249
|
+
return agentUsageFromValue(event.usage) ?? emptyAgentUsage();
|
|
250
|
+
}
|
|
251
|
+
function firstEventTime(events) {
|
|
252
|
+
return events.find((event) => timestampMs(event.at) !== void 0)?.at;
|
|
253
|
+
}
|
|
254
|
+
function timestampMs(value) {
|
|
255
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
256
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
257
|
+
}
|
|
258
|
+
function isoTime(value, now) {
|
|
259
|
+
return value && timestampMs(value) !== void 0 ? value : now.toISOString();
|
|
260
|
+
}
|
|
261
|
+
function isoTimeFromMs(value) {
|
|
262
|
+
return new Date(value).toISOString();
|
|
263
|
+
}
|
|
264
|
+
function numberField(value) {
|
|
265
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
266
|
+
}
|
|
267
|
+
function optionalNumberField(value) {
|
|
268
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
269
|
+
}
|
|
270
|
+
function nullableNumberField(value) {
|
|
271
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
272
|
+
}
|
|
273
|
+
function booleanField(value) {
|
|
274
|
+
return typeof value === "boolean" ? value : false;
|
|
275
|
+
}
|
|
276
|
+
function stringField(value, fallback) {
|
|
277
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
calculateRunMetrics,
|
|
281
|
+
readRunMetrics
|
|
282
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PiAssetFile = {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly content: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function resolvePiAssetsCacheRoot(input: {
|
|
6
|
+
readonly platform: string;
|
|
7
|
+
readonly home: string;
|
|
8
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function materializePiAssets(input: {
|
|
11
|
+
readonly archive: string;
|
|
12
|
+
readonly cacheRoot: string;
|
|
13
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/internal/pi-assets.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { gunzipSync } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
// ../core/src/errors.ts
|
|
8
|
+
function isNodeError(error) {
|
|
9
|
+
return error instanceof Error && "code" in error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/internal/pi-assets.ts
|
|
13
|
+
function resolvePiAssetsCacheRoot(input) {
|
|
14
|
+
if (input.environment.NORN_PI_CACHE_DIR) return resolve(input.environment.NORN_PI_CACHE_DIR);
|
|
15
|
+
if (input.platform === "darwin") return join(input.home, "Library", "Caches", "norn", "pi");
|
|
16
|
+
if (input.platform === "win32") return join(input.environment.LOCALAPPDATA || join(input.home, "AppData", "Local"), "norn", "pi");
|
|
17
|
+
const xdgCache = input.environment.XDG_CACHE_HOME;
|
|
18
|
+
return join(xdgCache?.startsWith("/") ? xdgCache : join(input.home, ".cache"), "norn", "pi");
|
|
19
|
+
}
|
|
20
|
+
async function materializePiAssets(input) {
|
|
21
|
+
const digest = createHash("sha256").update(input.archive).digest("hex");
|
|
22
|
+
const files = JSON.parse(gunzipSync(Buffer.from(input.archive, "base64")).toString("utf8"));
|
|
23
|
+
const expectedFiles = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
if (!file.path || file.path.includes("\\") || file.path.includes(":") || file.path.split("/").some((part) => !part || part === "." || part === "..") || expectedFiles.has(file.path)) {
|
|
26
|
+
throw new Error("Invalid bundled Pi asset path");
|
|
27
|
+
}
|
|
28
|
+
expectedFiles.set(file.path, Buffer.from(file.content, "base64"));
|
|
29
|
+
}
|
|
30
|
+
return new PiAssetCache({ cacheRoot: input.cacheRoot, digest, expectedFiles }).materialize();
|
|
31
|
+
}
|
|
32
|
+
var PiAssetCache = class {
|
|
33
|
+
constructor(input) {
|
|
34
|
+
this.input = input;
|
|
35
|
+
this.root = join(input.cacheRoot, `v1-${input.digest}`);
|
|
36
|
+
}
|
|
37
|
+
input;
|
|
38
|
+
root;
|
|
39
|
+
async materialize() {
|
|
40
|
+
await mkdir(this.input.cacheRoot, { recursive: true, mode: 448 });
|
|
41
|
+
if (!(await lstat(this.input.cacheRoot)).isDirectory()) throw new Error("Pi asset cache root must be a real directory");
|
|
42
|
+
if (await this.validateExistingCache()) return this.root;
|
|
43
|
+
const staging = await mkdtemp(join(this.input.cacheRoot, ".extract-"));
|
|
44
|
+
try {
|
|
45
|
+
for (const [path, content] of this.input.expectedFiles) {
|
|
46
|
+
const parts = path.split("/");
|
|
47
|
+
await mkdir(join(staging, ...parts.slice(0, -1)), { recursive: true, mode: 448 });
|
|
48
|
+
await writeFile(join(staging, ...parts), content, { flag: "wx", mode: 384 });
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await rename(staging, this.root);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!isNodeError(error) || !["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code ?? "") || !await this.validateExistingCache()) throw error;
|
|
54
|
+
}
|
|
55
|
+
if (!await this.validateExistingCache()) throw new Error("Pi assets disappeared after extraction");
|
|
56
|
+
return this.root;
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(staging, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async validateExistingCache() {
|
|
62
|
+
try {
|
|
63
|
+
if (!(await lstat(this.root)).isDirectory()) throw this.invalidCacheError();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
await this.validateDirectory({ directory: this.root, prefix: "", seen });
|
|
70
|
+
if (seen.size !== this.input.expectedFiles.size) throw this.invalidCacheError();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
async validateDirectory(input) {
|
|
74
|
+
for (const entry of await readdir(input.directory, { withFileTypes: true })) {
|
|
75
|
+
const path = input.prefix ? `${input.prefix}/${entry.name}` : entry.name;
|
|
76
|
+
const absolutePath = join(input.directory, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (![...this.input.expectedFiles.keys()].some((file) => file.startsWith(`${path}/`))) throw this.invalidCacheError();
|
|
79
|
+
await this.validateDirectory({ directory: absolutePath, prefix: path, seen: input.seen });
|
|
80
|
+
} else {
|
|
81
|
+
const expected = this.input.expectedFiles.get(path);
|
|
82
|
+
if (!entry.isFile() || !expected || !(await readFile(absolutePath)).equals(expected)) throw this.invalidCacheError();
|
|
83
|
+
input.seen.add(path);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
invalidCacheError() {
|
|
88
|
+
return new Error(`Bundled Pi assets are incomplete or modified: ${this.root}. Remove this cache entry and retry.`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export {
|
|
92
|
+
materializePiAssets,
|
|
93
|
+
resolvePiAssetsCacheRoot
|
|
94
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { NornAgentResourceAdapter } from "@vimhead.dev/norn";
|
|
3
|
+
export declare class NornSessionResourceBindings {
|
|
4
|
+
readonly tools: ToolDefinition[];
|
|
5
|
+
private readonly bindings;
|
|
6
|
+
bind(input: {
|
|
7
|
+
readonly adapters: readonly NornAgentResourceAdapter[];
|
|
8
|
+
readonly runId: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly reservedTools: readonly string[];
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
dispose(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/internal/resource-bindings.ts
|
|
2
|
+
var NornSessionResourceBindings = class {
|
|
3
|
+
tools = [];
|
|
4
|
+
bindings = [];
|
|
5
|
+
async bind(input) {
|
|
6
|
+
const adapterNames = /* @__PURE__ */ new Set();
|
|
7
|
+
const tools = new Set(input.reservedTools);
|
|
8
|
+
for (const adapter of input.adapters) {
|
|
9
|
+
if (adapterNames.has(adapter.name)) throw new Error(`Duplicate resource adapter: ${adapter.name}`);
|
|
10
|
+
adapterNames.add(adapter.name);
|
|
11
|
+
const binding = await adapter.bind({ runId: input.runId, label: input.label });
|
|
12
|
+
this.bindings.push(binding);
|
|
13
|
+
for (const tool of binding.tools) {
|
|
14
|
+
if (tools.has(tool.name)) throw new Error(`Resource tool name collision: ${tool.name}`);
|
|
15
|
+
tools.add(tool.name);
|
|
16
|
+
this.tools.push(tool);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async dispose() {
|
|
21
|
+
const errors = [];
|
|
22
|
+
for (const binding of this.bindings.splice(0).reverse()) {
|
|
23
|
+
try {
|
|
24
|
+
await binding.dispose();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
errors.push(error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (errors.length) throw new AggregateError(errors, "Resource binding cleanup failed");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
NornSessionResourceBindings
|
|
34
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { NornRunHealth } from "@vimhead.dev/norn";
|
|
2
|
+
export type NornRunLeaseOwner = {
|
|
3
|
+
readonly token: string;
|
|
4
|
+
readonly acquiredAt: string;
|
|
5
|
+
readonly heartbeatAt: string;
|
|
6
|
+
readonly pid: number;
|
|
7
|
+
readonly processGroupId: number;
|
|
8
|
+
readonly command: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
export declare class NornRunLease {
|
|
11
|
+
private readonly lockRoot;
|
|
12
|
+
private readonly token;
|
|
13
|
+
private readonly processOwner;
|
|
14
|
+
private heartbeat;
|
|
15
|
+
private heartbeatChain;
|
|
16
|
+
private isReleased;
|
|
17
|
+
private constructor();
|
|
18
|
+
static acquire(runRoot: string, processOwner?: NornRunProcessOwner): Promise<NornRunLease>;
|
|
19
|
+
assertOwned(): Promise<void>;
|
|
20
|
+
release(): Promise<void>;
|
|
21
|
+
private get ownerPath();
|
|
22
|
+
private startHeartbeat;
|
|
23
|
+
private refreshHeartbeat;
|
|
24
|
+
private writeOwner;
|
|
25
|
+
}
|
|
26
|
+
export type NornRunProcessOwner = {
|
|
27
|
+
readonly pid: number;
|
|
28
|
+
readonly processGroupId: number;
|
|
29
|
+
readonly command: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
export declare function getRunLeaseHealth(runRoot: string): Promise<NornRunHealth>;
|
|
32
|
+
export declare function getRunLeaseOwner(runRoot: string): Promise<NornRunLeaseOwner | undefined>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/internal/run-lease.ts
|
|
2
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3
|
+
import { mkdir as mkdir2, readFile, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// ../core/src/errors.ts
|
|
7
|
+
function isNodeError(error) {
|
|
8
|
+
return error instanceof Error && "code" in error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../core/src/atomic-files.ts
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
async function writeJsonAtomically(path, value) {
|
|
16
|
+
await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
async function writeTextAtomically(path, content) {
|
|
20
|
+
await mkdir(dirname(path), { recursive: true });
|
|
21
|
+
let existingMode;
|
|
22
|
+
try {
|
|
23
|
+
existingMode = (await stat(path)).mode & 511;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
26
|
+
}
|
|
27
|
+
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
|
|
30
|
+
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
31
|
+
await rename(tmpPath, path);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
await rm(tmpPath, { force: true }).catch(() => void 0);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/internal/run-lease.ts
|
|
39
|
+
var LOCK_DIR_NAME = "active.lock";
|
|
40
|
+
var OWNER_FILE_NAME = "owner.json";
|
|
41
|
+
var HEARTBEAT_INTERVAL_MS = 15e3;
|
|
42
|
+
var HEARTBEAT_TTL_MS = 6e4;
|
|
43
|
+
var ACQUIRE_ATTEMPTS = 5;
|
|
44
|
+
var NornRunLease = class _NornRunLease {
|
|
45
|
+
constructor(lockRoot, token, processOwner) {
|
|
46
|
+
this.lockRoot = lockRoot;
|
|
47
|
+
this.token = token;
|
|
48
|
+
this.processOwner = processOwner;
|
|
49
|
+
}
|
|
50
|
+
lockRoot;
|
|
51
|
+
token;
|
|
52
|
+
processOwner;
|
|
53
|
+
heartbeat;
|
|
54
|
+
heartbeatChain = Promise.resolve();
|
|
55
|
+
isReleased = false;
|
|
56
|
+
static async acquire(runRoot, processOwner = currentRunProcessOwner()) {
|
|
57
|
+
const lockRoot = join(runRoot, LOCK_DIR_NAME);
|
|
58
|
+
for (let attempt = 1; attempt <= ACQUIRE_ATTEMPTS; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
await mkdir2(lockRoot);
|
|
61
|
+
const lease = new _NornRunLease(lockRoot, randomUUID2(), processOwner);
|
|
62
|
+
try {
|
|
63
|
+
await lease.writeOwner({ acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
await rm2(lockRoot, { recursive: true, force: true });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
lease.startHeartbeat();
|
|
69
|
+
return lease;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isNodeError(error) || error.code !== "EEXIST") throw error;
|
|
72
|
+
const owner = await readRunLeaseOwner(join(lockRoot, OWNER_FILE_NAME));
|
|
73
|
+
const isStale = owner ? isRunLeaseStale(owner) : await isRunLeaseDirectoryStale(lockRoot);
|
|
74
|
+
if (!isStale) throw new Error(`Run is already active: ${runRoot}`);
|
|
75
|
+
await rm2(lockRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Could not acquire run lease: ${runRoot}`);
|
|
79
|
+
}
|
|
80
|
+
async assertOwned() {
|
|
81
|
+
if (this.isReleased) throw new Error("Run lease has been released");
|
|
82
|
+
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
83
|
+
if (!owner || owner.token !== this.token) throw new Error("Run lease was lost");
|
|
84
|
+
}
|
|
85
|
+
async release() {
|
|
86
|
+
if (this.isReleased) return;
|
|
87
|
+
this.isReleased = true;
|
|
88
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
89
|
+
await this.heartbeatChain.catch(() => void 0);
|
|
90
|
+
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
91
|
+
if (owner?.token === this.token) await rm2(this.lockRoot, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
get ownerPath() {
|
|
94
|
+
return join(this.lockRoot, OWNER_FILE_NAME);
|
|
95
|
+
}
|
|
96
|
+
startHeartbeat() {
|
|
97
|
+
this.heartbeat = setInterval(() => {
|
|
98
|
+
this.heartbeatChain = this.heartbeatChain.then(() => this.refreshHeartbeat()).catch(() => {
|
|
99
|
+
this.isReleased = true;
|
|
100
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
101
|
+
});
|
|
102
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
103
|
+
this.heartbeat.unref?.();
|
|
104
|
+
}
|
|
105
|
+
async refreshHeartbeat() {
|
|
106
|
+
if (this.isReleased) return;
|
|
107
|
+
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
108
|
+
if (!owner || owner.token !== this.token) throw new Error("Run lease was lost");
|
|
109
|
+
await this.writeOwner({ acquiredAt: owner.acquiredAt });
|
|
110
|
+
}
|
|
111
|
+
async writeOwner(input) {
|
|
112
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
113
|
+
await writeJsonAtomically(this.ownerPath, { token: this.token, acquiredAt: input.acquiredAt, heartbeatAt: now, ...this.processOwner });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
async function getRunLeaseHealth(runRoot) {
|
|
117
|
+
const lockRoot = join(runRoot, LOCK_DIR_NAME);
|
|
118
|
+
const owner = await readRunLeaseOwner(join(lockRoot, OWNER_FILE_NAME));
|
|
119
|
+
if (owner) return isRunLeaseStale(owner) ? "unhealthy" : "healthy";
|
|
120
|
+
return await isRunLeaseDirectoryStale(lockRoot) ? "unhealthy" : "healthy";
|
|
121
|
+
}
|
|
122
|
+
async function getRunLeaseOwner(runRoot) {
|
|
123
|
+
return readRunLeaseOwner(join(runRoot, LOCK_DIR_NAME, OWNER_FILE_NAME));
|
|
124
|
+
}
|
|
125
|
+
function currentRunProcessOwner() {
|
|
126
|
+
return {
|
|
127
|
+
pid: process.pid,
|
|
128
|
+
processGroupId: process.pid,
|
|
129
|
+
command: process.argv
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function readRunLeaseOwner(path) {
|
|
133
|
+
try {
|
|
134
|
+
return parseRunLeaseOwner(JSON.parse(await readFile(path, "utf8")));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function parseRunLeaseOwner(value) {
|
|
141
|
+
if (!value || typeof value !== "object") return void 0;
|
|
142
|
+
const owner = value;
|
|
143
|
+
if (typeof owner.token !== "string" || owner.token.length === 0) return void 0;
|
|
144
|
+
if (typeof owner.acquiredAt !== "string" || Number.isNaN(Date.parse(owner.acquiredAt))) return void 0;
|
|
145
|
+
if (typeof owner.heartbeatAt !== "string" || Number.isNaN(Date.parse(owner.heartbeatAt))) return void 0;
|
|
146
|
+
const pid = typeof owner.pid === "number" && Number.isInteger(owner.pid) ? owner.pid : 0;
|
|
147
|
+
const processGroupId = typeof owner.processGroupId === "number" && Number.isInteger(owner.processGroupId) ? owner.processGroupId : pid;
|
|
148
|
+
const command = Array.isArray(owner.command) ? owner.command.filter((value2) => typeof value2 === "string") : [];
|
|
149
|
+
return { token: owner.token, acquiredAt: owner.acquiredAt, heartbeatAt: owner.heartbeatAt, pid, processGroupId, command };
|
|
150
|
+
}
|
|
151
|
+
function isRunLeaseStale(owner) {
|
|
152
|
+
return Date.now() - Date.parse(owner.heartbeatAt) > HEARTBEAT_TTL_MS;
|
|
153
|
+
}
|
|
154
|
+
async function isRunLeaseDirectoryStale(lockRoot) {
|
|
155
|
+
try {
|
|
156
|
+
return Date.now() - (await stat2(lockRoot)).mtimeMs > HEARTBEAT_TTL_MS;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (isNodeError(error) && error.code === "ENOENT") return true;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
NornRunLease,
|
|
164
|
+
getRunLeaseHealth,
|
|
165
|
+
getRunLeaseOwner
|
|
166
|
+
};
|