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,7 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, open, readFile, readdir, rm } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { writeFileAtomically } from '../platform/atomic-write.js';
5
- import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
5
+ import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
6
+ import { createAgentLifecycle, isTerminalLifecycleState, markLifecycleOrphaned, parseAgentLifecycleSnapshot, recoverLifecycle, transitionLifecycle, transferLifecycleOwner, } from '../core/agent-orchestration.js';
6
7
  const JOB_ID_PATTERN = /^[0-9a-f]{8}$/u;
7
8
  const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
8
9
  const LOCK_WAIT_MS = 5_000;
@@ -98,6 +99,170 @@ function parseDispatch(source, filePath) {
98
99
  }
99
100
  return value;
100
101
  }
102
+ function ownerFromLease(lease) {
103
+ return { token: lease.token, pid: lease.pid, acquiredAt: lease.createdAt };
104
+ }
105
+ function legacyLifecycleState(state) {
106
+ if (state.state === 'stopped')
107
+ return 'cancelled';
108
+ if (state.state === 'failed')
109
+ return 'failed';
110
+ return state.tempo === 'active' ? 'running' : 'waiting';
111
+ }
112
+ function projectState(state, lifecycle) {
113
+ const next = {
114
+ ...state,
115
+ state: lifecycle.state === 'failed' || lifecycle.state === 'orphaned'
116
+ ? 'failed'
117
+ : lifecycle.state === 'cancelled' || lifecycle.state === 'completed'
118
+ ? 'stopped'
119
+ : 'working',
120
+ tempo: lifecycle.state === 'waiting'
121
+ ? state.needs !== undefined
122
+ ? 'blocked'
123
+ : 'idle'
124
+ : lifecycle.state === 'queued'
125
+ ? state.needs !== undefined
126
+ ? 'blocked'
127
+ : 'active'
128
+ : isTerminalLifecycleState(lifecycle.state)
129
+ ? 'idle'
130
+ : 'active',
131
+ firstTerminalAt: isTerminalLifecycleState(lifecycle.state)
132
+ ? lifecycle.terminalAt
133
+ : null,
134
+ updatedAt: new Date().toISOString(),
135
+ };
136
+ if (isTerminalLifecycleState(lifecycle.state)) {
137
+ delete next.pid;
138
+ delete next.socketPath;
139
+ delete next.controlToken;
140
+ delete next.inFlight;
141
+ delete next.needs;
142
+ }
143
+ return next;
144
+ }
145
+ function projectionMatches(state, projection) {
146
+ return (state.state === projection.state &&
147
+ state.tempo === projection.tempo &&
148
+ state.firstTerminalAt === projection.firstTerminalAt &&
149
+ state.pid === projection.pid &&
150
+ state.socketPath === projection.socketPath &&
151
+ state.controlToken === projection.controlToken &&
152
+ state.needs === projection.needs &&
153
+ JSON.stringify(state.inFlight) === JSON.stringify(projection.inFlight));
154
+ }
155
+ export class ClaudeJobExecution {
156
+ store;
157
+ lease;
158
+ token;
159
+ generation;
160
+ jobId;
161
+ current;
162
+ released = false;
163
+ constructor(store, lease, token, generation, jobId, current) {
164
+ this.store = store;
165
+ this.lease = lease;
166
+ this.token = token;
167
+ this.generation = generation;
168
+ this.jobId = jobId;
169
+ this.current = current;
170
+ }
171
+ get snapshot() {
172
+ return this.current;
173
+ }
174
+ async update(mutateState) {
175
+ this.ensureActive();
176
+ const result = await this.store.mutateExecution(this, (lifecycle, state) => ({ lifecycle, state: mutateState(state) }));
177
+ this.current = result.lifecycle;
178
+ return result.state;
179
+ }
180
+ async running(mutateState) {
181
+ return this.transition('running', mutateState);
182
+ }
183
+ async waiting(mutateState) {
184
+ return this.transition('waiting', mutateState);
185
+ }
186
+ async beginCancellation(mutateState) {
187
+ return this.transition('cancelling', mutateState);
188
+ }
189
+ async finish(state, mutateState) {
190
+ return this.transition(state, mutateState);
191
+ }
192
+ async handoff() {
193
+ if (this.released)
194
+ return;
195
+ if (this.current.state !== 'queued')
196
+ throw new Error('Lifecycle handoff is only allowed while queued');
197
+ let ownershipError;
198
+ try {
199
+ await this.store.assertExecutionOwner(this);
200
+ }
201
+ catch (error) {
202
+ ownershipError = error;
203
+ }
204
+ let releaseError;
205
+ try {
206
+ await this.lease.release();
207
+ }
208
+ catch (error) {
209
+ releaseError = error;
210
+ }
211
+ if (releaseError === undefined)
212
+ this.released = true;
213
+ if (ownershipError !== undefined && releaseError !== undefined) {
214
+ throw new AggregateError([ownershipError, releaseError], 'Lifecycle handoff failed while releasing execution lease');
215
+ }
216
+ if (ownershipError !== undefined)
217
+ throw ownershipError;
218
+ if (releaseError !== undefined)
219
+ throw releaseError;
220
+ }
221
+ async release() {
222
+ if (this.released)
223
+ return;
224
+ let orphanError;
225
+ if (!isTerminalLifecycleState(this.current.state)) {
226
+ try {
227
+ this.current = await this.store.orphanExecution(this);
228
+ }
229
+ catch (error) {
230
+ if (!(error instanceof Error) ||
231
+ !/stale or missing/u.test(error.message))
232
+ orphanError = error;
233
+ }
234
+ }
235
+ let releaseError;
236
+ try {
237
+ await this.lease.release();
238
+ }
239
+ catch (error) {
240
+ releaseError = error;
241
+ }
242
+ if (releaseError === undefined && orphanError === undefined)
243
+ this.released = true;
244
+ if (orphanError !== undefined && releaseError !== undefined) {
245
+ throw new AggregateError([orphanError, releaseError], 'Lifecycle orphaning and execution lease release both failed');
246
+ }
247
+ if (orphanError !== undefined)
248
+ throw orphanError;
249
+ if (releaseError !== undefined)
250
+ throw releaseError;
251
+ }
252
+ async transition(next, mutateState) {
253
+ this.ensureActive();
254
+ const result = await this.store.mutateExecution(this, (lifecycle, state) => ({
255
+ lifecycle: transitionLifecycle(lifecycle, next, this.token),
256
+ state: mutateState ? mutateState(state) : state,
257
+ }));
258
+ this.current = result.lifecycle;
259
+ return result.lifecycle;
260
+ }
261
+ ensureActive() {
262
+ if (this.released)
263
+ throw new Error('Lifecycle execution has been released or is stale');
264
+ }
265
+ }
101
266
  export class ClaudeJobStore {
102
267
  configRoot;
103
268
  lockRoot;
@@ -127,6 +292,236 @@ export class ClaudeJobStore {
127
292
  throw error;
128
293
  }
129
294
  }
