praxis-agent 0.35.0 → 0.37.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 (111) hide show
  1. package/README.md +20 -2
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/durable-follow-up.d.ts +14 -0
  7. package/dist/application/durable-follow-up.js +52 -0
  8. package/dist/application/native-session-transcript.d.ts +72 -0
  9. package/dist/application/native-session-transcript.js +351 -0
  10. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  11. package/dist/application/native-sidechain-transcript.js +156 -0
  12. package/dist/application/session-service.d.ts +23 -4
  13. package/dist/application/session-service.js +1130 -494
  14. package/dist/application/subagent-service.d.ts +5 -1
  15. package/dist/application/subagent-service.js +749 -320
  16. package/dist/application/team-agent-runtime.d.ts +29 -0
  17. package/dist/application/team-agent-runtime.js +169 -0
  18. package/dist/application/team-capability.d.ts +27 -0
  19. package/dist/application/team-capability.js +41 -0
  20. package/dist/application/team-lead-operations.d.ts +38 -0
  21. package/dist/application/team-lead-operations.js +155 -0
  22. package/dist/application/team-mailbox.d.ts +54 -0
  23. package/dist/application/team-mailbox.js +196 -0
  24. package/dist/application/team-manager.d.ts +113 -0
  25. package/dist/application/team-manager.js +681 -0
  26. package/dist/application/team-workspace.d.ts +29 -0
  27. package/dist/application/team-workspace.js +197 -0
  28. package/dist/application/top-level-agent-manager.d.ts +1 -0
  29. package/dist/application/top-level-agent-manager.js +1025 -403
  30. package/dist/application/transcript-projection.d.ts +35 -0
  31. package/dist/application/transcript-projection.js +139 -0
  32. package/dist/application/turn-lifecycle.d.ts +2 -0
  33. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  34. package/dist/application/turn-memory-coordinator.js +98 -0
  35. package/dist/cli/agents-dashboard.js +10 -5
  36. package/dist/cli/interactive.d.ts +7 -0
  37. package/dist/cli/interactive.js +645 -334
  38. package/dist/cli/tui/claude-style.d.ts +8 -25
  39. package/dist/cli/tui/claude-style.js +299 -277
  40. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  41. package/dist/cli/tui/decision-surface-model.js +131 -0
  42. package/dist/cli/tui/decision-surface.d.ts +7 -0
  43. package/dist/cli/tui/decision-surface.js +41 -0
  44. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  45. package/dist/cli/tui/help-surface-model.js +131 -0
  46. package/dist/cli/tui/mcp-panel.js +1 -2
  47. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  48. package/dist/cli/tui/permission-surface-model.js +360 -0
  49. package/dist/cli/tui/permission-surface.d.ts +7 -0
  50. package/dist/cli/tui/permission-surface.js +69 -0
  51. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  52. package/dist/cli/tui/presentation-environment.js +149 -0
  53. package/dist/cli/tui/task-panel.js +10 -1
  54. package/dist/cli/tui/theme.d.ts +46 -3
  55. package/dist/cli/tui/theme.js +414 -56
  56. package/dist/cli/tui/tool-permission.js +16 -8
  57. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  58. package/dist/cli/tui/transcript-presentation.js +26 -6
  59. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  60. package/dist/cli/tui/transcript-viewport.js +517 -111
  61. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  62. package/dist/cli/tui/transcript-window-model.js +772 -0
  63. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  64. package/dist/cli/tui/tui-interaction-router.js +127 -0
  65. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  66. package/dist/cli/tui/tui-screen-model.js +210 -0
  67. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  68. package/dist/cli/tui/tui-view-model.js +1 -68
  69. package/dist/cli-runtime.d.ts +4 -2
  70. package/dist/cli-runtime.js +397 -32
  71. package/dist/compatibility/claude/transcript-codec.js +6 -0
  72. package/dist/core/agent-orchestration.d.ts +31 -0
  73. package/dist/core/agent-orchestration.js +235 -0
  74. package/dist/core/permission-policy.d.ts +4 -0
  75. package/dist/core/permission-policy.js +21 -0
  76. package/dist/core/runtime.d.ts +6 -0
  77. package/dist/core/runtime.js +20 -2
  78. package/dist/core/team-mailbox.d.ts +54 -0
  79. package/dist/core/team-mailbox.js +212 -0
  80. package/dist/core/team-ownership.d.ts +84 -0
  81. package/dist/core/team-ownership.js +505 -0
  82. package/dist/core/tool-execution-scheduler.js +6 -1
  83. package/dist/core/transcript-event.d.ts +5 -1
  84. package/dist/core/transcript-event.js +12 -0
  85. package/dist/persistence/claude-job-store.d.ts +58 -0
  86. package/dist/persistence/claude-job-store.js +483 -1
  87. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  88. package/dist/persistence/claude-transcript-store.js +2 -10
  89. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  90. package/dist/persistence/native-team-mailbox.js +542 -0
  91. package/dist/persistence/native-team-store.d.ts +34 -0
  92. package/dist/persistence/native-team-store.js +257 -0
  93. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  94. package/dist/persistence/native-transcript-reader.js +283 -0
  95. package/dist/persistence/native-transcript-store.d.ts +69 -0
  96. package/dist/persistence/native-transcript-store.js +321 -0
  97. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  98. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  99. package/dist/persistence/transcript-file-append.d.ts +2 -0
  100. package/dist/persistence/transcript-file-append.js +10 -0
  101. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  102. package/dist/platform/exclusive-file-lease.js +3 -0
  103. package/dist/tools/claude-capabilities.d.ts +4 -0
  104. package/dist/tools/claude-capabilities.js +18 -2
  105. package/dist/tools/team-lead-tools.d.ts +19 -0
  106. package/dist/tools/team-lead-tools.js +479 -0
  107. package/dist/tools/team-member-tools.d.ts +21 -0
  108. package/dist/tools/team-member-tools.js +259 -0
  109. package/package.json +1 -1
  110. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  111. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -2,20 +2,26 @@ import { readFile, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { isClaudeSessionId } from '../compatibility/claude/paths.js';
4
4
  import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
5
+ import { createAgentLifecycle, continueLifecycle, isTerminalLifecycleState, markLifecycleOrphaned, parseAgentLifecycleSnapshot, recoverLifecycle, transitionLifecycle, } from '../core/agent-orchestration.js';
6
+ import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
5
7
  import { writeFileAtomically } from '../platform/atomic-write.js';
6
- const statuses = new Set([
7
- 'running',
8
- 'completed',
9
- 'failed',
10
- 'killed',
11
- ]);
8
+ function statusFor(snapshot) {
9
+ if (snapshot.state === 'completed')
10
+ return 'completed';
11
+ if (snapshot.state === 'failed')
12
+ return 'failed';
13
+ if (snapshot.state === 'cancelled')
14
+ return 'killed';
15
+ return 'running';
16
+ }
17
+ function publicRecord(record) {
18
+ return Object.assign({ ...record }, { status: statusFor(record.lifecycle) });
19
+ }
12
20
  function assertIdentity(sessionId, agentId) {
13
- if (!isClaudeSessionId(sessionId)) {
21
+ if (!isClaudeSessionId(sessionId))
14
22
  throw new Error(`Invalid subagent lifecycle session ID: ${sessionId}`);
15
- }
16
- if (!isClaudeAgentId(agentId)) {
23
+ if (!isClaudeAgentId(agentId))
17
24
  throw new Error(`Invalid subagent lifecycle agent ID: ${agentId}`);
18
- }
19
25
  }
20
26
  function isNonnegativeInteger(value) {
21
27
  return Number.isSafeInteger(value) && Number(value) >= 0;
@@ -24,54 +30,43 @@ function isNonnegativeNumber(value) {
24
30
  return typeof value === 'number' && Number.isFinite(value) && value >= 0;
25
31
  }
26
32
  function isUsage(value) {
27
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
33
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
28
34
  return false;
29
- }
30
35
  const usage = value;
31
36
  if (!isNonnegativeInteger(usage.inputTokens) ||
32
- !isNonnegativeInteger(usage.outputTokens)) {
37
+ !isNonnegativeInteger(usage.outputTokens))
33
38
  return false;
34
- }
35
39
  for (const field of [
36
40
  'cacheReadInputTokens',
37
41
  'cacheCreationInputTokens',
38
42
  'webSearchRequests',
39
- ]) {
40
- if (usage[field] !== undefined && !isNonnegativeInteger(usage[field])) {
43
+ ])
44
+ if (usage[field] !== undefined && !isNonnegativeInteger(usage[field]))
41
45
  return false;
42
- }
43
- }
44
- for (const field of ['contextWindow', 'maxOutputTokens']) {
46
+ for (const field of ['contextWindow', 'maxOutputTokens'])
45
47
  if (usage[field] !== undefined &&
46
- (!Number.isSafeInteger(usage[field]) || Number(usage[field]) < 1)) {
48
+ (!Number.isSafeInteger(usage[field]) || Number(usage[field]) < 1))
47
49
  return false;
48
- }
49
- }
50
50
  return true;
