@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.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 +145 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +442 -162
- package/dist/src/background-widget.d.ts +5 -1
- package/dist/src/background-widget.js +5 -10
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +59 -37
- package/dist/src/execution.d.ts +6 -2
- package/dist/src/execution.js +48 -7
- package/dist/src/host-delivery.js +5 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +11 -8
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +6 -1
- package/dist/src/host-view.js +46 -20
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -118
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1985 -1690
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +8 -5
- package/dist/src/paths.js +24 -13
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +29 -71
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +19 -3
- package/dist/src/settings.js +63 -50
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +12 -3
- package/dist/src/trajectory.js +32 -11
- package/dist/src/types.d.ts +129 -45
- package/dist/src/types.js +5 -8
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +46 -6
- package/dist/src/validation.d.ts +14 -3
- package/dist/src/validation.js +143 -87
- package/dist/starter/index.js +8 -11
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +8 -9
- package/dist/subagents/index.js +904 -956
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +48 -31
- package/dist/subagents/src/contracts.js +20 -9
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +14 -8
- package/dist/subagents/src/manager.js +52 -73
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +1 -1
- package/dist/subagents/src/view.js +15 -68
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +35 -13
- package/dist/trajectory/index.js +576 -932
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +35 -13
- package/dist/trajectory/src/index.js +134 -104
- package/dist/trajectory/src/server.js +268 -27
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +10 -10
- package/skills/pi-extensible-workflows/SKILL.md +12 -8
- package/src/agent-execution.ts +412 -164
- package/src/background-widget.ts +5 -11
- package/src/decoders.ts +51 -37
- package/src/execution.ts +46 -9
- package/src/host-delivery.ts +5 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +23 -28
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +41 -16
- package/src/host.ts +229 -142
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -9
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +28 -64
- package/src/runtime/agent-runner.ts +2 -0
- package/src/script-tools.ts +76 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +56 -50
- package/src/store.ts +18 -2
- package/src/trajectory.ts +32 -15
- package/src/types.ts +82 -34
- package/src/utils.ts +42 -5
- package/src/validation.ts +121 -80
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -9
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +10 -16
- package/subagents/src/contracts.ts +21 -11
- package/subagents/src/index.ts +17 -8
- package/subagents/src/manager.ts +48 -128
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +16 -63
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +35 -13
- package/trajectory/src/index.ts +112 -91
- package/trajectory/src/server.ts +46 -10
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -325
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -326
package/src/index.ts
CHANGED
|
@@ -3,10 +3,9 @@ export * from "./package-identity.js";
|
|
|
3
3
|
export { createToolTimingExtension, TOOL_TIMING_ENTRY_TYPE } from "./tool-timing.js";
|
|
4
4
|
export type { ToolTimingEntry } from "./tool-timing.js";
|
|
5
5
|
export * from "./utils.js";
|
|
6
|
-
export { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
6
|
+
export { canonicalPath, extensionIdentity, sameFilesystemPath } from "./paths.js";
|
|
7
7
|
export * from "./budget.js";
|
|
8
8
|
export * from "./validation.js";
|
|
9
|
-
export * from "./roles.js";
|
|
10
9
|
export * from "./registry.js";
|
|
11
10
|
export * from "./execution.js";
|
|
12
11
|
export * from "./host.js";
|
package/src/paths.ts
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { readdir } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { WorkflowError } from "./types.js";
|
|
7
8
|
import { isNodeError } from "./utils.js";
|
|
8
9
|
|
|
9
10
|
export function safePart(value: string): string { return value.replace(/[^a-zA-Z0-9._-]/g, "_"); }
|
|
10
11
|
|
|
12
|
+
export function canonicalPath(path: string): string { const absolute = resolve(path); try { return realpathSync(absolute); } catch { return absolute; } }
|
|
13
|
+
export function extensionIdentity(path: string): string { return path.startsWith("builtin:") ? path : canonicalPath(path.startsWith("file:") ? fileURLToPath(path) : path); }
|
|
14
|
+
export function sameFilesystemPath(left: string, right: string): boolean { return canonicalPath(left) === canonicalPath(right); }
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* guessing at the identity of an object that does not exist.
|
|
16
|
+
* The path Pi's resource loader uses for a resource path an extension returns: trimmed, `~`, `~/` and `file://` expanded,
|
|
17
|
+
* relative to the loader cwd. Synthetic `<...>` and `builtin:` paths stay as they are.
|
|
15
18
|
*/
|
|
16
|
-
export function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
export function piResourcePath(path: string, cwd: string): string {
|
|
20
|
+
//NOTE: Pi also rewrites Windows shell paths and `~\` on win32; those forms are not mirrored here and fail closed.
|
|
21
|
+
if (path.startsWith("<") || path.startsWith("builtin:")) return path;
|
|
22
|
+
const trimmed = path.trim();
|
|
23
|
+
const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : /^file:\/\//.test(trimmed) ? fileURLToPath(trimmed) : trimmed;
|
|
24
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
25
|
+
}
|
|
26
|
+
/** Pi attributes a loaded resource to a contributed path by lexical containment of resolved paths, without following symlinks. */
|
|
27
|
+
export function piResourceContains(root: string, path: string): boolean {
|
|
28
|
+
const relativePath = relative(resolve(root), resolve(path));
|
|
29
|
+
return relativePath === "" || (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
|
|
19
30
|
}
|
|
20
|
-
|
|
21
|
-
export function sameFilesystemPath(left: string, right: string): boolean { return canonicalPath(left) === canonicalPath(right); }
|
|
22
31
|
export function workflowProjectSettingsPath(cwd: string): string { return join(cwd, ".pi", "pi-extensible-workflows", "settings.json"); }
|
|
23
32
|
|
|
24
33
|
export function projectStorageKey(cwd: string): string {
|
package/src/persistence.ts
CHANGED
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
} from "./paths.js";
|
|
5
5
|
export { hasLiveSessionLease, SessionLease, acquireSessionLease, listRunIds, processAlive } from "./session-lease.js";
|
|
6
6
|
export { RunStore } from "./store.js";
|
|
7
|
+
export { decodePreparedAgentConfiguration } from "./decoders.js";
|
|
7
8
|
export {
|
|
8
9
|
EffectiveSystemPrompt, PersistedRun, RunSummaryAgent, RunSummaryArtifacts, RunSummary,
|
|
9
10
|
CompletedOperation, AwaitingCheckpoint, PendingWorkflowDecision, PersistedOwnershipNode,
|
package/src/pi-runtime-runner.ts
CHANGED
|
@@ -172,7 +172,7 @@ export class PiRuntimeAgentRunner implements RuntimeAgentRunner {
|
|
|
172
172
|
let structuredResult: RuntimeJsonValue | undefined;
|
|
173
173
|
const resultTool = prepared.resultTool;
|
|
174
174
|
if (!resultTool) throw new WorkflowError("INTERNAL_ERROR", "Workflow agent session is missing its workflow_result tool");
|
|
175
|
-
const preparedForSession = preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool);
|
|
175
|
+
const preparedForSession = Object.freeze({ ...preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool), ...(request.turnControl ? { turnControl: request.turnControl } : {}) });
|
|
176
176
|
const callbacks = this.#options.callbacks;
|
|
177
177
|
const handoff = this.#options.handoff;
|
|
178
178
|
let adapter: PiRuntimeSessionAdapter | undefined;
|
|
@@ -364,6 +364,8 @@ export class PiRuntimeAgentRunner implements RuntimeAgentRunner {
|
|
|
364
364
|
throwIfProgressFailed();
|
|
365
365
|
beginTurn(false);
|
|
366
366
|
throwIfTurnPolicyFailed();
|
|
367
|
+
// NOTE: A transport that already sent turnControl (Herdr, in its pane launch prompt) reports turnControlDelivered; steering its suspended local session would fail.
|
|
368
|
+
if (request.turnControl && !session.turnControlDelivered) await activeSession.steer(request.turnControl);
|
|
367
369
|
await promptAndRecover(request.task, true);
|
|
368
370
|
completeTurn(hasResult() || !hasToolCall(lastAssistant), false, true);
|
|
369
371
|
if (turnPolicyFailure) throw turnPolicyFailure;
|
package/src/registry.ts
CHANGED
|
@@ -1,48 +1,16 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import { Value } from "typebox/value";
|
|
5
|
-
import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext
|
|
2
|
+
import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, RegisteredAgentPreparationHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext } from "./types.js";
|
|
6
3
|
import type { SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
|
|
7
4
|
import { byPriorityThenName, deepFreeze, errorCode, errorText, fail, jsonValue, MODEL_ALIAS_NAME, object, validWorkflowExtensionNamespace } from "./utils.js";
|
|
8
5
|
import { loadSettings, resolveWorkflowSettings } from "./settings.js";
|
|
9
6
|
import { validateSchema } from "./utils.js";
|
|
10
|
-
import { canonicalPath } from "./paths.js";
|
|
11
|
-
import { isCorePackageName } from "./package-identity.js";
|
|
12
7
|
|
|
13
|
-
const RESERVED_GLOBALS = new Set(["agent", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
8
|
+
const RESERVED_GLOBALS = new Set(["agent", "shell", "tools", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
14
9
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
15
10
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
16
11
|
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
17
12
|
export type SubagentStatusObserver = (status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>) => void;
|
|
18
13
|
|
|
19
|
-
function normalizeRoleDirectory(value: unknown): string {
|
|
20
|
-
try {
|
|
21
|
-
if (value instanceof URL) {
|
|
22
|
-
if (value.protocol !== "file:") fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
23
|
-
return fileURLToPath(value);
|
|
24
|
-
}
|
|
25
|
-
if (typeof value === "string") {
|
|
26
|
-
if (value.startsWith("file:")) return fileURLToPath(value);
|
|
27
|
-
if (isAbsolute(value)) return value;
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
fail("INVALID_METADATA", `Invalid workflow role directory: ${errorText(error)}`);
|
|
31
|
-
}
|
|
32
|
-
fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
33
|
-
}
|
|
34
|
-
function isBuiltinRoleDirectory(path: string): boolean {
|
|
35
|
-
const canonical = canonicalPath(path);
|
|
36
|
-
const starterDirectory = dirname(canonical);
|
|
37
|
-
if (basename(canonical) !== "roles" || basename(starterDirectory) !== "starter") return false;
|
|
38
|
-
const distributionDirectory = dirname(starterDirectory);
|
|
39
|
-
const packageRoot = basename(distributionDirectory) === "dist" ? dirname(distributionDirectory) : distributionDirectory;
|
|
40
|
-
try {
|
|
41
|
-
const metadata: unknown = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
42
|
-
return object(metadata) && isCorePackageName(metadata.name);
|
|
43
|
-
} catch { return false; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
14
|
function catalogSchema(schema: unknown, at: string): JsonSchema {
|
|
47
15
|
validateSchema(schema, at);
|
|
48
16
|
return structuredClone(schema);
|
|
@@ -51,9 +19,9 @@ export class WorkflowRegistry {
|
|
|
51
19
|
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
52
20
|
readonly #functionSources = new Map<string, WorkflowFunctionSource>();
|
|
53
21
|
readonly #globals = new Map<string, string>();
|
|
22
|
+
readonly #preparationHooks = new Map<string, RegisteredAgentPreparationHook>();
|
|
54
23
|
readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
|
|
55
24
|
readonly #agentAttemptActions = new Map<string, AgentAttemptAction>();
|
|
56
|
-
readonly #roleDirectories = new Map<string, WorkflowRoleDirectoryRegistration>();
|
|
57
25
|
readonly #modelAliases = new Map<string, { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }>();
|
|
58
26
|
readonly #settingsValidators = new Set<{ headline: string; validate: NonNullable<WorkflowExtension["validateSettings"]> }>();
|
|
59
27
|
#frozen = false;
|
|
@@ -67,9 +35,10 @@ export class WorkflowRegistry {
|
|
|
67
35
|
register(extension: WorkflowExtension): void {
|
|
68
36
|
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
69
37
|
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
70
|
-
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "
|
|
38
|
+
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentPreparationHooks", "agentSetupHooks", "agentAttemptActions"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
|
|
71
39
|
const functions = extension.functions ?? {};
|
|
72
40
|
const modelAliases = extension.modelAliases ?? {};
|
|
41
|
+
const agentPreparationHooks = extension.agentPreparationHooks ?? {};
|
|
73
42
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
74
43
|
const agentAttemptActions = extension.agentAttemptActions ?? {};
|
|
75
44
|
const source = extension.source;
|
|
@@ -79,10 +48,7 @@ export class WorkflowRegistry {
|
|
|
79
48
|
if (validateSettings !== undefined && typeof validateSettings !== "function") fail("INVALID_METADATA", "Workflow extension validateSettings must be a function");
|
|
80
49
|
if (source !== undefined && (typeof source !== "string" || !source.trim())) fail("INVALID_METADATA", "Workflow extension source must be a non-empty module URL");
|
|
81
50
|
if (dependencyValues !== undefined && (!Array.isArray(dependencyValues) || dependencyValues.some((dependency) => typeof dependency !== "string" || dependency.trim() !== dependency || !PACKAGE_NAME.test(dependency)) || new Set(dependencyValues).size !== dependencyValues.length)) fail("INVALID_METADATA", "Workflow extension dependencies must be unique, non-empty package names");
|
|
82
|
-
|
|
83
|
-
if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
|
|
84
|
-
const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
|
|
85
|
-
if (!object(functions) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent setup hooks, agent attempt actions, role directories, or a settings validator");
|
|
51
|
+
if (!object(functions) || !object(modelAliases) || !object(agentPreparationHooks) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentPreparationHooks).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent preparation hooks, agent setup hooks, agent attempt actions, or a settings validator");
|
|
86
52
|
const names = Object.keys(functions);
|
|
87
53
|
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
88
54
|
for (const name of names) {
|
|
@@ -101,6 +67,11 @@ export class WorkflowRegistry {
|
|
|
101
67
|
if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function") fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
|
|
102
68
|
if (this.#modelAliases.has(name)) fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
|
|
103
69
|
}
|
|
70
|
+
for (const [name, hook] of Object.entries(agentPreparationHooks)) {
|
|
71
|
+
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "optionsSchema", "prepare"].includes(key)) || typeof hook.prepare !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent preparation hook: ${name}`);
|
|
72
|
+
if (this.#preparationHooks.has(name)) fail("DUPLICATE_NAME", `Agent preparation hook already registered: ${name}`);
|
|
73
|
+
if (hook.optionsSchema !== undefined) validateSchema(hook.optionsSchema, `${name} options schema`);
|
|
74
|
+
}
|
|
104
75
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
105
76
|
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
106
77
|
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
@@ -110,17 +81,22 @@ export class WorkflowRegistry {
|
|
|
110
81
|
if ((action.visibleStandalone !== undefined) !== (action.runStandalone !== undefined)) fail("INVALID_METADATA", `Standalone agent attempt actions require visibleStandalone and runStandalone: ${name}`);
|
|
111
82
|
if (this.#agentAttemptActions.has(name)) fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
|
|
112
83
|
}
|
|
113
|
-
const stored = deepFreeze({ ...extension, functions, modelAliases, agentSetupHooks, agentAttemptActions
|
|
84
|
+
const stored = deepFreeze({ ...extension, functions, modelAliases, agentPreparationHooks, agentSetupHooks, agentAttemptActions });
|
|
114
85
|
if (source !== undefined) for (const name of names) this.#functionSources.set(name, Object.freeze({ module: source, export: "default", dependencies: Object.freeze([...dependencies]) }));
|
|
115
86
|
this.#extensions.add(stored);
|
|
116
|
-
for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline }, ...(isBuiltinRoleDirectory(directory) ? { builtin: true as const } : {}) }));
|
|
117
87
|
for (const name of names) this.#globals.set(name, name);
|
|
118
88
|
for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, resolve: alias.resolve });
|
|
89
|
+
for (const [name, hook] of Object.entries(agentPreparationHooks)) this.#preparationHooks.set(name, { name, priority: hook.priority ?? 10, prepare: hook.prepare, ...(hook.optionsSchema === undefined ? {} : { optionsSchema: structuredClone(hook.optionsSchema) }), ...(source === undefined ? {} : { source }) });
|
|
119
90
|
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
120
91
|
for (const [name, action] of Object.entries(agentAttemptActions)) this.#agentAttemptActions.set(name, action);
|
|
121
92
|
if (validateSettings !== undefined) this.#settingsValidators.add({ headline: extension.headline, validate: validateSettings });
|
|
122
93
|
}
|
|
123
94
|
|
|
95
|
+
/** Every registration's headline and declared module provenance, for hosts that must prove which extension made it. */
|
|
96
|
+
registrations(): ReadonlyArray<Readonly<{ headline: string; source?: string }>> {
|
|
97
|
+
return [...this.#extensions].map(({ headline, source }) => Object.freeze({ headline, ...(source === undefined ? {} : { source }) }));
|
|
98
|
+
}
|
|
99
|
+
|
|
124
100
|
function(name: string): WorkflowFunction {
|
|
125
101
|
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
|
|
126
102
|
const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
|
|
@@ -145,7 +121,7 @@ export class WorkflowRegistry {
|
|
|
145
121
|
let source = "global settings";
|
|
146
122
|
try {
|
|
147
123
|
const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
|
|
148
|
-
aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
|
|
124
|
+
aliases = resolved?.effective.modelAliases ?? (context ? loadSettings().modelAliases : resolveWorkflowSettings(process.cwd(), false).effective.modelAliases);
|
|
149
125
|
if (resolved) { source = resolved.projectTrusted && [resolved.sources.modelAliases, resolved.sources.skills, resolved.sources.extensions, resolved.sources.tools].some((path) => path === resolved.projectSettingsPath) ? "trusted project settings" : "global settings"; settings = { concurrency: resolved.effective.concurrency, backgroundWidget: resolved.effective.backgroundWidget ?? true, modelAliases: resolved.effective.modelAliases ?? {}, skills: resolved.effective.skills ?? [], extensions: Array.isArray(resolved.effective.extensions) ? resolved.effective.extensions : [], tools: resolved.effective.tools ?? [], ...(resolved.effective.extensionSettings === undefined ? {} : { extensionSettings: resolved.effective.extensionSettings }), ...(resolved.effective.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: resolved.effective.worktreePostCreateCommand }), globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources }; }
|
|
150
126
|
} catch { aliases = undefined; }
|
|
151
127
|
const staticEntries: WorkflowCatalogModelAlias[] = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
|
|
@@ -197,7 +173,7 @@ export class WorkflowRegistry {
|
|
|
197
173
|
for (const { headline, validate } of this.#settingsValidators) {
|
|
198
174
|
try { validate(frozenSettings, Object.freeze({ ...context })); }
|
|
199
175
|
catch (error) {
|
|
200
|
-
const location = context.source
|
|
176
|
+
const location = context.source;
|
|
201
177
|
fail("INVALID_SETTINGS", `Invalid extension settings (${headline}; ${location}): ${errorText(error)}`);
|
|
202
178
|
}
|
|
203
179
|
}
|
|
@@ -217,16 +193,11 @@ export class WorkflowRegistry {
|
|
|
217
193
|
return structuredClone(stored);
|
|
218
194
|
}
|
|
219
195
|
|
|
196
|
+
agentPreparationHooks(): readonly RegisteredAgentPreparationHook[] { return [...this.#preparationHooks.values()].sort(byPriorityThenName); }
|
|
220
197
|
agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
|
|
221
198
|
return [...this.#hooks.values()].sort(byPriorityThenName);
|
|
222
199
|
}
|
|
223
200
|
agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>> { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
|
|
224
|
-
roleDirectories(): readonly string[] {
|
|
225
|
-
return [...this.#roleDirectories.keys()];
|
|
226
|
-
}
|
|
227
|
-
roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
228
|
-
return [...this.#roleDirectories.values()];
|
|
229
|
-
}
|
|
230
201
|
modelAliases(): readonly { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }[] { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
|
|
231
202
|
async resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed: ReadonlySet<string> = new Set()): Promise<Readonly<Record<string, string>>> {
|
|
232
203
|
const resolved: Record<string, string> = {};
|
|
@@ -247,7 +218,7 @@ export class WorkflowRegistry {
|
|
|
247
218
|
return Object.freeze(resolved);
|
|
248
219
|
}
|
|
249
220
|
}
|
|
250
|
-
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "
|
|
221
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentPreparationHooks" | "agentSetupHooks" | "agentAttemptActions" | "setSubagentStatusObserver" | "observeSubagentStatus" | "registrations">;
|
|
251
222
|
interface WorkflowRegistryHost { api: WorkflowRegistryApi; activeHosts: number }
|
|
252
223
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
253
224
|
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
@@ -269,10 +240,10 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
|
|
|
269
240
|
validateExtensionSettings: (...args) => { registry.validateExtensionSettings(...args); },
|
|
270
241
|
modelAliases: () => registry.modelAliases(),
|
|
271
242
|
resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
|
|
272
|
-
|
|
273
|
-
roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
|
|
243
|
+
agentPreparationHooks: () => registry.agentPreparationHooks(),
|
|
274
244
|
agentSetupHooks: () => registry.agentSetupHooks(),
|
|
275
245
|
agentAttemptActions: () => registry.agentAttemptActions(),
|
|
246
|
+
registrations: () => registry.registrations(),
|
|
276
247
|
};
|
|
277
248
|
}
|
|
278
249
|
function workflowRegistryHost(): WorkflowRegistryHost {
|
|
@@ -301,7 +272,9 @@ export function retainWorkflowRegistry(): () => void {
|
|
|
301
272
|
}
|
|
302
273
|
export function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().api; }
|
|
303
274
|
beginWorkflowExtensionLoading();
|
|
304
|
-
export function registerWorkflowExtension(extension: WorkflowExtension): void {
|
|
275
|
+
export function registerWorkflowExtension(extension: WorkflowExtension): void {
|
|
276
|
+
loadingRegistry().register(extension);
|
|
277
|
+
}
|
|
305
278
|
export function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog { return loadingRegistry().catalog(context); }
|
|
306
279
|
export function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(context); }
|
|
307
280
|
export function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogModelAlias | WorkflowCatalogError { return loadingRegistry().catalogDetail(name, context); }
|
|
@@ -310,13 +283,4 @@ export function registeredWorkflowFunctionSources(): Readonly<Record<string, Wor
|
|
|
310
283
|
const sources = loadingRegistry().functionSources;
|
|
311
284
|
return typeof sources === "function" ? sources() : {};
|
|
312
285
|
}
|
|
313
|
-
export
|
|
314
|
-
const directories = loadingRegistry().roleDirectories;
|
|
315
|
-
return typeof directories === "function" ? directories() : [];
|
|
316
|
-
}
|
|
317
|
-
export function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
318
|
-
const registrations = loadingRegistry().roleDirectoryRegistrations;
|
|
319
|
-
return typeof registrations === "function" ? registrations() : [];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowRoleDirectoryRegistration } from "./types.js";
|
|
286
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings } from "./types.js";
|
|
@@ -121,6 +121,8 @@ export interface RuntimeAgentRunRequest {
|
|
|
121
121
|
readonly customTools: readonly RuntimeTool[];
|
|
122
122
|
/** Explicit result schema; omitted agents use the default string workflow_result schema. */
|
|
123
123
|
readonly resultSchema?: RuntimeJsonSchema;
|
|
124
|
+
/** Transient retry or budget text delivered as a separate steered message after the task, never inside it. */
|
|
125
|
+
readonly turnControl?: string;
|
|
124
126
|
readonly run: RuntimeRunIdentity;
|
|
125
127
|
readonly agent: RuntimeAgentIdentity;
|
|
126
128
|
readonly signal: AbortSignal;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExtensionToolContext, ToolLoadout, ToolLoadoutChanges } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { WorkflowError, type JsonValue } from "./types.js";
|
|
3
|
+
import { errorText, jsonValue, object } from "./utils.js";
|
|
4
|
+
import { RPC_LIMIT_BYTES } from "./execution.js";
|
|
5
|
+
import { scriptToolReferences } from "./validation.js";
|
|
6
|
+
|
|
7
|
+
type ScriptTool = ExtensionToolContext["tools"][number];
|
|
8
|
+
type ScriptToolOutcome = Awaited<ReturnType<ExtensionToolContext["executeTool"]>>;
|
|
9
|
+
|
|
10
|
+
/** Workflow and subagent tools stay out of scripts, so a run cannot launch or control runs. Codemode is model-only and never callable. */
|
|
11
|
+
export function scriptToolExcluded(name: string): boolean {
|
|
12
|
+
return name === "workflow" || name.startsWith("workflow_") || name.startsWith("subagents_");
|
|
13
|
+
}
|
|
14
|
+
/** The identifier scripts use for a tool, the same as Pi codemode's, so its `tools.<id>(args)` lines apply to workflow scripts. */
|
|
15
|
+
export function scriptToolIdentifier(name: string): string {
|
|
16
|
+
let identifier = "";
|
|
17
|
+
for (const char of name) identifier += (identifier === "" ? /^[A-Za-z_$]$/ : /^[A-Za-z0-9_$]$/).test(char) ? char : "_";
|
|
18
|
+
return identifier || "_";
|
|
19
|
+
}
|
|
20
|
+
/** The context of a Pi tool call, whose `executeTool()` runs session tools; absent for commands, session events, and headless launches. */
|
|
21
|
+
export function scriptToolContext(ctx: unknown): ExtensionToolContext | undefined {
|
|
22
|
+
return object(ctx) && typeof ctx.executeTool === "function" && Array.isArray(ctx.tools) ? ctx as unknown as ExtensionToolContext : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function scriptToolIdentifiers(context: ExtensionToolContext | undefined): string[] {
|
|
25
|
+
return (context?.tools ?? []).filter(({ name }) => !scriptToolExcluded(name)).map(({ name }) => scriptToolIdentifier(name));
|
|
26
|
+
}
|
|
27
|
+
// NOTE: a launch rejects identifiers that several tools share; a later tool set may still collide, and the first match wins.
|
|
28
|
+
export function scriptTool(context: ExtensionToolContext, identifier: string): ScriptTool | undefined {
|
|
29
|
+
return context.tools.find(({ name }) => !scriptToolExcluded(name) && scriptToolIdentifier(name) === identifier);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What a script call resolves to, as in codemode: the structured content of a tool with an output
|
|
33
|
+
* schema, also for error results that carry one, otherwise its text; other failures throw. The value
|
|
34
|
+
* is normalized through JSON and size-checked before it is journaled, so a replay returns the same value.
|
|
35
|
+
*/
|
|
36
|
+
export function scriptToolValue(tool: ScriptTool, outcome: ScriptToolOutcome): JsonValue {
|
|
37
|
+
const structured = tool.outputSchema === undefined ? undefined : outcome.result.structuredContent;
|
|
38
|
+
const text = outcome.result.content.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n");
|
|
39
|
+
if (structured === undefined && outcome.isError) throw new WorkflowError("TOOL_FAILED", `${tool.name}: ${text || "failed"}`);
|
|
40
|
+
let value: unknown;
|
|
41
|
+
try { value = JSON.parse(JSON.stringify(structured ?? text)) as unknown; } catch (error) { throw new WorkflowError("TOOL_FAILED", `${tool.name} returned a result that is not JSON: ${errorText(error)}`); }
|
|
42
|
+
if (!jsonValue(value)) throw new WorkflowError("TOOL_FAILED", `${tool.name} returned a result that is not JSON`);
|
|
43
|
+
if (Buffer.byteLength(JSON.stringify(value)) > RPC_LIMIT_BYTES) throw new WorkflowError("TOOL_FAILED", `${tool.name} returned more than the 10 MB workflow RPC limit`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** Fails a launch whose script names a tool this session cannot call, before any agent runs. */
|
|
47
|
+
export function validateScriptToolReferences(script: string, context: ExtensionToolContext | undefined): void {
|
|
48
|
+
const references = scriptToolReferences(script);
|
|
49
|
+
if (!references?.length) return;
|
|
50
|
+
if (!context) throw new WorkflowError("UNKNOWN_TOOL", `Workflow scripts can call session tools only when launched by the workflow tool; this launch has none for ${references.map((name) => `tools.${name}`).join(", ")}`);
|
|
51
|
+
const identifiers = scriptToolIdentifiers(context);
|
|
52
|
+
const unknown = references.filter((name) => !identifiers.includes(name));
|
|
53
|
+
if (unknown.length) throw new WorkflowError("UNKNOWN_TOOL", `Workflow script calls tools this session cannot call: ${unknown.map((name) => `tools.${name}`).join(", ")}`);
|
|
54
|
+
const ambiguous = references.filter((name) => identifiers.indexOf(name) !== identifiers.lastIndexOf(name));
|
|
55
|
+
if (ambiguous.length) throw new WorkflowError("UNKNOWN_TOOL", `Workflow script calls tools whose names collide after normalization: ${ambiguous.map((name) => `tools.${name}`).join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
function describeScriptOutput(schema: unknown): string | undefined {
|
|
58
|
+
if (!object(schema) || schema.type !== "object" || !object(schema.properties)) return schema === undefined ? undefined : "a JSON value";
|
|
59
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
60
|
+
return `\`{ ${Object.keys(schema.properties).map((name) => required.has(name) ? name : `${name}?`).join(", ")} }\``;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Tells the model what `tools.<id>(args)` resolves to for declared tools with an output schema; the
|
|
64
|
+
* workflow description says that the others resolve to their text. With codemode active its own
|
|
65
|
+
* identical `Codemode:` lines say this, and pi keeps only one description per tool, so this defers.
|
|
66
|
+
*/
|
|
67
|
+
export function prepareScriptToolLoadout(loadout: ToolLoadout): ToolLoadoutChanges | undefined {
|
|
68
|
+
if (loadout.declared.some(({ name }) => name === "codemode")) return undefined;
|
|
69
|
+
const callable = new Set(loadout.callable.map(({ name }) => name));
|
|
70
|
+
const descriptions: Record<string, string> = {};
|
|
71
|
+
for (const tool of loadout.declared) {
|
|
72
|
+
const output = callable.has(tool.name) && !scriptToolExcluded(tool.name) ? describeScriptOutput(tool.outputSchema) : undefined;
|
|
73
|
+
if (output) descriptions[tool.name] = `${tool.description.trim()}\n\nWorkflow scripts: \`tools.${scriptToolIdentifier(tool.name)}(args)\` resolves to ${output}.`;
|
|
74
|
+
}
|
|
75
|
+
return { descriptions };
|
|
76
|
+
}
|
package/src/session-lease.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { link, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -12,12 +13,39 @@ const SESSION_OWNER_FILE = "owner.json";
|
|
|
12
13
|
const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
|
|
13
14
|
const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const START_MARGIN_MS = 60_000;
|
|
17
|
+
let clockTicks: number | undefined;
|
|
18
|
+
|
|
19
|
+
function clockTickRate(): number {
|
|
20
|
+
if (clockTicks !== undefined) return clockTicks;
|
|
21
|
+
try {
|
|
22
|
+
const rate = Number(execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
23
|
+
clockTicks = Number.isInteger(rate) && rate > 0 ? rate : 100;
|
|
24
|
+
} catch { clockTicks = 100; }
|
|
25
|
+
return clockTicks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Process start in epoch ms from /proc/<pid>/stat field 22; undefined when unreadable or unparsable.
|
|
29
|
+
async function linuxProcessStart(pid: number, procRoot: string): Promise<number | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
const text = await readFile(join(procRoot, String(pid), "stat"), "utf8");
|
|
32
|
+
// Field 2 (command name) may contain spaces and parentheses, so count fields after the last ")".
|
|
33
|
+
const close = text.lastIndexOf(")");
|
|
34
|
+
const ticks = close < 0 ? Number.NaN : Number(text.slice(close + 2).split(" ")[19]);
|
|
35
|
+
const btime = /^btime (\d+)$/m.exec(await readFile(join(procRoot, "stat"), "utf8"))?.[1];
|
|
36
|
+
if (btime === undefined || !Number.isInteger(ticks) || ticks < 0) return undefined;
|
|
37
|
+
return Number(btime) * 1000 + (ticks / clockTickRate()) * 1000;
|
|
38
|
+
} catch { return undefined; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The /proc/<pid> ctime is not a start time: procfs rebuilds the inode after cache reclaim.
|
|
42
|
+
// A live owner never starts after its stamp; a reused pid starts later.
|
|
43
|
+
export async function processAlive(pid: number, startedAt?: number, procRoot = "/proc"): Promise<boolean> {
|
|
44
|
+
try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
|
|
45
|
+
if (startedAt === undefined || process.platform !== "linux") return true;
|
|
46
|
+
const start = await linuxProcessStart(pid, procRoot);
|
|
47
|
+
if (start !== undefined) return start <= startedAt + START_MARGIN_MS;
|
|
16
48
|
try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
|
|
17
|
-
if (startedAt !== undefined && process.platform === "linux") {
|
|
18
|
-
try { if ((await stat(`/proc/${String(pid)}`)).ctimeMs > startedAt) return false; }
|
|
19
|
-
catch (error) { if (isNodeError(error, "ENOENT")) return false; }
|
|
20
|
-
}
|
|
21
49
|
return true;
|
|
22
50
|
}
|
|
23
51
|
export async function hasLiveSessionLease(cwd: string, sessionId: string, home = homedir()): Promise<boolean> {
|
|
@@ -73,7 +101,7 @@ export async function acquireSessionLease(cwd: string, sessionId: string, home =
|
|
|
73
101
|
const path = join(directory, SESSION_OWNER_FILE);
|
|
74
102
|
for (;;) {
|
|
75
103
|
const token = randomUUID();
|
|
76
|
-
const owner: SessionOwner = { pid: process.pid, token, startedAt:
|
|
104
|
+
const owner: SessionOwner = { pid: process.pid, token, startedAt: Date.now() };
|
|
77
105
|
try {
|
|
78
106
|
const handle = await open(path, "wx", 0o600);
|
|
79
107
|
try { await handle.writeFile(`${JSON.stringify(owner)}\n`, "utf8"); } finally { await handle.close(); }
|