@llblab/pi-actors 0.43.0 → 0.44.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 (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
@@ -3,12 +3,16 @@
3
3
  * Zones: file persistence, atomic writes, runtime state support
4
4
  * Owns generic durable JSON file writes shared by registry config and async run state.
5
5
  */
6
+ import { spawnSync } from "node:child_process";
6
7
  import { createHash, randomUUID } from "node:crypto";
7
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
8
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
9
  import { tmpdir } from "node:os";
9
10
  import { basename, dirname, join, parse, resolve } from "node:path";
10
- const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
11
+ const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 30000 : 15000;
11
12
  const FILE_MUTATION_LOCK_STALE_MS = 30000;
13
+ const FILE_MUTATION_LOCK_RECLAIM_POLL_MS = 100;
14
+ const FILE_MUTATION_LOCK_REMOVAL_GRACE_MS = 250;
15
+ const FILE_MUTATION_LOCK_MAX_WAIT_MS = 50;
12
16
  const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
13
17
  function canonicalMutationPath(path) {
14
18
  const absolute = resolve(path);
@@ -42,6 +46,20 @@ export function mutationLockPath(path) {
42
46
  .digest("hex");
43
47
  return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
44
48
  }
49
+ function isZombieProcess(pid) {
50
+ if (process.platform === "linux")
51
+ try {
52
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
53
+ return stat.slice(stat.lastIndexOf(")") + 2).startsWith("Z ");
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ if (process.platform !== "darwin")
59
+ return false;
60
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "stat="], { encoding: "utf8" });
61
+ return result.status === 0 && result.stdout.trim().startsWith("Z");
62
+ }
45
63
  function lockOwnerStatus(lockPath) {
46
64
  try {
47
65
  const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
@@ -50,6 +68,9 @@ function lockOwnerStatus(lockPath) {
50
68
  return "unknown";
51
69
  try {
52
70
  process.kill(pid, 0);
71
+ if (Date.now() - statSync(lockPath).mtimeMs > FILE_MUTATION_LOCK_REMOVAL_GRACE_MS &&
72
+ isZombieProcess(pid))
73
+ return "dead";
53
74
  return "alive";
54
75
  }
55
76
  catch (error) {
@@ -71,17 +92,59 @@ function readLockToken(lockPath) {
71
92
  return undefined;
72
93
  }
73
94
  }
95
+ function prepareLockBoundary(lockPath, token, onBeforePublish) {
96
+ const pendingPath = `${lockPath}.${process.pid}.${token}.pending`;
97
+ try {
98
+ mkdirSync(pendingPath);
99
+ writeFileSync(join(pendingPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
100
+ onBeforePublish?.();
101
+ return pendingPath;
102
+ }
103
+ catch (error) {
104
+ try {
105
+ rmSync(pendingPath, { recursive: true, force: true });
106
+ }
107
+ catch { }
108
+ throw error;
109
+ }
110
+ }
111
+ function removeLockBoundary(lockPath, token) {
112
+ if (readLockToken(lockPath) !== token)
113
+ return false;
114
+ if (process.platform === "win32")
115
+ try {
116
+ rmSync(lockPath, { recursive: true, force: true, maxRetries: 20, retryDelay: 5 });
117
+ return true;
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ const removingPath = `${lockPath}.${process.pid}.${randomUUID()}.removing`;
123
+ try {
124
+ renameSync(lockPath, removingPath);
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ try {
130
+ rmSync(removingPath, { recursive: true, force: true });
131
+ }
132
+ catch { }
133
+ return true;
134
+ }
74
135
  function tryReclaimRemovalBoundary(reclaimPath) {
75
136
  if (!existsSync(reclaimPath))
76
137
  return;
77
138
  try {
78
- const inspectedToken = readLockToken(reclaimPath);
79
139
  const age = Date.now() - statSync(reclaimPath).mtimeMs;
140
+ if (age <= FILE_MUTATION_LOCK_REMOVAL_GRACE_MS)
141
+ return;
142
+ const inspectedToken = readLockToken(reclaimPath);
80
143
  const ownerStatus = lockOwnerStatus(reclaimPath);
81
144
  if ((ownerStatus === "dead" ||
82
145
  (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
83
146
  readLockToken(reclaimPath) === inspectedToken) {
84
- rmSync(reclaimPath, { recursive: true, force: true });
147
+ removeLockBoundary(reclaimPath, inspectedToken);
85
148
  }
86
149
  }
87
150
  catch {
@@ -91,28 +154,28 @@ function tryReclaimRemovalBoundary(reclaimPath) {
91
154
  function withRemovalBoundary(lockPath, action) {
92
155
  const reclaimPath = `${lockPath}.reclaim`;
93
156
  const token = randomUUID();
157
+ const pendingPath = prepareLockBoundary(reclaimPath, token);
94
158
  try {
95
- mkdirSync(reclaimPath);
96
159
  try {
97
- writeFileSync(join(reclaimPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
160
+ renameSync(pendingPath, reclaimPath);
98
161
  }
99
- catch (error) {
100
- rmSync(reclaimPath, { recursive: true, force: true });
101
- throw error;
162
+ catch {
163
+ tryReclaimRemovalBoundary(reclaimPath);
164
+ return false;
165
+ }
166
+ try {
167
+ action();
168
+ return true;
169
+ }
170
+ finally {
171
+ removeLockBoundary(reclaimPath, token);
102
172
  }
103
- }
104
- catch {
105
- tryReclaimRemovalBoundary(reclaimPath);
106
- return false;
107
- }
108
- try {
109
- action();
110
- return true;
111
173
  }
112
174
  finally {
113
- if (readLockToken(reclaimPath) === token) {
114
- rmSync(reclaimPath, { recursive: true, force: true });
175
+ try {
176
+ rmSync(pendingPath, { recursive: true, force: true });
115
177
  }
178
+ catch { }
116
179
  }
117
180
  }
118
181
  function tryReclaimMutationLock(lockPath, options) {
@@ -129,8 +192,7 @@ function tryReclaimMutationLock(lockPath, options) {
129
192
  (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
130
193
  readLockToken(lockPath) === inspectedToken) {
131
194
  options.onBeforeReclaimRemove?.();
132
- rmSync(lockPath, { recursive: true, force: true });
133
- reclaimed = true;
195
+ reclaimed = removeLockBoundary(lockPath, inspectedToken);
134
196
  }
135
197
  })) {
136
198
  return false;
@@ -142,32 +204,45 @@ export function acquireFileMutationLock(path, options = {}) {
142
204
  const lockPath = mutationLockPath(path);
143
205
  const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
144
206
  const token = randomUUID();
207
+ const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
145
208
  let contentionReported = false;
146
- for (;;) {
147
- try {
148
- mkdirSync(lockPath);
209
+ let nextReclaimAt = 0;
210
+ let waitMs = 10;
211
+ try {
212
+ for (;;) {
149
213
  try {
150
- writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
214
+ renameSync(pendingPath, lockPath);
215
+ break;
151
216
  }
152
217
  catch (error) {
153
- rmSync(lockPath, { recursive: true, force: true });
154
- throw error;
218
+ if (!contentionReported) {
219
+ contentionReported = true;
220
+ options.onContention?.();
221
+ }
222
+ const now = Date.now();
223
+ if (now >= nextReclaimAt) {
224
+ nextReclaimAt = now + FILE_MUTATION_LOCK_RECLAIM_POLL_MS;
225
+ if (tryReclaimMutationLock(lockPath, options)) {
226
+ waitMs = 10;
227
+ continue;
228
+ }
229
+ }
230
+ if (now >= deadline) {
231
+ throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
232
+ cause: error,
233
+ });
234
+ }
235
+ const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(waitMs / 2)));
236
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs + jitter);
237
+ waitMs = Math.min(FILE_MUTATION_LOCK_MAX_WAIT_MS, waitMs + 5);
155
238
  }
156
- break;
157
239
  }
158
- catch (error) {
159
- if (!contentionReported) {
160
- contentionReported = true;
161
- options.onContention?.();
162
- }
163
- tryReclaimMutationLock(lockPath, options);
164
- if (Date.now() >= deadline) {
165
- throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
166
- cause: error,
167
- });
168
- }
169
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
240
+ }
241
+ finally {
242
+ try {
243
+ rmSync(pendingPath, { recursive: true, force: true });
170
244
  }
245
+ catch { }
171
246
  }
172
247
  let released = false;
173
248
  return () => {
@@ -177,9 +252,7 @@ export function acquireFileMutationLock(path, options = {}) {
177
252
  const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
178
253
  let removalContentionReported = false;
179
254
  while (!withRemovalBoundary(lockPath, () => {
180
- if (readLockToken(lockPath) === token) {
181
- rmSync(lockPath, { recursive: true, force: true });
182
- }
255
+ removeLockBoundary(lockPath, token);
183
256
  })) {
184
257
  if (!removalContentionReported) {
185
258
  removalContentionReported = true;
@@ -200,11 +273,12 @@ export function withFileMutationLock(path, mutate, options = {}) {
200
273
  release();
201
274
  }
202
275
  }
203
- export function writeTextAtomic(path, content) {
276
+ export function writeTextAtomic(path, content, options = {}) {
204
277
  mkdirSync(dirname(path), { recursive: true });
205
278
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
206
279
  try {
207
280
  writeFileSync(tempPath, content, "utf8");
281
+ options.onBeforeReplace?.();
208
282
  renameSync(tempPath, path);
209
283
  }
210
284
  catch (error) {
@@ -41,6 +41,7 @@ export declare class ActorInspectorOverlay {
41
41
  private killDialogChoice;
42
42
  private rowIndex;
43
43
  private runCache?;
44
+ private selectedTraceId?;
44
45
  private runIndex;
45
46
  private selectorIndex;
46
47
  private selectorMode?;
@@ -81,6 +82,7 @@ export declare class ActorInspectorOverlay {
81
82
  private readRunMeta;
82
83
  private documentProjection;
83
84
  private inlineDocumentValue;
85
+ private labeledDocumentLines;
84
86
  private labeledScalarLines;
85
87
  private document;
86
88
  private wrapDocument;
@@ -3,12 +3,17 @@
3
3
  * Zones: owned actor selection, Recipe/Trace/Control tabs, safe Run Kill confirmation
4
4
  * Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
5
5
  */
6
- import { readFileSync } from "node:fs";
6
+ import { readFileSync, statSync } from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import * as ActorInspector from "./inspector.js";
10
+ import * as ControlProjection from "./control-projection.js";
10
11
  import * as RunsControls from "./runs-controls.js";
11
12
  import * as RunControlDelivery from "./runs-control-delivery.js";
13
+ import * as RunsTrace from "./runs-trace.js";
14
+ import * as Limits from "./limits.js";
15
+ import { computeRunControlCapacity } from "./run-evidence-policy.js";
16
+ import * as RuntimeTriage from "./runtime-triage.js";
12
17
  import * as TraceProjection from "./trace-projection.js";
13
18
  const TABS = ["recipe", "trace", "control"];
14
19
  const TRACE_SOURCES = [
@@ -39,6 +44,7 @@ export class ActorInspectorOverlay {
39
44
  killDialogChoice = "cancel";
40
45
  rowIndex = 0;
41
46
  runCache;
47
+ selectedTraceId;
42
48
  runIndex = 0;
43
49
  selectorIndex = 0;
44
50
  selectorMode;
@@ -56,9 +62,7 @@ export class ActorInspectorOverlay {
56
62
  this.theme = options.theme;
57
63
  this.tui = options.tui;
58
64
  this.refreshTimer = setInterval(() => {
59
- this.runCache = undefined;
60
- if (this.focus !== "list" && this.focus !== "detail")
61
- this.traceCache = undefined;
65
+ this.invalidate();
62
66
  this.tui.requestRender();
63
67
  }, 1_000);
64
68
  this.refreshTimer.unref?.();
@@ -66,7 +70,10 @@ export class ActorInspectorOverlay {
66
70
  dispose() {
67
71
  clearInterval(this.refreshTimer);
68
72
  }
69
- invalidate() { }
73
+ invalidate() {
74
+ this.runCache = undefined;
75
+ this.traceCache = undefined;
76
+ }
70
77
  handleInput(data) {
71
78
  const runs = this.runs();
72
79
  this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
@@ -90,11 +97,14 @@ export class ActorInspectorOverlay {
90
97
  this.focus = this.tab === "trace" ? "tabs" : "runs";
91
98
  }
92
99
  else if (this.focus === "detail") {
100
+ this.selectedTraceId = this.traceDetail?.id;
93
101
  this.traceDetail = undefined;
94
102
  this.detailScroll = 0;
95
103
  this.focus = "list";
96
104
  }
97
105
  else if (this.focus === "document" || this.focus === "list") {
106
+ if (this.focus === "list")
107
+ this.selectedTraceId = undefined;
98
108
  this.focus = "tabs";
99
109
  }
100
110
  else {
@@ -146,9 +156,12 @@ export class ActorInspectorOverlay {
146
156
  this.focus = "runs";
147
157
  }
148
158
  else if (matchesKey(data, "down")) {
149
- this.focus = this.tab === "trace" && this.traceItems(runs[this.runIndex]).length > 0
150
- ? "list"
151
- : "document";
159
+ const items = this.tab === "trace" ? this.traceItems(runs[this.runIndex]) : [];
160
+ this.focus = items.length > 0 ? "list" : "document";
161
+ if (this.focus === "list") {
162
+ this.rowIndex = 0;
163
+ this.selectedTraceId = items[0]?.id;
164
+ }
152
165
  }
153
166
  else if (matchesKey(data, "return")) {
154
167
  if (this.tab === "trace")
@@ -158,12 +171,17 @@ export class ActorInspectorOverlay {
158
171
  }
159
172
  }
160
173
  else if (this.focus === "list") {
161
- const count = this.traceItems(runs[this.runIndex]).length;
162
- if (matchesKey(data, "left"))
174
+ const items = this.traceItems(runs[this.runIndex]);
175
+ const count = items.length;
176
+ if (matchesKey(data, "left")) {
177
+ this.selectedTraceId = undefined;
163
178
  this.focus = "tabs";
179
+ }
164
180
  else if (matchesKey(data, "up")) {
165
- if (this.rowIndex === 0)
181
+ if (this.rowIndex === 0) {
182
+ this.selectedTraceId = undefined;
166
183
  this.focus = "tabs";
184
+ }
167
185
  else
168
186
  this.rowIndex -= 1;
169
187
  }
@@ -177,13 +195,15 @@ export class ActorInspectorOverlay {
177
195
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
178
196
  }
179
197
  else if (matchesKey(data, "return") || matchesKey(data, "right")) {
180
- const item = this.traceItems(runs[this.runIndex])[this.rowIndex];
198
+ const item = items[this.rowIndex];
181
199
  if (item) {
182
200
  this.traceDetail = item;
183
201
  this.detailScroll = 0;
184
202
  this.focus = "detail";
185
203
  }
186
204
  }
205
+ if (this.focus === "list")
206
+ this.selectedTraceId = items[this.rowIndex]?.id;
187
207
  }
188
208
  if (data.toLowerCase() === "f" && this.tab === "trace") {
189
209
  this.cycleTraceSource(runs[this.runIndex]);
@@ -239,6 +259,7 @@ export class ActorInspectorOverlay {
239
259
  this.documentScroll = 0;
240
260
  this.detailScroll = 0;
241
261
  this.rowIndex = 0;
262
+ this.selectedTraceId = undefined;
242
263
  this.traceDetail = undefined;
243
264
  }
244
265
  cycleRun(runs, direction) {
@@ -260,6 +281,7 @@ export class ActorInspectorOverlay {
260
281
  this[key] += this.contentViewportRows();
261
282
  }
262
283
  else if (matchesKey(data, "left") && target === "detail") {
284
+ this.selectedTraceId = this.traceDetail?.id;
263
285
  this.traceDetail = undefined;
264
286
  this.detailScroll = 0;
265
287
  this.focus = "list";
@@ -361,7 +383,10 @@ export class ActorInspectorOverlay {
361
383
  if (!confirmation || !this.killRun)
362
384
  return;
363
385
  try {
364
- this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
386
+ const result = this.killRun(confirmation.run, confirmation.runInstanceId);
387
+ this.feedback = result.ok ? undefined : result;
388
+ if (result.ok)
389
+ this.invalidate();
365
390
  }
366
391
  catch (error) {
367
392
  const message = error instanceof Error ? error.message : String(error);
@@ -386,8 +411,7 @@ export class ActorInspectorOverlay {
386
411
  `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
387
412
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
388
413
  "",
389
- this.theme.fg("text", "This sends canonical kill Control."),
390
- this.theme.fg("error", "The action is destructive and cannot be undone."),
414
+ this.theme.fg("text", "This invokes generation-fenced runtime kill."),
391
415
  "",
392
416
  `${cancel} ${kill}`,
393
417
  "",
@@ -478,33 +502,45 @@ export class ActorInspectorOverlay {
478
502
  }
479
503
  renderTraceList(run) {
480
504
  const items = this.traceItems(run);
505
+ const summary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(path.join(this.stateRoot, run.run)));
506
+ const banner = summary.history_complete ? [] : [this.theme.fg("warning", ` ! Trace history incomplete: ${summary.compacted ? `${summary.compactions_total} compaction(s), ` : ""}${summary.dropped_event_count_exact ? summary.dropped_events : "unknown"} dropped event(s), ${summary.dropped_bytes} bytes`)];
481
507
  if (items.length === 0)
482
- return [this.theme.fg("muted", " No Trace evidence")];
508
+ return [...banner, this.theme.fg("muted", " No Trace evidence")];
509
+ if (this.selectedTraceId) {
510
+ const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
511
+ if (selectedIndex >= 0)
512
+ this.rowIndex = selectedIndex;
513
+ else
514
+ this.selectedTraceId = undefined;
515
+ }
483
516
  this.rowIndex = Math.min(this.rowIndex, items.length - 1);
517
+ if (this.focus === "list" && !this.selectedTraceId) {
518
+ this.selectedTraceId = items[this.rowIndex]?.id;
519
+ }
484
520
  const viewportRows = this.contentViewportRows();
485
521
  const maxStart = Math.max(0, items.length - viewportRows);
486
522
  const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
487
523
  this.contentStripeIndices = items
488
524
  .slice(start, start + viewportRows)
489
525
  .map((_item, offset) => start + offset);
490
- return items.slice(start, start + viewportRows).map((item, offset) => {
491
- const index = start + offset;
492
- const detail = item.detail && typeof item.detail === "object"
493
- ? item.detail
494
- : {};
495
- const marker = item.level === "error"
496
- ? this.theme.fg("error", "!")
497
- : detail.attention
498
- ? this.theme.fg("warning", "•")
499
- : this.theme.fg("muted", "·");
500
- const prefix = this.focus === "list" && index === this.rowIndex
501
- ? this.theme.fg("accent", " ▶ ")
502
- : " ";
503
- const row = `${prefix}${this.theme.fg("text", `#${index + 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
504
- return this.focus === "list" && index === this.rowIndex
505
- ? this.theme.fg("accent", row)
506
- : row;
507
- });
526
+ return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
527
+ const index = start + offset;
528
+ const detail = item.detail && typeof item.detail === "object"
529
+ ? item.detail
530
+ : {};
531
+ const marker = item.level === "error"
532
+ ? this.theme.fg("error", "!")
533
+ : detail.attention
534
+ ? this.theme.fg("warning", "A")
535
+ : " ";
536
+ const prefix = this.focus === "list" && index === this.rowIndex
537
+ ? this.theme.fg("accent", " ▶ ")
538
+ : " ";
539
+ const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
540
+ return this.focus === "list" && index === this.rowIndex
541
+ ? this.theme.fg("accent", row)
542
+ : row;
543
+ })];
508
544
  }
509
545
  renderTraceDetail(width) {
510
546
  if (!this.traceDetail)
@@ -520,13 +556,29 @@ export class ActorInspectorOverlay {
520
556
  ? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
521
557
  : undefined;
522
558
  const meta = this.readRunMeta(stateDir);
559
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
560
+ const capacity = computeRunControlCapacity(journal.records);
561
+ const stalePending = journal.records.reduce((count, control) => count + Number(Boolean(RuntimeTriage.classifyRuntimeControl({ run: run.run,
562
+ runInstanceId: run.runInstanceId, status: run.status }, control, Date.now()).stale)), 0);
523
563
  return {
524
564
  status: run.status,
525
565
  run_instance_id: run.runInstanceId,
566
+ pending: capacity.pending, pending_limit: capacity.limit,
567
+ available: capacity.available, backpressured: capacity.backpressured,
568
+ journal_bytes: (() => { try {
569
+ return statSync(RunsControls.runControlsFile(stateDir)).size;
570
+ }
571
+ catch {
572
+ return 0;
573
+ } })(),
574
+ journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
575
+ stale_pending: stalePending,
576
+ diagnostics: journal.diagnostics.map((item) => item.line === undefined
577
+ ? { reason: "unreadable_control_journal" } : { line: item.line, reason: "invalid_control_json" }),
526
578
  actor_actions: Array.isArray(meta.control) ? meta.control : [],
527
579
  runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
528
580
  endpoint,
529
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir).slice(-20).reverse(),
581
+ recent_controls: journal.records.slice(-20).reverse().map((control) => ControlProjection.projectRunControl(control)),
530
582
  };
531
583
  }
532
584
  allTraceItems(run) {
@@ -567,14 +619,7 @@ export class ActorInspectorOverlay {
567
619
  }
568
620
  documentProjection(value, width) {
569
621
  const sections = value && typeof value === "object" && !Array.isArray(value)
570
- ? Object.entries(value).map(([key, item]) => {
571
- const inline = this.inlineDocumentValue(item);
572
- return inline !== undefined
573
- ? [`${key}: ${inline}`]
574
- : item && typeof item === "object"
575
- ? [`${key}:`, ...this.document(item, 1)]
576
- : this.labeledScalarLines(key, item);
577
- })
622
+ ? Object.entries(value).map(([key, item]) => this.labeledDocumentLines(key, item))
578
623
  : [this.document(value)];
579
624
  const lines = [];
580
625
  const stripes = [];
@@ -596,19 +641,22 @@ export class ActorInspectorOverlay {
596
641
  return undefined;
597
642
  }
598
643
  if (value && typeof value === "object") {
599
- const entries = Object.entries(value);
600
- if (entries.length === 0)
644
+ if (Object.keys(value).length === 0)
601
645
  return "{}";
602
- if (entries.length <= 3 &&
603
- entries.every(([, item]) => (item === null || typeof item !== "object") &&
604
- !(typeof item === "string" && /[\r\n]/u.test(item)))) {
605
- return `{ ${entries
606
- .map(([key, item]) => `${key}: ${String(item ?? "none")}`)
607
- .join(", ")} }`;
608
- }
609
646
  }
610
647
  return undefined;
611
648
  }
649
+ labeledDocumentLines(key, value, depth = 0) {
650
+ const inline = this.inlineDocumentValue(value);
651
+ if (inline !== undefined)
652
+ return [`${" ".repeat(depth)}${key}: ${inline}`];
653
+ if (value && typeof value === "object") {
654
+ const indent = " ".repeat(depth);
655
+ const nested = this.document(value, depth);
656
+ return [`${indent}${key}: ${nested[0].slice(indent.length)}`, ...nested.slice(1)];
657
+ }
658
+ return this.labeledScalarLines(key, value, depth);
659
+ }
612
660
  labeledScalarLines(key, value, depth = 0) {
613
661
  const indent = " ".repeat(depth);
614
662
  const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
@@ -624,25 +672,32 @@ export class ActorInspectorOverlay {
624
672
  if (inline !== undefined)
625
673
  return [`${" ".repeat(depth)}${inline}`];
626
674
  if (Array.isArray(value)) {
627
- return value.flatMap((item, index) => {
628
- const marker = `${" ".repeat(depth)}- #${index + 1}`;
629
- const itemInline = this.inlineDocumentValue(item);
630
- return itemInline !== undefined
631
- ? [`${marker}: ${itemInline}`]
632
- : [marker, ...this.document(item, depth + 1)];
633
- });
675
+ const indent = " ".repeat(depth);
676
+ return [
677
+ `${indent}[`,
678
+ ...value.flatMap((item, index) => {
679
+ const marker = `${indent} - #${index + 1}`;
680
+ const itemInline = this.inlineDocumentValue(item);
681
+ return itemInline !== undefined
682
+ ? [`${marker}: ${itemInline}`]
683
+ : [marker, ...this.document(item, depth + 2)];
684
+ }),
685
+ `${indent}]`,
686
+ ];
634
687
  }
635
688
  if (typeof value === "object") {
636
- return Object.entries(value).flatMap(([key, item]) => {
637
- const itemInline = this.inlineDocumentValue(item);
638
- if (itemInline !== undefined) {
639
- return [`${" ".repeat(depth)}${key}: ${itemInline}`];
640
- }
641
- if (item && typeof item === "object") {
642
- return [`${" ".repeat(depth)}${key}:`, ...this.document(item, depth + 1)];
643
- }
644
- return this.labeledScalarLines(key, item, depth);
645
- });
689
+ const indent = " ".repeat(depth);
690
+ const entries = Object.entries(value);
691
+ return [
692
+ `${indent}{`,
693
+ ...entries.flatMap(([key, item], index) => {
694
+ const lines = this.labeledDocumentLines(key, item, depth + 1);
695
+ if (index < entries.length - 1)
696
+ lines[lines.length - 1] += ",";
697
+ return lines;
698
+ }),
699
+ `${indent}}`,
700
+ ];
646
701
  }
647
702
  return String(value)
648
703
  .split(/\r?\n/u)
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Shared output and preview size limits.
3
- * Zones: output governance, Trace previews, inspect defaults
2
+ * Shared output, preview, and Control envelope limits.
3
+ * Zones: output governance, Trace previews, Inspect defaults, Control portability
4
4
  */
5
5
  export declare const DEFAULT_INSPECT_LINES = 40;
6
6
  export declare const TOOL_OUTPUT_MAX_BYTES: number;
7
7
  export declare const TOOL_OUTPUT_MAX_LINES = 2000;
8
8
  export declare const COMPACT_PREVIEW_CHARS = 160;
9
- export declare const CONTROL_INPUT_MAX_BYTES: number;
9
+ export declare const CONTROL_ACTION_MAX_LENGTH = 64;
10
+ export declare const CONTROL_INPUT_MAX_BYTES = 380;
11
+ export declare const CONTROL_WIRE_MAX_BYTES = 512;
10
12
  export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
11
13
  export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
12
14
  export declare const SESSION_EVIDENCE_MAX_TURNS = 100;
@@ -14,3 +16,13 @@ export declare const SESSION_EVIDENCE_TEXT_CHARS = 4000;
14
16
  export declare const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
15
17
  export declare const TRACE_EVENT_MAX_BYTES: number;
16
18
  export declare const TRACE_EVENT_MAX_READ = 200;
19
+ export declare const TRACE_JOURNAL_MAX_EVENTS = 2048;
20
+ export declare const TRACE_JOURNAL_TARGET_EVENTS = 1536;
21
+ export declare const TRACE_JOURNAL_MAX_BYTES: number;
22
+ export declare const TRACE_JOURNAL_TARGET_BYTES: number;
23
+ export declare const RUN_CONTROL_PENDING_LIMIT = 64;
24
+ export declare const RUN_CONTROL_TERMINAL_LIMIT = 128;
25
+ export declare const RUN_CONTROL_ERROR_MAX_BYTES: number;
26
+ export declare const RUN_CONTROL_JOURNAL_MAX_BYTES: number;
27
+ export declare const RUN_RETENTION_MAX_RECORDS = 256;
28
+ export declare const RUN_RETENTION_MAX_BYTES: number;