51
51
  }
52
52
  function isRunResult(value) {
53
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
53
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
54
54
  return false;
55
- }
56
55
  const result = value;
57
56
  if (typeof result.text !== 'string' ||
58
57
  !isUsage(result.usage) ||
59
58
  !isNonnegativeInteger(result.toolUseCount) ||
60
- !isNonnegativeInteger(result.durationMs)) {
59
+ !isNonnegativeInteger(result.durationMs))
61
60
  return false;
62
- }
63
61
  if (result.modelUsage !== undefined &&
64
62
  (typeof result.modelUsage !== 'object' ||
65
63
  result.modelUsage === null ||
66
64
  Array.isArray(result.modelUsage) ||
67
- !Object.values(result.modelUsage).every(isUsage))) {
65
+ !Object.values(result.modelUsage).every(isUsage)))
68
66
  return false;
69
- }
70
- for (const field of ['durationApiMs', 'durationApiWithoutRetriesMs']) {
71
- if (result[field] !== undefined && !isNonnegativeNumber(result[field])) {
67
+ for (const field of ['durationApiMs', 'durationApiWithoutRetriesMs'])
68
+ if (result[field] !== undefined && !isNonnegativeNumber(result[field]))
72
69
  return false;
73
- }
74
- }
75
70
  return ((result.isolationPath === undefined ||
76
71
  typeof result.isolationPath === 'string') &&
77
72
  (result.isolationRetained === undefined ||
@@ -80,9 +75,8 @@ function isRunResult(value) {
80
75
  typeof result.isolationWarning === 'string'));
81
76
  }