295
+ async createExecution(state, dispatch) {
296
+ assertJobId(state.daemonShort);
297
+ const id = state.daemonShort;
298
+ const ownerLease = await this.ownerLease(id).tryAcquire();
299
+ if (!ownerLease)
300
+ return null;
301
+ const directory = this.jobDirectory(id);
302
+ let createdDirectory = false;
303
+ let lifecycleMayExist = false;
304
+ const cleanup = async (primary) => {
305
+ const failures = [];
306
+ if (createdDirectory) {
307
+ try {
308
+ await rm(directory, { recursive: true, force: true });
309
+ }
310
+ catch (error) {
311
+ failures.push(error);
312
+ }
313
+ }
314
+ if (lifecycleMayExist) {
315
+ try {
316
+ await rm(this.lifecyclePath(id), { force: true });
317
+ }
318
+ catch (error) {
319
+ failures.push(error);
320
+ }
321
+ }
322
+ try {
323
+ await ownerLease.release();
324
+ }
325
+ catch (error) {
326
+ failures.push(error);
327
+ }
328
+ if (failures.length > 0) {
329
+ throw new AggregateError(primary === undefined ? failures : [primary, ...failures], 'Claude job execution setup and cleanup failed');
330
+ }
331
+ if (primary !== undefined)
332
+ throw primary;
333
+ };
334
+ try {
335
+ await mkdir(dirname(directory), { recursive: true });
336
+ try {
337
+ await mkdir(directory);
338
+ createdDirectory = true;
339
+ }
340
+ catch (error) {
341
+ if (error.code !== 'EEXIST')
342
+ throw error;
343
+ await cleanup();
344
+ return null;
345
+ }
346
+ const owner = ownerFromLease(ownerLease);
347
+ const lifecycle = createAgentLifecycle(owner);
348
+ const record = {
349
+ version: 1,
350
+ jobId: id,
351
+ lifecycle,
352
+ updatedAt: new Date().toISOString(),
353
+ };
354
+ lifecycleMayExist = true;
355
+ await this.writeLifecycle(record);
356
+ await writeFileAtomically(this.dispatchPath(id), `${JSON.stringify(dispatch)}\n`);
357
+ await writeFileAtomically(this.statePath(id), `${JSON.stringify(state)}\n`);
358
+ return new ClaudeJobExecution(this, ownerLease, owner.token, lifecycle.generation, id, lifecycle);
359
+ }
360
+ catch (error) {
361
+ await cleanup(error);
362
+ return null;
363
+ }
364
+ }
365
+ async readWithLifecycle(id) {
366
+ assertJobId(id);
367
+ const state = await this.read(id);
368
+ const lifecycle = await this.readLifecycle(id);
369
+ if (!lifecycle) {
370
+ const projected = legacyLifecycleState(state);
371
+ return {
372
+ state,
373
+ lifecycleState: projected,
374
+ lifecycle: null,
375
+ legacy: true,
376
+ };
377
+ }
378
+ const projected = projectState(state, lifecycle.lifecycle);
379
+ if (!projectionMatches(state, projected)) {
380
+ const repaired = await this.withLock(id, async () => {
381
+ const latestState = await this.read(id);
382
+ const latest = await this.readLifecycle(id);
383
+ if (!latest)
384
+ return { state: latestState, lifecycle: null };
385
+ const latestProjection = projectState(latestState, latest.lifecycle);
386
+ if (!projectionMatches(latestState, latestProjection))
387
+ await writeFileAtomically(this.statePath(id), `${JSON.stringify(latestProjection)}\n`);
388
+ return { state: latestProjection, lifecycle: latest.lifecycle };
389
+ });
390
+ if (!repaired.lifecycle)
391
+ return {
392
+ state: repaired.state,
393
+ lifecycleState: legacyLifecycleState(repaired.state),
394
+ lifecycle: null,
395
+ legacy: true,
396
+ };
397
+ return {
398
+ state: repaired.state,
399
+ lifecycleState: repaired.lifecycle.state,
400
+ lifecycle: repaired.lifecycle,
401
+ legacy: false,
402
+ };
403
+ }
404
+ return {
405
+ state,
406
+ lifecycleState: lifecycle.lifecycle.state,
407
+ lifecycle: lifecycle.lifecycle,
408
+ legacy: false,
409
+ };
410
+ }
411
+ async listWithLifecycle() {
412
+ const states = await this.list();
413
+ const views = await Promise.all(states.map(async (state) => {
414
+ try {
415
+ return await this.readWithLifecycle(state.daemonShort);
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ }));
421
+ return views.filter((view) => view !== null);
422
+ }
423
+ async claimExecution(id) {
424
+ assertJobId(id);
425
+ const lease = await this.ownerLease(id).tryAcquire();
426
+ if (!lease)
427
+ throw new Error(`Agent ${id} lifecycle execution is already owned`);
428
+ try {
429
+ const execution = await this.withLock(id, async () => {
430
+ const state = await this.read(id);
431
+ const existing = await this.readLifecycle(id);
432
+ const owner = ownerFromLease(lease);
433
+ let lifecycle;
434
+ if (!existing) {
435
+ if (state.state !== 'working')
436
+ throw new Error(`Cannot claim non-working legacy agent: ${id}`);
437
+ if (state.pid !== undefined &&
438
+ state.pid !== process.pid &&
439
+ isProcessAlive(state.pid))
440
+ throw new Error(`Agent ${id} is owned by a live process`);
441
+ lifecycle = transitionLifecycle(createAgentLifecycle(owner), 'running', owner.token);
442
+ if (state.tempo !== 'active')
443
+ lifecycle = transitionLifecycle(lifecycle, 'waiting', owner.token);
444
+ }
445
+ else if (existing.lifecycle.state === 'orphaned') {
446
+ lifecycle = recoverLifecycle(existing.lifecycle, owner);
447
+ }
448
+ else if (isTerminalLifecycleState(existing.lifecycle.state)) {
449
+ throw new Error(`Cannot claim terminal agent lifecycle: ${id}`);
450
+ }
451
+ else if (existing.lifecycle.state === 'queued') {
452
+ lifecycle = transferLifecycleOwner(existing.lifecycle, existing.lifecycle.owner?.token ?? '', owner);
453
+ }
454
+ else if (existing.lifecycle.owner) {
455
+ const orphaned = markLifecycleOrphaned(existing.lifecycle, existing.lifecycle.owner.token);
456
+ lifecycle = recoverLifecycle(orphaned, owner);
457
+ }
458
+ else {
459
+ throw new Error(`Invalid claimable lifecycle: ${id}`);
460
+ }
461
+ const next = {
462
+ version: 1,
463
+ jobId: id,
464
+ lifecycle,
465
+ updatedAt: new Date().toISOString(),
466
+ };
467
+ await this.writeLifecycle(next);
468
+ const projected = projectState(state, lifecycle);
469
+ await writeFileAtomically(this.statePath(id), `${JSON.stringify(projected)}\n`);
470
+ return new ClaudeJobExecution(this, lease, owner.token, lifecycle.generation, id, lifecycle);
471
+ });
472
+ return execution;
473
+ }
474
+ catch (error) {
475
+ await lease.release();
476
+ throw error;
477
+ }
478
+ }
479
+ async reconcileOwnerLoss(id) {
480
+ assertJobId(id);
481
+ const initial = await this.readWithLifecycle(id);
482
+ if (!initial.lifecycle) {
483
+ const pid = initial.state.pid;
484
+ return {
485
+ owned: pid !== undefined && isProcessAlive(pid),
486
+ view: initial,
487
+ };
488
+ }
489
+ if (isTerminalLifecycleState(initial.lifecycle.state))
490
+ return { owned: false, view: initial };
491
+ const probe = await this.ownerLease(id).tryAcquire();
492
+ if (!probe) {
493
+ const reread = await this.readWithLifecycle(id);
494
+ if (!reread.lifecycle || isTerminalLifecycleState(reread.lifecycle.state))
495
+ return { owned: false, view: reread };
496
+ return { owned: true, view: reread };
497
+ }
498
+ try {
499
+ await this.withLock(id, async () => {
500
+ const latest = await this.readLifecycle(id);
501
+ if (!latest)
502
+ return false;
503
+ if (isTerminalLifecycleState(latest.lifecycle.state))
504
+ return false;
505
+ if (latest.lifecycle.generation !== initial.lifecycle?.generation ||
506
+ latest.lifecycle.owner?.token !== initial.lifecycle?.owner?.token)
507
+ return false;
508
+ const orphaned = markLifecycleOrphaned(latest.lifecycle, latest.lifecycle.owner?.token ?? '');
509
+ await this.writeLifecycle({
510
+ version: 1,
511
+ jobId: id,
512
+ lifecycle: orphaned,
513
+ updatedAt: new Date().toISOString(),
514
+ });
515
+ const state = await this.read(id);
516
+ await writeFileAtomically(this.statePath(id), `${JSON.stringify(projectState(state, orphaned))}\n`);
517
+ return true;
518
+ });
519
+ return { owned: false, view: await this.readWithLifecycle(id) };
520
+ }
521
+ finally {
522
+ await probe.release();
523
+ }
524
+ }
130
525
  async read(id) {
131
526
  assertJobId(id);
132
527
  try {
@@ -284,6 +679,87 @@ export class ClaudeJobStore {
284
679
  }
285
680
  await writeFileAtomically(this.outputPath(id), content.subarray(start).toString('utf8'));
286
681
  }
682
+ async mutateExecution(execution, operation) {
683
+ return this.withLock(this.jobIdFromExecution(execution), async () => {
684
+ const current = await this.readLifecycle(this.jobIdFromExecution(execution));
685
+ const state = await this.read(this.jobIdFromExecution(execution));
686
+ if (!current ||
687
+ current.lifecycle.generation !== execution.generation ||
688
+ current.lifecycle.owner?.token !== execution.token)
689
+ throw new Error('Lifecycle execution owner token is stale or missing');
690
+ const result = operation(current.lifecycle, state);
691
+ if (result.state.daemonShort !== this.jobIdFromExecution(execution))
692
+ throw new Error('Agent ID cannot change');
693
+ const record = {
694
+ version: 1,
695
+ jobId: this.jobIdFromExecution(execution),
696
+ lifecycle: result.lifecycle,
697
+ updatedAt: new Date().toISOString(),
698
+ };
699
+ await this.writeLifecycle(record);
700
+ const projected = projectState({ ...result.state, updatedAt: record.updatedAt }, result.lifecycle);
701
+ await writeFileAtomically(this.statePath(this.jobIdFromExecution(execution)), `${JSON.stringify(projected)}\n`);
702
+ return { lifecycle: result.lifecycle, state: projected };
703
+ });
704
+ }
705
+ async assertExecutionOwner(execution) {
706
+ await this.withLock(this.jobIdFromExecution(execution), async () => {
707
+ const current = await this.readLifecycle(this.jobIdFromExecution(execution));
708
+ if (!current ||
709
+ current.lifecycle.generation !== execution.generation ||
710
+ current.lifecycle.owner?.token !== execution.token)
711
+ throw new Error('Lifecycle execution owner token is stale or missing');
712
+ });
713
+ }
714
+ async orphanExecution(execution) {
715
+ const result = await this.mutateExecution(execution, (lifecycle, state) => {
716
+ const next = markLifecycleOrphaned(lifecycle, execution.token);
717
+ return { lifecycle: next, state };
718
+ });
719
+ return result.lifecycle;
720
+ }
721
+ jobIdFromExecution(execution) {
722
+ return execution.jobId;
723
+ }
724
+ async readLifecycle(id) {
725
+ let source;
726
+ try {
727
+ source = await readFile(this.lifecyclePath(id), 'utf8');
728
+ }
729
+ catch (error) {
730
+ if (error.code === 'ENOENT')
731
+ return null;
732
+ throw error;
733
+ }
734
+ let value;
735
+ try {
736
+ value = JSON.parse(source);
737
+ }
738
+ catch (error) {
739
+ throw new Error(`Invalid Claude job lifecycle: ${this.lifecyclePath(id)}`, {
740
+ cause: error,
741
+ });
742
+ }
743
+ if (!isRecord(value))
744
+ throw new Error(`Invalid Claude job lifecycle: ${this.lifecyclePath(id)}`);
745
+ let lifecycle;
746
+ try {
747
+ lifecycle = parseAgentLifecycleSnapshot(value.lifecycle);
748
+ }
749
+ catch {
750
+ throw new Error(`Invalid Claude job lifecycle: ${this.lifecyclePath(id)}`);
751
+ }
752
+ if (!isRecord(value) ||
753
+ value.version !== 1 ||
754
+ value.jobId !== id ||
755
+ typeof value.updatedAt !== 'string' ||
756
+ Number.isNaN(Date.parse(value.updatedAt)))
757
+ throw new Error(`Invalid Claude job lifecycle: ${this.lifecyclePath(id)}`);
758
+ return { ...value, lifecycle };
759
+ }
760
+ async writeLifecycle(record) {
761
+ await writeFileAtomically(this.lifecyclePath(record.jobId), `${JSON.stringify(record)}\n`, { mode: 0o600 });
762
+ }
287
763
  async withLock(id, operation) {
288
764
  const lease = new ExclusiveFileLease(join(this.lockRoot, 'locks', `job-${id}.lock`));
289
765
  const deadline = Date.now() + LOCK_WAIT_MS;
@@ -317,6 +793,12 @@ export class ClaudeJobStore {
317
793
  outputPath(id) {
318
794
  return join(this.jobDirectory(id), 'output.log');
319
795
  }
796
+ lifecyclePath(id) {
797
+ return join(this.lockRoot, 'agent-lifecycle', `${id}.json`);
798
+ }
799
+ ownerLease(id) {
800
+ return new ExclusiveFileLease(join(this.lockRoot, 'locks', `job-${id}.owner.lock`));
801
+ }
320
802
  }
321
803
  export function newClaudeJobIdentity() {
322
804
  const sessionId = randomUUID();
@@ -1,5 +1,7 @@
1
1
  import type { ClaudeSchemaAdapter, ClaudeTranscriptEntry } from '../compatibility/claude/schema.js';
2
2
  import { type ClaudeSessionIndex } from './claude-session-index.js';
3
+ import { classifyTranscriptAppend } from './transcript-file-append.js';
4
+ export { classifyTranscriptAppend };
3
5
  export interface TranscriptTail {
4
6
  byteLength: number;
5
7
  lastLineHash: string | null;
@@ -65,7 +67,6 @@ export interface ClaudeTranscriptStoreOptions {
65
67
  schema: ClaudeSchemaAdapter;
66
68
  writeProfile?: 'main' | 'sidechain';
67
69
  }
68
- export declare function classifyTranscriptAppend(written: Uint8Array, previousByteLength: number, encodedLine: Uint8Array): 'appended' | 'interleaved-write';
69
70
  export declare class ClaudeTranscriptStore {
70
71
  private readonly sessionFile;
71
72
  private readonly lease;
@@ -7,6 +7,8 @@ import { isClaudeDurableMetadataType } from '../compatibility/claude/session-met
7
7
  import { getClaudeContentBlocks, indexClaudeToolLinks, recoverClaudeToolResultLinks, } from '../compatibility/claude/tool-links.js';
8
8
  import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
9
9
  import { readClaudeSessionIndex, } from './claude-session-index.js';
10
+ import { classifyTranscriptAppend } from './transcript-file-append.js';
11
+ export { classifyTranscriptAppend };
10
12
  export class ClaudeTranscriptParseError extends Error {
11
13
  lineNumber;
12
14
  byteOffset;
@@ -34,16 +36,6 @@ const NON_TAIL_ENTRY_TYPES = new Set([
34
36
  'tag',
35
37
  'worktree-state',
36
38
  ]);
37
- export function classifyTranscriptAppend(written, previousByteLength, encodedLine) {
38
- const expectedEnd = previousByteLength + encodedLine.length;
39
- if (written.length === expectedEnd &&
40
- Buffer.from(written)
41
- .subarray(previousByteLength, expectedEnd)
42
- .equals(encodedLine)) {
43
- return 'appended';
44
- }
45
- return 'interleaved-write';
46
- }
47
39
  function hashLine(line) {
48
40
  return createHash('sha256').update(line).digest('hex');
49
41
  }
@@ -0,0 +1,68 @@
1
+ import { type TeamMailboxMessage, type TeamMailboxPayload } from '../core/team-mailbox.js';
2
+ export interface NativeTeamMailboxRetention {
3
+ readonly maxRecords?: number;
4
+ readonly maxBytes?: number;
5
+ }
6
+ export interface NativeTeamMailboxOptions {
7
+ readonly nativeRoot: string;
8
+ readonly projectIdentity: string;
9
+ readonly teamId: string;
10
+ readonly participants: readonly string[];
11
+ readonly retention?: NativeTeamMailboxRetention;
12
+ }
13
+ export interface NativeTeamMailboxAppendInput {
14
+ readonly messageId: string;
15
+ readonly sender: string;
16
+ readonly recipients: readonly string[];
17
+ readonly payload: TeamMailboxPayload;
18
+ readonly createdAt?: string;
19
+ }
20
+ export interface NativeTeamMailboxAppendResult {
21
+ readonly message: TeamMailboxMessage;
22
+ readonly inserted: boolean;
23
+ }
24
+ export interface NativeTeamMailboxReadProof {
25
+ readonly participant: string;
26
+ readonly expectedCursor: number;
27
+ readonly throughSequence: number;
28
+ readonly messageIds: readonly string[];
29
+ }
30
+ export interface NativeTeamMailboxReadResult {
31
+ readonly messages: readonly TeamMailboxMessage[];
32
+ readonly expectedCursor: number;
33
+ readonly throughSequence: number;
34
+ readonly messageIds: readonly string[];
35
+ readonly proof: NativeTeamMailboxReadProof;
36
+ }
37
+ export declare class NativeTeamMailbox {
38
+ private readonly options;
39
+ readonly messagesPath: string;
40
+ readonly statePath: string;
41
+ readonly mutationPath: string;
42
+ private readonly participants;
43
+ private readonly participantSet;
44
+ private readonly maxRecords;
45
+ private readonly maxBytes;
46
+ private mutation;
47
+ private constructor();
48
+ static open(options: NativeTeamMailboxOptions): NativeTeamMailbox;
49
+ append(input: NativeTeamMailboxAppendInput): Promise<NativeTeamMailboxAppendResult>;
50
+ read(participant: string, bounds?: {
51
+ maxMessages?: number;
52
+ }): Promise<NativeTeamMailboxReadResult>;
53
+ acknowledge(proof: NativeTeamMailboxReadProof): Promise<void>;
54
+ private enqueue;
55
+ private mutate;
56
+ private acquireMutation;
57
+ private load;
58
+ private truncateAndSync;
59
+ private rewriteRecords;
60
+ private ensureMode;
61
+ private loadState;
62
+ private initialState;
63
+ private writeState;
64
+ private compact;
65
+ private assertParticipant;
66
+ private assertParticipants;
67
+ }
68
+ //# sourceMappingURL=native-team-mailbox.d.ts.map