@llblab/pi-actors 0.41.0 → 0.42.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.
Files changed (150) hide show
  1. package/AGENTS.md +13 -8
  2. package/BACKLOG.md +2 -81
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +29 -82
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +29 -1
  25. package/dist/lib/inspector-overlay.js +537 -90
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +52 -1
  28. package/dist/lib/observability.d.ts +39 -3
  29. package/dist/lib/observability.js +162 -34
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-response.d.ts +1 -1
  86. package/dist/lib/tools-response.js +5 -7
  87. package/dist/lib/tools-spawn.js +4 -1
  88. package/dist/lib/tools.d.ts +1 -0
  89. package/dist/lib/tools.js +1 -0
  90. package/dist/recipes/draft-review.json +24 -0
  91. package/dist/recipes/tool-review.json +24 -0
  92. package/dist/scripts/release-gates.mjs +165 -0
  93. package/dist/skills/actors/SKILL.md +11 -14
  94. package/dist/skills/swarm/SKILL.md +1 -1
  95. package/docs/actor-inspector.md +32 -18
  96. package/docs/async-runs.md +10 -2
  97. package/docs/recipe-library.md +7 -3
  98. package/docs/template-recipes.md +4 -11
  99. package/docs/tool-registry.md +11 -4
  100. package/index.ts +29 -103
  101. package/lib/async-runs.ts +218 -62
  102. package/lib/automatic-review-runtime.ts +135 -0
  103. package/lib/draft-consolidation-transaction.ts +821 -0
  104. package/lib/draft-consolidation.ts +181 -0
  105. package/lib/draft-review.ts +325 -0
  106. package/lib/draft-sleep.ts +576 -0
  107. package/lib/file-state.ts +143 -19
  108. package/lib/inspector-actions.ts +79 -0
  109. package/lib/inspector-command.ts +54 -0
  110. package/lib/inspector-overlay.ts +675 -105
  111. package/lib/inspector.ts +78 -3
  112. package/lib/observability.ts +219 -40
  113. package/lib/paths.ts +43 -0
  114. package/lib/recipes-discovery.ts +34 -26
  115. package/lib/recipes-usage.ts +569 -40
  116. package/lib/review-control.ts +137 -0
  117. package/lib/review-diagnostics.ts +164 -0
  118. package/lib/review-projection.ts +200 -0
  119. package/lib/run-ui-runtime.ts +153 -0
  120. package/lib/runs-artifacts.ts +1 -1
  121. package/lib/runs-control.ts +49 -5
  122. package/lib/runs-identity.ts +1 -1
  123. package/lib/runs-index.ts +57 -21
  124. package/lib/runs-mailbox.ts +1 -1
  125. package/lib/runs-messages.ts +1 -1
  126. package/lib/runs-outbox.ts +1 -1
  127. package/lib/runs-ownership.ts +1 -1
  128. package/lib/runs-parent-teardown.ts +257 -0
  129. package/lib/runs-process.ts +1 -1
  130. package/lib/runs-retention.ts +1 -1
  131. package/lib/runs-start.ts +12 -68
  132. package/lib/runs-status.ts +12 -8
  133. package/lib/runtime.ts +34 -17
  134. package/lib/tool-review-lineage-transaction.ts +881 -0
  135. package/lib/tool-review-lineage.ts +145 -0
  136. package/lib/tool-review-scheduler.ts +635 -0
  137. package/lib/tool-review-transaction.ts +563 -0
  138. package/lib/tool-review.ts +270 -0
  139. package/lib/tools-inspect.ts +33 -3
  140. package/lib/tools-local.ts +8 -2
  141. package/lib/tools-message.ts +45 -30
  142. package/lib/tools-response.ts +5 -6
  143. package/lib/tools-spawn.ts +8 -1
  144. package/lib/tools.ts +5 -0
  145. package/package.json +3 -2
  146. package/recipes/draft-review.json +24 -0
  147. package/recipes/tool-review.json +24 -0
  148. package/scripts/release-gates.mjs +165 -0
  149. package/skills/actors/SKILL.md +11 -14
  150. package/skills/swarm/SKILL.md +1 -1
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Automatic recipe-review diagnostics.
3
+ * Zones: bounded draft/tool cycle evidence, lineage summaries, revision snapshot inventory
4
+ * Owns read-only diagnostic projection; schedulers and transactions own mutation.
5
+ */
6
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
+ import { basename, dirname, join } from "node:path";
8
+ import * as Paths from "./paths.js";
9
+ const MAX_FILE_BYTES = 1024 * 1024;
10
+ const MAX_ENTRIES = 100;
11
+ function readJson(path) {
12
+ if (!path || !existsSync(path) || statSync(path).size > MAX_FILE_BYTES)
13
+ return undefined;
14
+ try {
15
+ const value = JSON.parse(readFileSync(path, "utf8"));
16
+ return value && typeof value === "object" && !Array.isArray(value)
17
+ ? value
18
+ : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ function text(value, limit = 240) {
25
+ if (typeof value !== "string")
26
+ return undefined;
27
+ return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
28
+ }
29
+ function strings(value, limit = 20) {
30
+ return Array.isArray(value)
31
+ ? value.filter((entry) => typeof entry === "string").slice(-limit)
32
+ : [];
33
+ }
34
+ function cycleState(state) {
35
+ if (!state)
36
+ return { phase: "idle" };
37
+ return {
38
+ phase: text(state.phase) ?? "unknown",
39
+ attempts: state.attempts,
40
+ failed_stage: text(state.failedStage),
41
+ last_error: text(state.lastError, 500),
42
+ next_action: text(state.nextAction, 500),
43
+ processing_attempts: state.processingAttempts,
44
+ batch_id: text(state.batchId),
45
+ review_id: text(state.reviewId),
46
+ run_id: text(state.runId),
47
+ sources: Array.isArray(state.sourcePaths) ? state.sourcePaths.length : undefined,
48
+ tools: Array.isArray(state.toolNames) ? state.toolNames.length : undefined,
49
+ updated_at: text(state.updatedAt),
50
+ };
51
+ }
52
+ function draftEvidence(state) {
53
+ const evidence = readJson(text(state?.evidencePath, 4096));
54
+ if (!evidence)
55
+ return undefined;
56
+ const decisions = Array.isArray(evidence.decisions)
57
+ ? evidence.decisions
58
+ : [];
59
+ const garbage = evidence.garbageCollection;
60
+ return {
61
+ actions: Object.fromEntries(["promote", "discard"].map((action) => [
62
+ action,
63
+ decisions.filter((decision) => decision.action === action).length,
64
+ ])),
65
+ garbage_collection: garbage
66
+ ? { reviewed_sources: garbage.reviewedSources, status: garbage.status }
67
+ : undefined,
68
+ reviewed_sources: decisions.length,
69
+ };
70
+ }
71
+ function toolEvidence(state) {
72
+ const approved = readJson(text(state?.approvedPath, 4096));
73
+ if (!approved)
74
+ return undefined;
75
+ const decisions = Array.isArray(approved.decisions)
76
+ ? approved.decisions
77
+ : [];
78
+ return {
79
+ actions: Object.fromEntries(["keep", "evolve", "replace", "demote", "merge", "split"].map((action) => [
80
+ action,
81
+ decisions.filter((decision) => decision.action === action).length,
82
+ ])),
83
+ lineage_phase: readJson(text(state?.lineageJournalPath, 4096))?.phase,
84
+ review_id: text(approved.reviewId),
85
+ sources: Array.isArray(approved.sources) ? approved.sources.length : 0,
86
+ targets: Array.isArray(approved.targets) ? approved.targets.length : 0,
87
+ transaction_phase: readJson(join(dirname(text(state?.approvedPath, 4096)), "journal.json"))?.phase,
88
+ };
89
+ }
90
+ function lineageSummaries(recipeRoot) {
91
+ const root = join(recipeRoot, ".usage", "recipes");
92
+ if (!existsSync(root))
93
+ return [];
94
+ return readdirSync(root, { withFileTypes: true })
95
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
96
+ .map((entry) => join(root, entry.name))
97
+ .sort()
98
+ .slice(0, MAX_ENTRIES)
99
+ .flatMap((path) => {
100
+ const record = readJson(path);
101
+ if (!record)
102
+ return [];
103
+ return [{
104
+ current_path: text(record.current_path),
105
+ demoted_at: text(record.demoted_at),
106
+ demoted_from_revision: record.demoted_from_revision,
107
+ lifetime_calls: record.lifetime_calls,
108
+ name: text(record.lineage_name) ?? basename(path, ".json"),
109
+ review_epochs: strings(record.review_epochs, 5),
110
+ revision: record.revision,
111
+ revision_calls: record.revision_calls,
112
+ rollback_of_revision: record.rollback_of_revision,
113
+ }];
114
+ });
115
+ }
116
+ function snapshotCount(recipeRoot) {
117
+ const root = join(recipeRoot, ".usage", "revisions");
118
+ if (!existsSync(root))
119
+ return 0;
120
+ let count = 0;
121
+ for (const lineage of readdirSync(root, { withFileTypes: true }).slice(0, MAX_ENTRIES)) {
122
+ if (!lineage.isDirectory())
123
+ continue;
124
+ count += readdirSync(join(root, lineage.name), { withFileTypes: true })
125
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
126
+ .length;
127
+ }
128
+ return count;
129
+ }
130
+ export function readAutomaticReviewDiagnostics(options = {}) {
131
+ const recipeRoot = options.recipeRoot ?? Paths.getRecipeRoot();
132
+ const draftState = readJson(options.draftStatePath ?? Paths.getDraftSleepStatePath());
133
+ const toolState = readJson(options.toolStatePath ?? Paths.getToolReviewStatePath());
134
+ const lineages = lineageSummaries(recipeRoot);
135
+ return {
136
+ draft_review: {
137
+ ...cycleState(draftState),
138
+ evidence: draftEvidence(draftState),
139
+ },
140
+ lineage_count: lineages.length,
141
+ lineages,
142
+ revision_snapshots: snapshotCount(recipeRoot),
143
+ tool_review: {
144
+ ...cycleState(toolState),
145
+ evidence: toolEvidence(toolState),
146
+ },
147
+ };
148
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Automatic recipe-review confidentiality projection.
3
+ * Owns: value-free model inputs derived from trusted immutable draft/tool captures.
4
+ */
5
+ import type { DraftReviewInput, DraftReviewResult } from "./draft-review.ts";
6
+ import type { ToolReviewInput, ToolReviewResult } from "./tool-review.ts";
7
+ export declare const draftReviewIdentity: (index: number) => string;
8
+ export declare const toolReviewIdentity: (index: number) => string;
9
+ export declare function draftReviewRevision(input: DraftReviewInput, index: number): string;
10
+ export declare function toolReviewRevision(input: ToolReviewInput, index: number): string;
11
+ export declare function projectDraftReviewInput(input: DraftReviewInput): Record<string, unknown>;
12
+ export declare function restoreDraftReviewResult(input: DraftReviewInput, result: DraftReviewResult): DraftReviewResult;
13
+ export declare function projectToolReviewInput(input: ToolReviewInput): Record<string, unknown>;
14
+ export declare function restoreToolReviewResult(input: ToolReviewInput, result: ToolReviewResult): ToolReviewResult;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Automatic recipe-review confidentiality projection.
3
+ * Owns: value-free model inputs derived from trusted immutable draft/tool captures.
4
+ */
5
+ function occurrenceId(prefix, index) {
6
+ return `${prefix}_${String(index + 1).padStart(2, "0")}`;
7
+ }
8
+ export const draftReviewIdentity = (index) => occurrenceId("draft", index);
9
+ export const toolReviewIdentity = (index) => occurrenceId("tool", index);
10
+ function contentGroups(hashes) {
11
+ const groups = new Map();
12
+ return hashes.map((hash) => {
13
+ let group = groups.get(hash);
14
+ if (!group) {
15
+ group = occurrenceId("content_group", groups.size);
16
+ groups.set(hash, group);
17
+ }
18
+ return group;
19
+ });
20
+ }
21
+ function draftContentGroups(input) {
22
+ return contentGroups([
23
+ ...input.activeTools.map((tool) => tool.sha256),
24
+ ...input.drafts.map((draft) => draft.sha256),
25
+ ]);
26
+ }
27
+ export function draftReviewRevision(input, index) {
28
+ return draftContentGroups(input)[input.activeTools.length + index];
29
+ }
30
+ export function toolReviewRevision(input, index) {
31
+ return contentGroups(input.tools.map((tool) => tool.sha256))[index];
32
+ }
33
+ function countArray(value) {
34
+ return Array.isArray(value) ? value.length : 0;
35
+ }
36
+ function countRecord(value) {
37
+ return value && typeof value === "object" && !Array.isArray(value)
38
+ ? Object.keys(value).length
39
+ : 0;
40
+ }
41
+ function templateShape(value) {
42
+ if (typeof value === "string")
43
+ return { kind: "command", nodes: 1 };
44
+ if (Array.isArray(value)) {
45
+ return {
46
+ branches: value.length,
47
+ kind: "sequence",
48
+ nodes: 1 + value.reduce((total, child) => total + Number(templateShape(child).nodes ?? 0), 0),
49
+ };
50
+ }
51
+ if (!value || typeof value !== "object")
52
+ return { kind: "invalid", nodes: 0 };
53
+ const record = value;
54
+ const children = Array.isArray(record.template)
55
+ ? record.template
56
+ : record.template === undefined
57
+ ? []
58
+ : [record.template];
59
+ return {
60
+ branches: children.length,
61
+ conditional: record.when !== undefined,
62
+ kind: record.parallel === true ? "parallel" : "object",
63
+ repeat: record.repeat !== undefined,
64
+ retry: record.retry !== undefined,
65
+ nodes: 1 + children.reduce((total, child) => total + Number(templateShape(child).nodes ?? 0), 0),
66
+ };
67
+ }
68
+ function contractShape(recipe) {
69
+ if (!recipe)
70
+ return undefined;
71
+ const mailbox = recipe.mailbox && typeof recipe.mailbox === "object" && !Array.isArray(recipe.mailbox)
72
+ ? recipe.mailbox
73
+ : {};
74
+ return {
75
+ argument_count: countArray(recipe.args),
76
+ artifact_count: countRecord(recipe.artifacts),
77
+ async: recipe.async === true,
78
+ default_count: countRecord(recipe.defaults),
79
+ import_count: countRecord(recipe.imports),
80
+ mailbox_accepts_count: countArray(mailbox.accepts),
81
+ mailbox_emits_count: countArray(mailbox.emits),
82
+ template_shape: templateShape(recipe.template),
83
+ };
84
+ }
85
+ function usageShape(usage) {
86
+ const numeric = (key) => {
87
+ const value = usage?.[key];
88
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
89
+ ? value
90
+ : 0;
91
+ };
92
+ return {
93
+ lifetime_calls: numeric("lifetime_calls") || numeric("calls"),
94
+ revision: numeric("revision"),
95
+ revision_calls: numeric("revision_calls"),
96
+ };
97
+ }
98
+ export function projectDraftReviewInput(input) {
99
+ return {
100
+ activeTools: input.activeTools.map((_tool, index) => ({
101
+ name: occurrenceId("active_tool", index),
102
+ sha256: draftContentGroups(input)[index],
103
+ })),
104
+ batchId: input.batchId,
105
+ createdAt: input.createdAt,
106
+ drafts: input.drafts.map((draft, index) => ({
107
+ contract: contractShape(draft.recipe),
108
+ draft: draftReviewIdentity(index),
109
+ riskLabels: [...draft.riskLabels],
110
+ sha256: draftReviewRevision(input, index),
111
+ usage: usageShape(draft.usage),
112
+ valid: draft.valid,
113
+ })),
114
+ };
115
+ }
116
+ export function restoreDraftReviewResult(input, result) {
117
+ const drafts = new Map(input.drafts.map((draft, index) => [draftReviewIdentity(index), {
118
+ draft,
119
+ revision: draftReviewRevision(input, index),
120
+ }]));
121
+ return {
122
+ ...result,
123
+ decisions: result.decisions.map((decision) => {
124
+ const captured = drafts.get(decision.draft);
125
+ return {
126
+ ...decision,
127
+ draft: captured?.draft.path ?? `unknown:${decision.draft}`,
128
+ sha256: captured && decision.sha256 === captured.revision
129
+ ? captured.draft.sha256
130
+ : `invalid:${decision.sha256}`,
131
+ };
132
+ }),
133
+ };
134
+ }
135
+ export function projectToolReviewInput(input) {
136
+ return {
137
+ createdAt: input.createdAt,
138
+ reviewId: input.reviewId,
139
+ tools: input.tools.map((tool, index) => ({
140
+ contract: contractShape(tool.recipe),
141
+ name: toolReviewIdentity(index),
142
+ riskLabels: [...tool.riskLabels],
143
+ sha256: toolReviewRevision(input, index),
144
+ usage: usageShape(tool.usage),
145
+ valid: tool.valid,
146
+ })),
147
+ };
148
+ }
149
+ export function restoreToolReviewResult(input, result) {
150
+ const tools = new Map(input.tools.map((tool, index) => [toolReviewIdentity(index), {
151
+ revision: toolReviewRevision(input, index),
152
+ tool,
153
+ }]));
154
+ return {
155
+ ...result,
156
+ decisions: result.decisions.map((decision) => {
157
+ const captured = tools.get(decision.source);
158
+ return {
159
+ ...decision,
160
+ sha256: captured && decision.sha256 === captured.revision
161
+ ? captured.tool.sha256
162
+ : `invalid:${decision.sha256}`,
163
+ source: captured?.tool.name ?? `unknown:${decision.source}`,
164
+ ...(decision.target
165
+ ? { target: tools.get(decision.target)?.tool.name ?? decision.target }
166
+ : {}),
167
+ };
168
+ }),
169
+ };
170
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ambient run observability runtime.
3
+ * Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
4
+ * Owns event-driven run UI coordination without owning actor execution semantics.
5
+ */
6
+ import * as Pi from "./pi.ts";
7
+ export interface RunUiRuntime {
8
+ close(): void;
9
+ shutdown(eventReason: string, ctx: Pi.ExtensionContext): void;
10
+ start(ctx: Pi.ExtensionContext): void;
11
+ }
12
+ export interface RunUiRuntimeDeps {
13
+ getActiveContext(): Pi.ExtensionContext | undefined;
14
+ getRunOwnerId(ctx: Pi.ExtensionContext): string;
15
+ onRunEvent(): void;
16
+ pi: Pi.ExtensionAPI;
17
+ }
18
+ export declare function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Ambient run observability runtime.
3
+ * Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
4
+ * Owns event-driven run UI coordination without owning actor execution semantics.
5
+ */
6
+ import * as AsyncRuns from "./async-runs.js";
7
+ import * as Observability from "./observability.js";
8
+ import * as Paths from "./paths.js";
9
+ import * as Pi from "./pi.js";
10
+ export function createRunUiRuntime(deps) {
11
+ let animationInterval;
12
+ let notifyTimeout;
13
+ let lastWatcherDiagnosticId = 0;
14
+ const observation = Observability.createRunUiObservationState();
15
+ const retirementAttempts = new Set();
16
+ const terminalNotificationsInFlight = new Set();
17
+ const retireCandidateRuns = (ctx, summary) => {
18
+ void Observability.executeRunRetirements(summary, {
19
+ attempted: retirementAttempts,
20
+ cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
21
+ notify: (message, level) => ctx.ui.notify(message, level),
22
+ sendStop: (candidate) => AsyncRuns.sendRunMessage(candidate.stateDir, "stop"),
23
+ });
24
+ };
25
+ const update = (ctx, notify = false, terminalOnly = false) => {
26
+ const ownerId = deps.getRunOwnerId(ctx);
27
+ const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
28
+ ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
29
+ if (!notify)
30
+ return;
31
+ const sink = Pi.createNotificationSink(deps.pi, ctx);
32
+ retireCandidateRuns(ctx, snapshot.summary);
33
+ Observability.deliverRunTransitionNotifications(snapshot.transitions, sink, terminalNotificationsInFlight);
34
+ Observability.pruneRunUiObservationState(observation, snapshot);
35
+ if (!terminalOnly) {
36
+ Observability.deliverRunOutboxNotifications(snapshot.outboxEvents, sink);
37
+ }
38
+ };
39
+ const reportDiagnostics = (ctx) => {
40
+ for (const diagnostic of watcher.getDiagnostics()) {
41
+ if (diagnostic.id <= lastWatcherDiagnosticId)
42
+ continue;
43
+ lastWatcherDiagnosticId = diagnostic.id;
44
+ ctx.ui.notify(diagnostic.message, diagnostic.code === "rearmed" ? "info" : "warning");
45
+ }
46
+ };
47
+ const scheduleUpdate = () => {
48
+ if (notifyTimeout)
49
+ clearTimeout(notifyTimeout);
50
+ notifyTimeout = setTimeout(() => {
51
+ const ctx = deps.getActiveContext();
52
+ if (!ctx)
53
+ return;
54
+ watcher.refresh();
55
+ update(ctx, true);
56
+ deps.onRunEvent();
57
+ reportDiagnostics(ctx);
58
+ }, 50);
59
+ notifyTimeout.unref?.();
60
+ };
61
+ const watcher = Observability.createRunStateWatcher({
62
+ stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
63
+ onChange: scheduleUpdate,
64
+ });
65
+ const reconciliation = Observability.createRunTerminalReconciliationLoop({
66
+ onError: (error) => {
67
+ const ctx = deps.getActiveContext();
68
+ if (!ctx)
69
+ return;
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
72
+ },
73
+ reconcile: () => {
74
+ const ctx = deps.getActiveContext();
75
+ if (!ctx)
76
+ return;
77
+ Observability.reconcileRunTerminalNotifications({
78
+ inFlight: terminalNotificationsInFlight,
79
+ ownerId: deps.getRunOwnerId(ctx),
80
+ sink: Pi.createNotificationSink(deps.pi, ctx),
81
+ state: observation,
82
+ });
83
+ reportDiagnostics(ctx);
84
+ },
85
+ refreshWatcher: () => watcher.refresh(),
86
+ });
87
+ const close = () => {
88
+ watcher.close();
89
+ reconciliation.close();
90
+ if (notifyTimeout)
91
+ clearTimeout(notifyTimeout);
92
+ notifyTimeout = undefined;
93
+ if (animationInterval)
94
+ clearInterval(animationInterval);
95
+ animationInterval = undefined;
96
+ };
97
+ return {
98
+ close,
99
+ shutdown(eventReason, ctx) {
100
+ close();
101
+ const teardown = AsyncRuns.teardownRunsOwnedByParent(deps.getRunOwnerId(ctx), Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, { trigger: `session_shutdown:${eventReason}` });
102
+ if (teardown.failed === 0)
103
+ return;
104
+ try {
105
+ ctx.ui.notify(`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`, "warning");
106
+ }
107
+ catch {
108
+ /* stale shutdown context */
109
+ }
110
+ },
111
+ start(ctx) {
112
+ close();
113
+ update(ctx, true, true);
114
+ watcher.refresh();
115
+ reconciliation.start();
116
+ animationInterval = setInterval(() => {
117
+ if (deps.getActiveContext() === ctx)
118
+ update(ctx);
119
+ }, 1000);
120
+ animationInterval.unref?.();
121
+ },
122
+ };
123
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Async run artifact declarations and manifest resolution.
3
- * Owns artifact path template expansion and filesystem-backed artifact metadata.
3
+ * Owns: artifact path template expansion and filesystem-backed artifact metadata.
4
4
  */
5
5
  export type RunArtifactDeclaration = string | {
6
6
  path: string;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Async run artifact declarations and manifest resolution.
3
- * Owns artifact path template expansion and filesystem-backed artifact metadata.
3
+ * Owns: artifact path template expansion and filesystem-backed artifact metadata.
4
4
  */
5
5
  import { createHash } from "node:crypto";
6
6
  import { readFileSync } from "node:fs";
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * Async run process control primitives.
3
- * Owns platform signal planning, owned-process signalling, and terminal control markers.
3
+ * Owns: platform signal planning, owned-process signalling, and terminal control markers.
4
4
  */
5
+ import { type RunProcessIdentity, type RunProcessIdentityResult } from "./runs-process.ts";
5
6
  export interface RunProcessSignalPlan {
6
7
  args?: string[];
7
8
  command?: string;
8
9
  signalTarget: "processGroup" | "process" | "processTree";
9
10
  }
10
11
  export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Signals, runtimePlatform?: NodeJS.Platform): RunProcessSignalPlan;
11
- export declare function signalOwnedRunProcess(pid: number, signal: NodeJS.Signals): RunProcessSignalPlan;
12
+ export interface RunProcessSignalDeps {
13
+ killProcess?: typeof process.kill;
14
+ runtimePlatform?: NodeJS.Platform;
15
+ verifyIdentity?: (pid: number, expected: RunProcessIdentity, runtimePlatform: NodeJS.Platform) => RunProcessIdentityResult;
16
+ }
17
+ export declare function signalOwnedRunProcess(pid: number, signal: NodeJS.Signals, expectedIdentity?: RunProcessIdentity, deps?: RunProcessSignalDeps): RunProcessSignalPlan;
12
18
  export declare function markTerminalHandled(stateDir: string, details: Record<string, unknown>): void;
13
19
  export declare function buildTerminalProgress(existing: Record<string, unknown> | undefined, phase: "cancelled" | "killed"): Record<string, unknown>;
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Async run process control primitives.
3
- * Owns platform signal planning, owned-process signalling, and terminal control markers.
3
+ * Owns: platform signal planning, owned-process signalling, and terminal control markers.
4
4
  */
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { join } from "node:path";
7
7
  import { writeJsonAtomic } from "./file-state.js";
8
+ import { verifyRunProcessIdentity, } from "./runs-process.js";
8
9
  export function getRunProcessSignalPlan(pid, signal, runtimePlatform = process.platform) {
9
10
  if (runtimePlatform === "win32") {
10
11
  return {
@@ -20,8 +21,15 @@ export function getRunProcessSignalPlan(pid, signal, runtimePlatform = process.p
20
21
  }
21
22
  return { signalTarget: "processGroup" };
22
23
  }
23
- export function signalOwnedRunProcess(pid, signal) {
24
- const plan = getRunProcessSignalPlan(pid, signal);
24
+ export function signalOwnedRunProcess(pid, signal, expectedIdentity, deps = {}) {
25
+ const runtimePlatform = deps.runtimePlatform ?? process.platform;
26
+ if (expectedIdentity) {
27
+ const proof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(pid, expectedIdentity, runtimePlatform);
28
+ if (!proof.valid) {
29
+ throw new Error(`Run process identity changed before signaling: ${proof.status.replaceAll("_", " ")}`);
30
+ }
31
+ }
32
+ const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
25
33
  if (plan.command && plan.args) {
26
34
  const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
27
35
  if (result.status !== 0) {
@@ -31,12 +39,21 @@ export function signalOwnedRunProcess(pid, signal) {
31
39
  }
32
40
  return plan;
33
41
  }
42
+ const killProcess = deps.killProcess ?? process.kill.bind(process);
34
43
  try {
35
- process.kill(-pid, signal);
44
+ killProcess(-pid, signal);
36
45
  return { signalTarget: "processGroup" };
37
46
  }
38
- catch {
39
- process.kill(pid, signal);
47
+ catch (error) {
48
+ if (error.code !== "ESRCH")
49
+ throw error;
50
+ if (expectedIdentity) {
51
+ const fallbackProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(pid, expectedIdentity, runtimePlatform);
52
+ if (!fallbackProof.valid) {
53
+ throw new Error(`Run process identity changed before pid fallback: ${fallbackProof.status.replaceAll("_", " ")}`);
54
+ }
55
+ }
56
+ killProcess(pid, signal);
40
57
  return { signalTarget: "process" };
41
58
  }
42
59
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Async run identity helpers.
3
- * Owns run id normalization shared by run lifecycle and retention paths.
3
+ * Owns: run id normalization shared by run lifecycle and retention paths.
4
4
  */
5
5
  export declare function safeRunId(value: string | undefined): string;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Async run identity helpers.
3
- * Owns run id normalization shared by run lifecycle and retention paths.
3
+ * Owns: run id normalization shared by run lifecycle and retention paths.
4
4
  */
5
5
  export function safeRunId(value) {
6
6
  const run = (value || `run-${Date.now()}`).trim();
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Async run state index.
3
- * Owns recursive run-state discovery, index rebuild/read, and status filtering.
3
+ * Owns: recursive run-state discovery, index rebuild/read, and status filtering.
4
4
  */
5
5
  export interface RunStateIndexEntry {
6
6
  ownerId?: string;
@@ -12,7 +12,16 @@ export interface RunStateIndexEntry {
12
12
  updated_at?: string;
13
13
  }
14
14
  export type RunStatusReader = (runOrDir: string) => Record<string, unknown>;
15
- export declare function listRunStateDirs(stateRoot: string, depth?: number, seen?: Set<string>): string[];
15
+ export interface RunStateDiscoveryIssue {
16
+ path: string;
17
+ reason: "depth_truncated" | "unreadable";
18
+ }
19
+ export interface RunStateDiscoveryResult {
20
+ issues: RunStateDiscoveryIssue[];
21
+ stateDirs: string[];
22
+ }
23
+ export declare function discoverRunStateDirs(stateRoot: string, maxDepth?: number): RunStateDiscoveryResult;
24
+ export declare function listRunStateDirs(stateRoot: string): string[];
16
25
  export declare function rebuildRunStateIndex(stateRoot: string, getRunStatus: RunStatusReader): RunStateIndexEntry[];
17
26
  export declare function readRunStateIndex(stateRoot: string, readJson: (path: string) => Record<string, unknown> | undefined): RunStateIndexEntry[] | undefined;
18
27
  export declare function listRuns(stateRoot: string, getRunStatus: RunStatusReader, readJson: (path: string) => Record<string, unknown> | undefined, statusFilter?: string): Array<Record<string, unknown>>;