82
77
  function isNotification(value) {
83
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
78
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
84
79
  return false;
85
- }
86
80
  const notification = value;
87
81
  const accounting = notification.accounting;
88
82
  return (typeof notification.id === 'string' &&
@@ -103,12 +97,76 @@ function isNotification(value) {
103
97
  0 &&
104
98
  typeof accounting.delivered === 'boolean')));
105
99
  }
106
- /** Praxis-private state for a retained Claude-compatible sidechain. */
100
+ function ownerFromLease(lease) {
101
+ return { token: lease.token, pid: lease.pid, acquiredAt: lease.createdAt };
102
+ }
103
+ export class SubagentExecution {
104
+ store;
105
+ lease;
106
+ token;
107
+ generation;
108
+ current;
109
+ released = false;
110
+ constructor(store, lease, token, generation, current) {
111
+ this.store = store;
112
+ this.lease = lease;
113
+ this.token = token;
114
+ this.generation = generation;
115
+ this.current = current;
116
+ }
117
+ get snapshot() {
118
+ return this.current;
119
+ }
120
+ async transition(state) {
121
+ this.ensureActive();
122
+ this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => this.store.withLifecycle(record, transitionLifecycle(record.lifecycle, state, this.token)));
123
+ return this.current;
124
+ }
125
+ async running() {
126
+ return this.transition('running');
127
+ }
128
+ async beginCancellation() {
129
+ return this.transition('cancelling');
130
+ }
131
+ async finish(state, result, detail, notification) {
132
+ this.ensureActive();
133
+ this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => {
134
+ const lifecycle = transitionLifecycle(record.lifecycle, state, this.token);
135
+ const notifications = [...(record.notifications ?? [])];
136
+ if (notification && result)
137
+ notifications.push({ ...notification, result, consumed: false });
138
+ return this.store.withLifecycle(record, lifecycle, {
139
+ ...(detail === undefined ? {} : { detail }),
140
+ ...(result === undefined ? {} : { result }),
141
+ ...(notifications.length === 0 ? {} : { notifications }),
142
+ });
143
+ }, true);
144
+ return this.current;
145
+ }
146
+ async release() {
147
+ if (this.released)
148
+ return;
149
+ try {
150
+ if (!isTerminalLifecycleState(this.current.state))
151
+ this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => this.store.withLifecycle(record, markLifecycleOrphaned(record.lifecycle, this.token)));
152
+ }
153
+ finally {
154
+ this.released = true;
155
+ await this.lease.release();
156
+ }
157
+ }
158
+ ensureActive() {
159
+ if (this.released)
160
+ throw new Error('Lifecycle execution has been released');
161
+ }
162
+ }
107
163
  export class SubagentLifecycleStore {
108
164
  sessionId;
109
165
  agentId;
110
166
  transcriptPath;
111
167
  path;
168
+ ownerLock;
169
+ mutationLock;
112
170
  mutation = Promise.resolve();
113
171
  constructor(praxisRoot, sessionId, agentId, transcriptPath) {
114
172
  this.sessionId = sessionId;
@@ -116,141 +174,151 @@ export class SubagentLifecycleStore {
116
174
  this.transcriptPath = transcriptPath;
117
175
  assertIdentity(sessionId, agentId);
118
176
  this.path = join(praxisRoot, 'subagent-lifecycle', sessionId, `${agentId}.json`);
177
+ this.ownerLock = new ExclusiveFileLease(`${this.path}.owner.lock`);
178
+ this.mutationLock = new ExclusiveFileLease(`${this.path}.mutation.lock`);
119
179
  }
120
180
  async read() {
121
- let source;
181
+ const record = await this.readStored();
182
+ return record ? publicRecord(record) : null;
183
+ }
184
+ async acquire(mode = 'start') {
185
+ const lease = await this.ownerLock.tryAcquire();
186
+ if (!lease)
187
+ throw new Error(`Subagent lifecycle execution is already owned: ${this.path}`);
188
+ let mutationLease;
122
189
  try {
123
- source = await readFile(this.path, 'utf8');
190
+ mutationLease = await this.acquireMutationLock();
124
191
  }
125
192
  catch (error) {
126
- if (error.code === 'ENOENT')
127
- return null;
193
+ await lease.release();
128
194
  throw error;
129
195
  }
130
- let value;
131
196
  try {
132
- value = JSON.parse(source);
197
+ const record = await this.readStored();
198
+ const owner = ownerFromLease(lease);
199
+ let lifecycle;
200
+ if (!record) {
201
+ if (mode !== 'start')
202
+ throw new Error(`Cannot ${mode} missing subagent lifecycle state: ${this.path}`);
203
+ lifecycle = createAgentLifecycle(owner);
204
+ }
205
+ else if (mode === 'start') {
206
+ throw new Error(`Subagent lifecycle already exists: ${this.path}`);
207
+ }
208
+ else if (mode === 'recover') {
209
+ lifecycle = recoverLifecycle(record.lifecycle, owner);
210
+ }
211
+ else {
212
+ lifecycle = continueLifecycle(record.lifecycle, owner);
213
+ }
214
+ const next = this.withLifecycle(record ?? this.baseRecord(lifecycle), lifecycle);
215
+ await this.writeStored(next, false);
216
+ return new SubagentExecution(this, lease, owner.token, lifecycle.generation, lifecycle);
133
217
  }
134
218
  catch (error) {
135
- throw new Error(`Corrupt subagent lifecycle state: ${this.path}`, {
136
- cause: error,
137
- });
138
- }
139
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
140
- throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
219
+ await lease.release();
220
+ throw error;
141
221
  }
142
- const record = value;
143
- if (record.version !== 1 ||
144
- record.sessionId !== this.sessionId ||
145
- record.agentId !== this.agentId ||
146
- typeof record.status !== 'string' ||
147
- !statuses.has(record.status) ||
148
- typeof record.updatedAt !== 'string' ||
149
- Number.isNaN(Date.parse(record.updatedAt)) ||
150
- (record.transcriptBytes !== undefined &&
151
- !isNonnegativeInteger(record.transcriptBytes)) ||
152
- (record.detail !== undefined && typeof record.detail !== 'string') ||
153
- (record.result !== undefined && !isRunResult(record.result)) ||
154
- (record.notifications !== undefined &&
155
- (!Array.isArray(record.notifications) ||
156
- !record.notifications.every(isNotification)))) {
157
- throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
222
+ finally {
223
+ await mutationLease.release();
158
224
  }
159
- return record;
160
225
  }
161
- write(status, detail, terminal) {
162
- return this.mutate(async (current) => {
163
- const notifications = [...(current?.notifications ?? [])];
164
- if (terminal?.notification) {
165
- notifications.push({
166
- ...terminal.notification,
167
- result: terminal.result,
168
- consumed: false,
169
- });
226
+ start() {
227
+ return this.acquire('start');
228
+ }
229
+ continue() {
230
+ return this.acquire('continue');
231
+ }
232
+ recover() {
233
+ return this.acquire('recover');
234
+ }
235
+ async reconcileOwnerLoss() {
236
+ const current = await this.readStored();
237
+ if (!current || isTerminalLifecycleState(current.lifecycle.state)) {
238
+ return { owned: false, snapshot: current?.lifecycle ?? null };
239
+ }
240
+ const probe = await this.ownerLock.tryAcquire();
241
+ if (!probe) {
242
+ const reread = await this.readStored();
243
+ if (!reread || isTerminalLifecycleState(reread.lifecycle.state)) {
244
+ return { owned: false, snapshot: reread?.lifecycle ?? null };
170
245
  }
171
- return this.record({
172
- status,
173
- ...(detail === undefined ? {} : { detail }),
174
- ...(terminal === undefined ? {} : { result: terminal.result }),
175
- ...(notifications.length === 0 ? {} : { notifications }),
246
+ return { owned: true, snapshot: reread.lifecycle };
247
+ }
248
+ try {
249
+ const observed = await this.readStored();
250
+ if (!observed || isTerminalLifecycleState(observed.lifecycle.state)) {
251
+ return { owned: false, snapshot: observed?.lifecycle ?? null };
252
+ }
253
+ const reconciled = await this.mutate(async (latest) => {
254
+ if (!latest)
255
+ return observed;
256
+ if (latest.lifecycle.generation !== observed.lifecycle.generation ||
257
+ latest.lifecycle.owner?.token !== observed.lifecycle.owner?.token)
258
+ return latest;
259
+ return this.withLifecycle(latest, markLifecycleOrphaned(latest.lifecycle, latest.lifecycle.owner?.token ?? ''));
176
260
  });
177
- });
261
+ return { owned: false, snapshot: reconciled.lifecycle };
262
+ }
263
+ finally {
264
+ await probe.release();
265
+ }
178
266
  }
179
267
  acknowledgeNotification(id) {
180
268
  return this.mutate(async (current) => {
181
- if (!current) {
269
+ if (!current)
182
270
  throw new Error(`Missing subagent lifecycle state: ${this.path}`);
183
- }
184
271
  const notifications = current.notifications ?? [];
185
- const matched = notifications.find((notification) => notification.id === id && !notification.consumed);
186
- if (!matched)
272
+ if (!notifications.some((n) => n.id === id && !n.consumed))
187
273
  return current;
188
- return this.record({
189
- status: current.status,
190
- ...(current.detail === undefined ? {} : { detail: current.detail }),
191
- ...(current.result === undefined ? {} : { result: current.result }),
192
- notifications: notifications.map((notification) => notification.id === id
193
- ? { ...notification, consumed: true }
194
- : notification),
195
- }, { transcriptBytes: current.transcriptBytes });
196
- });
274
+ return this.withLifecycle(current, current.lifecycle, {
275
+ notifications: notifications.map((n) => n.id === id ? { ...n, consumed: true } : n),
276
+ });
277
+ }).then(() => undefined);
197
278
  }
198
279
  prepareNotificationDetached(id, model) {
199
- if (model.trim().length === 0) {
280
+ if (model.trim().length === 0)
200
281
  throw new Error('Detached notification model must not be blank');
201
- }
202
282
  return this.mutate(async (current) => {
203
- if (!current) {
283
+ if (!current)
204
284
  throw new Error(`Missing subagent lifecycle state: ${this.path}`);
205
- }
206
285
  const notifications = current.notifications ?? [];
207
- const matched = notifications.find((notification) => notification.id === id);
286
+ const matched = notifications.find((n) => n.id === id);
208
287
  if (!matched || matched.accounting?.kind === 'detached')
209
288
  return current;
210
- return this.record({
211
- status: current.status,
212
- ...(current.detail === undefined ? {} : { detail: current.detail }),
213
- ...(current.result === undefined ? {} : { result: current.result }),
214
- notifications: notifications.map((notification) => notification.id === id
289
+ return this.withLifecycle(current, current.lifecycle, {
290
+ notifications: notifications.map((n) => n.id === id
215
291
  ? {
216
- ...notification,
292
+ ...n,
217
293
  accounting: {
218
294
  kind: 'detached',
219
295
  model,
220
296
  delivered: false,
221
297
  },
222
298
  }
223
- : notification),
224
- }, { transcriptBytes: current.transcriptBytes });
225
- });
299
+ : n),
300
+ });
301
+ }).then(() => undefined);
226
302
  }
227
303
  confirmNotificationDetached(id) {
228
304
  return this.mutate(async (current) => {
229
- if (!current) {
305
+ if (!current)
230
306
  throw new Error(`Missing subagent lifecycle state: ${this.path}`);
231
- }
232
307
  const notifications = current.notifications ?? [];
233
- const matched = notifications.find((notification) => notification.id === id);
308
+ const matched = notifications.find((n) => n.id === id);
234
309
  if (!matched?.accounting || matched.accounting.delivered)
235
310
  return current;
236
- return this.record({
237
- status: current.status,
238
- ...(current.detail === undefined ? {} : { detail: current.detail }),
239
- ...(current.result === undefined ? {} : { result: current.result }),
240
- notifications: notifications.map((notification) => notification.id === id && notification.accounting
241
- ? {
242
- ...notification,
243
- accounting: { ...notification.accounting, delivered: true },
244
- }
245
- : notification),
246
- }, { transcriptBytes: current.transcriptBytes });
247
- });
311
+ return this.withLifecycle(current, current.lifecycle, {
312
+ notifications: notifications.map((n) => n.id === id && n.accounting
313
+ ? { ...n, accounting: { ...n.accounting, delivered: true } }
314
+ : n),
315
+ });
316
+ }).then(() => undefined);
248
317
  }
249
318
  async matchesTranscript(record) {
250
319
  if (record.transcriptBytes === undefined ||
251
- this.transcriptPath === undefined) {
320
+ this.transcriptPath === undefined)
252
321
  return true;
253
- }
254
322
  try {
255
323
  return (await stat(this.transcriptPath)).size === record.transcriptBytes;
256
324
  }
@@ -260,40 +328,199 @@ export class SubagentLifecycleStore {
260
328
  throw error;
261
329
  }
262
330
  }
263
- async record(value, boundary) {
264
- let transcriptBytes = boundary?.transcriptBytes;
265
- if (boundary === undefined && this.transcriptPath !== undefined) {
331
+ baseRecord(lifecycle) {
332
+ return {
333
+ version: 2,
334
+ sessionId: this.sessionId,
335
+ agentId: this.agentId,
336
+ lifecycle,
337
+ updatedAt: new Date().toISOString(),
338
+ };
339
+ }
340
+ withLifecycle(current, lifecycle, extra = {}) {
341
+ return {
342
+ version: 2,
343
+ sessionId: this.sessionId,
344
+ agentId: this.agentId,
345
+ lifecycle,
346
+ updatedAt: new Date().toISOString(),
347
+ ...(extra.detail === undefined
348
+ ? current.detail === undefined
349
+ ? {}
350
+ : { detail: current.detail }
351
+ : { detail: extra.detail }),
352
+ ...(extra.result === undefined
353
+ ? current.result === undefined
354
+ ? {}
355
+ : { result: current.result }
356
+ : { result: extra.result }),
357
+ ...(extra.notifications === undefined
358
+ ? current.notifications === undefined
359
+ ? {}
360
+ : { notifications: current.notifications }
361
+ : { notifications: extra.notifications }),
362
+ ...(current.transcriptBytes === undefined
363
+ ? {}
364
+ : { transcriptBytes: current.transcriptBytes }),
365
+ };
366
+ }
367
+ async mutateOwned(lease, generation, operation, refreshTranscript = false) {
368
+ const record = await this.mutate(async (current) => {
369
+ if (!current)
370
+ throw new Error(`Missing subagent lifecycle state: ${this.path}`);
371
+ if (current.lifecycle.generation !== generation ||
372
+ current.lifecycle.owner?.token !== lease.token)
373
+ throw new Error('Lifecycle execution owner token is stale or missing');
374
+ return operation(current);
375
+ }, refreshTranscript);
376
+ return record.lifecycle;
377
+ }
378
+ mutate(operation, refreshTranscript = false) {
379
+ const queued = this.mutation.then(async () => {
380
+ const lock = await this.acquireMutationLock();
266
381
  try {
267
- transcriptBytes = (await stat(this.transcriptPath)).size;
382
+ const result = await operation(await this.readStored());
383
+ await this.writeStored(result, refreshTranscript);
384
+ return result;
268
385
  }
269
- catch (error) {
270
- if (error.code !== 'ENOENT')
271
- throw error;
386
+ finally {
387
+ await lock.release();
272
388
  }
389
+ });
390
+ this.mutation = queued.then(() => undefined, () => undefined);
391
+ return queued;
392
+ }
393
+ async acquireMutationLock() {
394
+ const deadline = Date.now() + 5_000;
395
+ for (;;) {
396
+ const lock = await this.mutationLock.tryAcquire();
397
+ if (lock)
398
+ return lock;
399
+ if (Date.now() >= deadline)
400
+ throw new Error(`Subagent lifecycle mutation is busy: ${this.path}`);
401
+ await new Promise((resolve) => setTimeout(resolve, 10));
402
+ }
403
+ }
404
+ async readStored() {
405
+ let source;
406
+ try {
407
+ source = await readFile(this.path, 'utf8');
408
+ }
409
+ catch (error) {
410
+ if (error.code === 'ENOENT')
411
+ return null;
412
+ throw error;
413
+ }
414
+ let value;
415
+ try {
416
+ value = JSON.parse(source);
417
+ }
418
+ catch (error) {
419
+ throw new Error(`Corrupt subagent lifecycle state: ${this.path}`, {
420
+ cause: error,
421
+ });
422
+ }
423
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
424
+ throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
425
+ const record = value;
426
+ if (record.version === 1)
427
+ return this.normalizeLegacy(record);
428
+ let lifecycle;
429
+ try {
430
+ lifecycle = parseAgentLifecycleSnapshot(record.lifecycle);
431
+ }
432
+ catch {
433
+ throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
434
+ }
435
+ if (record.version !== 2 ||
436
+ record.sessionId !== this.sessionId ||
437
+ record.agentId !== this.agentId ||
438
+ typeof record.updatedAt !== 'string' ||
439
+ Number.isNaN(Date.parse(record.updatedAt)) ||
440
+ (record.transcriptBytes !== undefined &&
441
+ !isNonnegativeInteger(record.transcriptBytes)) ||
442
+ (record.detail !== undefined && typeof record.detail !== 'string') ||
443
+ (record.result !== undefined && !isRunResult(record.result)) ||
444
+ (record.notifications !== undefined &&
445
+ (!Array.isArray(record.notifications) ||
446
+ !record.notifications.every(isNotification))))
447
+ throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
448
+ return { ...record, lifecycle };
449
+ }
450
+ normalizeLegacy(record) {
451
+ if (record.sessionId !== this.sessionId ||
452
+ record.agentId !== this.agentId ||
453
+ typeof record.status !== 'string' ||
454
+ !['running', 'completed', 'failed', 'killed'].includes(record.status) ||
455
+ typeof record.updatedAt !== 'string' ||
456
+ Number.isNaN(Date.parse(record.updatedAt)) ||
457
+ (record.transcriptBytes !== undefined &&
458
+ !isNonnegativeInteger(record.transcriptBytes)) ||
459
+ (record.detail !== undefined && typeof record.detail !== 'string') ||
460
+ (record.result !== undefined && !isRunResult(record.result)) ||
461
+ (record.notifications !== undefined &&
462
+ (!Array.isArray(record.notifications) ||
463
+ !record.notifications.every(isNotification))))
464
+ throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
465
+ const status = record.status;
466
+ const state = status === 'killed'
467
+ ? 'cancelled'
468
+ : status === 'running'
469
+ ? 'orphaned'
470
+ : status;
471
+ const terminal = isTerminalLifecycleState(state);
472
+ let lifecycle;
473
+ try {
474
+ lifecycle = parseAgentLifecycleSnapshot({
475
+ generation: 1,
476
+ revision: 0,
477
+ state,
478
+ owner: null,
479
+ previousOwnerToken: null,
480
+ terminalAt: terminal ? record.updatedAt : null,
481
+ acceptance: 'pending',
482
+ });
483
+ }
484
+ catch {
485
+ throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
273
486
  }
274
487
  return {
275
- version: 1,
488
+ version: 2,
276
489
  sessionId: this.sessionId,
277
490
  agentId: this.agentId,
278
- status: value.status,
279
- updatedAt: new Date().toISOString(),
280
- ...(transcriptBytes === undefined ? {} : { transcriptBytes }),
281
- ...(value.detail === undefined ? {} : { detail: value.detail }),
282
- ...(value.result === undefined ? {} : { result: value.result }),
283
- ...(value.notifications === undefined
491
+ lifecycle,
492
+ updatedAt: record.updatedAt,
493
+ ...(record.transcriptBytes === undefined
494
+ ? {}
495
+ : { transcriptBytes: record.transcriptBytes }),
496
+ ...(record.detail === undefined
497
+ ? {}
498
+ : { detail: record.detail }),
499
+ ...(record.result === undefined
284
500
  ? {}
285
- : { notifications: value.notifications }),
501
+ : { result: record.result }),
502
+ ...(record.notifications === undefined
503
+ ? {}
504
+ : {
505
+ notifications: record.notifications,
506
+ }),
286
507
  };
287
508
  }
288
- mutate(mutation) {
289
- const operation = this.mutation.then(async () => {
290
- const record = await mutation(await this.read());
291
- await writeFileAtomically(this.path, `${JSON.stringify(record)}\n`, {
292
- mode: 0o600,
293
- });
509
+ async writeStored(record, refreshTranscript) {
510
+ let transcriptBytes = record.transcriptBytes;
511
+ if (refreshTranscript && this.transcriptPath !== undefined) {
512
+ try {
513
+ transcriptBytes = (await stat(this.transcriptPath)).size;
514
+ }
515
+ catch (error) {
516
+ if (error.code !== 'ENOENT')
517
+ throw error;
518
+ }
519
+ }
520
+ const next = transcriptBytes === undefined ? record : { ...record, transcriptBytes };
521
+ await writeFileAtomically(this.path, `${JSON.stringify(next)}\n`, {
522
+ mode: 0o600,
294
523
  });
295
- this.mutation = operation.catch(() => undefined);
296
- return operation;
297
524
  }
298
525
  }
299
526
  //# sourceMappingURL=subagent-lifecycle-store.js.map
@@ -0,0 +1,2 @@
1
+ export declare function classifyTranscriptAppend(written: Uint8Array, previousByteLength: number, encodedLine: Uint8Array): 'appended' | 'interleaved-write';
2
+ //# sourceMappingURL=transcript-file-append.d.ts.map
@@ -0,0 +1,10 @@
1
+ export function classifyTranscriptAppend(written, previousByteLength, encodedLine) {
2
+ const expectedEnd = previousByteLength + encodedLine.length;
3
+ if (written.length === expectedEnd &&
4
+ Buffer.from(written)
5
+ .subarray(previousByteLength, expectedEnd)
6
+ .equals(encodedLine))
7
+ return 'appended';
8
+ return 'interleaved-write';
9
+ }
10
+ //# sourceMappingURL=transcript-file-append.js.map
@@ -1,4 +1,7 @@
1
1
  export interface ExclusiveFileLeaseHandle {
2
+ readonly token: string;
3
+ readonly pid: number;
4
+ readonly createdAt: string;
2
5
  release(): Promise<void>;
3
6
  }
4
7
  export declare class ExclusiveFileLease {