praxis-agent 0.36.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 (86) hide show
  1. package/README.md +16 -2
  2. package/dist/application/durable-follow-up.d.ts +14 -0
  3. package/dist/application/durable-follow-up.js +52 -0
  4. package/dist/application/native-session-transcript.d.ts +72 -0
  5. package/dist/application/native-session-transcript.js +351 -0
  6. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  7. package/dist/application/native-sidechain-transcript.js +156 -0
  8. package/dist/application/session-service.d.ts +15 -0
  9. package/dist/application/session-service.js +611 -69
  10. package/dist/application/subagent-service.d.ts +5 -1
  11. package/dist/application/subagent-service.js +391 -143
  12. package/dist/application/team-agent-runtime.d.ts +29 -0
  13. package/dist/application/team-agent-runtime.js +169 -0
  14. package/dist/application/team-capability.d.ts +27 -0
  15. package/dist/application/team-capability.js +41 -0
  16. package/dist/application/team-lead-operations.d.ts +38 -0
  17. package/dist/application/team-lead-operations.js +155 -0
  18. package/dist/application/team-mailbox.d.ts +54 -0
  19. package/dist/application/team-mailbox.js +196 -0
  20. package/dist/application/team-manager.d.ts +113 -0
  21. package/dist/application/team-manager.js +681 -0
  22. package/dist/application/team-workspace.d.ts +29 -0
  23. package/dist/application/team-workspace.js +197 -0
  24. package/dist/application/transcript-projection.d.ts +2 -0
  25. package/dist/application/transcript-projection.js +30 -1
  26. package/dist/cli/interactive.d.ts +7 -0
  27. package/dist/cli/interactive.js +387 -136
  28. package/dist/cli/tui/claude-style.d.ts +8 -25
  29. package/dist/cli/tui/claude-style.js +158 -160
  30. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  31. package/dist/cli/tui/decision-surface-model.js +131 -0
  32. package/dist/cli/tui/decision-surface.d.ts +7 -0
  33. package/dist/cli/tui/decision-surface.js +41 -0
  34. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  35. package/dist/cli/tui/help-surface-model.js +131 -0
  36. package/dist/cli/tui/mcp-panel.js +1 -2
  37. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  38. package/dist/cli/tui/permission-surface-model.js +360 -0
  39. package/dist/cli/tui/permission-surface.d.ts +7 -0
  40. package/dist/cli/tui/permission-surface.js +69 -0
  41. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  42. package/dist/cli/tui/presentation-environment.js +149 -0
  43. package/dist/cli/tui/tool-permission.js +7 -3
  44. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  45. package/dist/cli/tui/transcript-presentation.js +26 -6
  46. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  47. package/dist/cli/tui/transcript-viewport.js +517 -111
  48. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  49. package/dist/cli/tui/transcript-window-model.js +772 -0
  50. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  51. package/dist/cli/tui/tui-screen-model.js +210 -0
  52. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  53. package/dist/cli/tui/tui-view-model.js +1 -68
  54. package/dist/cli-runtime.d.ts +2 -0
  55. package/dist/cli-runtime.js +397 -32
  56. package/dist/compatibility/claude/transcript-codec.js +6 -0
  57. package/dist/core/permission-policy.d.ts +4 -0
  58. package/dist/core/permission-policy.js +21 -0
  59. package/dist/core/runtime.d.ts +1 -0
  60. package/dist/core/runtime.js +5 -1
  61. package/dist/core/team-mailbox.d.ts +54 -0
  62. package/dist/core/team-mailbox.js +212 -0
  63. package/dist/core/team-ownership.d.ts +84 -0
  64. package/dist/core/team-ownership.js +505 -0
  65. package/dist/core/tool-execution-scheduler.js +6 -1
  66. package/dist/core/transcript-event.d.ts +5 -1
  67. package/dist/core/transcript-event.js +12 -0
  68. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  69. package/dist/persistence/claude-transcript-store.js +2 -10
  70. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  71. package/dist/persistence/native-team-mailbox.js +542 -0
  72. package/dist/persistence/native-team-store.d.ts +34 -0
  73. package/dist/persistence/native-team-store.js +257 -0
  74. package/dist/persistence/native-transcript-store.d.ts +69 -0
  75. package/dist/persistence/native-transcript-store.js +321 -0
  76. package/dist/persistence/transcript-file-append.d.ts +2 -0
  77. package/dist/persistence/transcript-file-append.js +10 -0
  78. package/dist/tools/claude-capabilities.d.ts +4 -0
  79. package/dist/tools/claude-capabilities.js +18 -2
  80. package/dist/tools/team-lead-tools.d.ts +19 -0
  81. package/dist/tools/team-lead-tools.js +479 -0
  82. package/dist/tools/team-member-tools.d.ts +21 -0
  83. package/dist/tools/team-member-tools.js +259 -0
  84. package/package.json +1 -1
  85. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  86. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -12,6 +12,7 @@ import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/
12
12
  import { assembleContextSnapshot } from '../core/prompt-composer.js';
13
13
  import { ContextBudget } from '../core/context-budget.js';
14
14
  import { AgentRuntime, } from '../core/runtime.js';
15
+ import { composePermissionResolvers } from '../core/permission-policy.js';
15
16
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
16
17
  import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
17
18
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
@@ -23,6 +24,8 @@ import { BackgroundAgentManager, BackgroundAgentRunError, BackgroundAgentShutdow
23
24
  import { isBackgroundBashTaskId } from './background-task-id.js';
24
25
  import { createManagedWorktree, restoreManagedWorktree, } from './managed-worktree.js';
25
26
  import { createWorkflowWorktree } from './workflow-worktree.js';
27
+ import { NativeSidechainTranscript, } from './native-sidechain-transcript.js';
28
+ import { DurableFollowUpTracker, } from './durable-follow-up.js';
26
29
  const DEFAULT_MAX_DEPTH = 4;
27
30
  const DEFAULT_MAX_CALLS = 16;
28
31
  const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -128,6 +131,48 @@ async function readSidechainMetadataName(metadataFile) {
128
131
  return undefined;
129
132
  }
130
133
  }
134
+ function projectNativeSidechainContinuationMessages(messages) {
135
+ const callCounts = new Map();
136
+ const resultCounts = new Map();
137
+ for (const message of messages) {
138
+ if (message.role === 'assistant') {
139
+ for (const call of message.toolCalls ?? [])
140
+ callCounts.set(call.id, (callCounts.get(call.id) ?? 0) + 1);
141
+ }
142
+ else if (message.role === 'tool') {
143
+ resultCounts.set(message.toolCallId, (resultCounts.get(message.toolCallId) ?? 0) + 1);
144
+ }
145
+ }
146
+ for (const [callId, count] of callCounts) {
147
+ if (count > 1)
148
+ throw new Error(`Native sidechain continuation has duplicate tool ID ${callId}`);
149
+ }
150
+ for (const [callId, count] of resultCounts) {
151
+ if (count > 1)
152
+ throw new Error(`Native sidechain continuation has duplicate tool result ${callId}`);
153
+ }
154
+ const resolved = new Set([...callCounts.keys()].filter((callId) => resultCounts.get(callId) === 1));
155
+ return messages.flatMap((message) => {
156
+ if (message.role === 'assistant') {
157
+ const toolCalls = (message.toolCalls ?? []).filter((call) => resolved.has(call.id));
158
+ if (message.content.trim() === '' && toolCalls.length === 0)
159
+ return [];
160
+ return [
161
+ {
162
+ role: 'assistant',
163
+ content: message.content,
164
+ ...(message.thinkingBlocks
165
+ ? { thinkingBlocks: message.thinkingBlocks }
166
+ : {}),
167
+ ...(toolCalls.length === 0 ? {} : { toolCalls }),
168
+ },
169
+ ];
170
+ }
171
+ if (message.role === 'tool' && !resolved.has(message.toolCallId))
172
+ return [];
173
+ return [message];
174
+ });
175
+ }
131
176
  export class StructuredOutputRegistry {
132
177
  base;
133
178
  schema;
@@ -459,8 +504,19 @@ export class ClaudeSubagentExecutor {
459
504
  }
460
505
  constructor(options) {
461
506
  this.options = options;
507
+ if (options.experimentalNativeTranscriptWrites === true &&
508
+ (options.dataPlane !== 'native' || options.persistence !== 'disk')) {
509
+ throw new Error('experimental native transcript writes require native dataPlane and disk persistence');
510
+ }
511
+ if (options.experimentalNativeTranscriptWrites === true &&
512
+ options.hooks !== undefined) {
513
+ throw new Error('experimental native sidechain writes do not support Claude hooks');
514
+ }
462
515
  this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
463
516
  }
517
+ nativeSidechainWritesEnabled() {
518
+ return this.options.experimentalNativeTranscriptWrites === true;
519
+ }
464
520
  async close() {
465
521
  for (const agentId of [...this.foreground.keys()]) {
466
522
  this.backgroundForegroundTask(agentId);
@@ -763,20 +819,30 @@ export class ClaudeSubagentExecutor {
763
819
  return settleInitialSetupFailure(error, initialIsolation);
764
820
  }
765
821
  const agentCwd = initialIsolation?.cwd ?? parentCwd;
766
- const sidechain = this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
767
- const root = createClaudeSidechainRoot({
768
- sessionId,
769
- promptId,
770
- prompt: input.prompt,
771
- agentId,
772
- cwd: agentCwd,
773
- claudeVersion: this.options.claudeVersion,
774
- gitBranch: null,
775
- uuid: randomUUID(),
776
- timestamp: new Date().toISOString(),
777
- });
822
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
823
+ ? new NativeSidechainTranscript({
824
+ ...sidechainPaths,
825
+ lockFile: join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`),
826
+ })
827
+ : undefined;
828
+ const claudeSidechain = nativeSidechain
829
+ ? undefined
830
+ : this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
831
+ const root = nativeSidechain
832
+ ? undefined
833
+ : createClaudeSidechainRoot({
834
+ sessionId,
835
+ promptId,
836
+ prompt: input.prompt,
837
+ agentId,
838
+ cwd: agentCwd,
839
+ claudeVersion: this.options.claudeVersion,
840
+ gitBranch: null,
841
+ uuid: randomUUID(),
842
+ timestamp: new Date().toISOString(),
843
+ });
778
844
  try {
779
- await sidechain.create(root, {
845
+ const metadata = {
780
846
  agentType: input.subagentType,
781
847
  description: input.description,
782
848
  toolUseId: call.id,
@@ -788,7 +854,19 @@ export class ClaudeSubagentExecutor {
788
854
  ...(input.isolation ? { isolation: input.isolation } : {}),
789
855
  ...(parentAgentId ? { parentAgentId } : {}),
790
856
  ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
791
- });
857
+ };
858
+ if (nativeSidechain) {
859
+ await nativeSidechain.create(input.prompt, {
860
+ ...metadata,
861
+ cwd: agentCwd,
862
+ promptId,
863
+ });
864
+ }
865
+ else {
866
+ if (!claudeSidechain || !root)
867
+ throw new Error('Claude sidechain initialization is unavailable');
868
+ await claudeSidechain.create(root, metadata);
869
+ }
792
870
  }
793
871
  catch (error) {
794
872
  return settleInitialSetupFailure(error, initialIsolation);
@@ -803,21 +881,28 @@ export class ClaudeSubagentExecutor {
803
881
  ...(initialExecution ? { initialExecution } : {}),
804
882
  ...(initialIsolation ? { initialIsolation } : {}),
805
883
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
806
- execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
807
- lease,
808
- root,
809
- input,
810
- provider,
811
- agentId,
812
- spawnDepth,
813
- promptId,
814
- toolUseId: call.id,
815
- transcriptPath: sidechainPaths.transcriptFile,
816
- toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
817
- ...(continuation ? { continuationMessage: message } : {}),
818
- signal,
819
- cwd,
820
- })),
884
+ execute: async (cwd, message, signal, continuation) => {
885
+ const run = (lease) => this.runSidechain({
886
+ ...lease,
887
+ sessionId,
888
+ input,
889
+ provider,
890
+ agentId,
891
+ spawnDepth,
892
+ promptId,
893
+ toolUseId: call.id,
894
+ transcriptPath: sidechainPaths.transcriptFile,
895
+ toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
896
+ ...(continuation ? { continuationMessage: message } : {}),
897
+ signal,
898
+ cwd,
899
+ });
900
+ if (nativeSidechain)
901
+ return nativeSidechain.withLease((nativeLease) => run({ nativeLease }));
902
+ if (!claudeSidechain)
903
+ throw new Error('Claude sidechain execution is unavailable');
904
+ return claudeSidechain.withLease((claudeLease) => run({ claudeLease }));
905
+ },
821
906
  });
822
907
  const resolvedModel = provider.model ?? 'praxis/provider';
823
908
  const spec = {
@@ -1314,7 +1399,13 @@ export class ClaudeSubagentExecutor {
1314
1399
  metadataFile: agentFiles.metadataFile,
1315
1400
  };
1316
1401
  const sessionPaths = this.sessionPaths(options.sessionId);
1317
- const sidechain = new ClaudeSidechainStore(paths, join(sessionPaths.praxisRoot, 'locks', `${options.sessionId}-${options.runId}-${options.agentId}.lock`), this.schema);
1402
+ const lockFile = join(sessionPaths.praxisRoot, 'locks', `${options.sessionId}-${options.runId}-${options.agentId}.lock`);
1403
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
1404
+ ? new NativeSidechainTranscript({ ...paths, lockFile })
1405
+ : undefined;
1406
+ const claudeSidechain = nativeSidechain
1407
+ ? undefined
1408
+ : new ClaudeSidechainStore(paths, lockFile, this.schema);
1318
1409
  const isolation = options.isolation
1319
1410
  ? await createWorkflowWorktree({
1320
1411
  cwd: this.cwd(),
@@ -1324,29 +1415,47 @@ export class ClaudeSubagentExecutor {
1324
1415
  })
1325
1416
  : null;
1326
1417
  const agentCwd = isolation?.cwd ?? this.cwd();
1327
- const root = createClaudeSidechainRoot({
1328
- sessionId: options.sessionId,
1329
- promptId: options.promptId,
1330
- prompt: options.prompt,
1331
- agentId: options.agentId,
1332
- cwd: agentCwd,
1333
- claudeVersion: this.options.claudeVersion,
1334
- gitBranch: null,
1335
- uuid: randomUUID(),
1336
- timestamp: new Date().toISOString(),
1337
- });
1418
+ const root = nativeSidechain
1419
+ ? undefined
1420
+ : createClaudeSidechainRoot({
1421
+ sessionId: options.sessionId,
1422
+ promptId: options.promptId,
1423
+ prompt: options.prompt,
1424
+ agentId: options.agentId,
1425
+ cwd: agentCwd,
1426
+ claudeVersion: this.options.claudeVersion,
1427
+ gitBranch: null,
1428
+ uuid: randomUUID(),
1429
+ timestamp: new Date().toISOString(),
1430
+ });
1338
1431
  const startedAt = Date.now();
1339
1432
  const structured = { calls: 0, value: undefined };
1340
1433
  let run;
1341
1434
  let cleanup;
1342
1435
  try {
1343
- await sidechain.createWorkflow(root, {
1344
- agentType: options.agentType ?? 'workflow-subagent',
1345
- spawnDepth: 1,
1346
- });
1347
- run = await sidechain.withLease((lease) => this.runSidechain({
1348
- lease,
1349
- root,
1436
+ if (nativeSidechain) {
1437
+ await nativeSidechain.create(options.prompt, {
1438
+ agentType: options.agentType ?? 'workflow-subagent',
1439
+ description: input.description,
1440
+ toolUseId: `workflow:${options.runId}`,
1441
+ spawnDepth: 1,
1442
+ cwd: agentCwd,
1443
+ promptId: options.promptId,
1444
+ ...(options.isolation ? { isolation: options.isolation } : {}),
1445
+ ...(isolation ? { worktreePath: isolation.cwd } : {}),
1446
+ });
1447
+ }
1448
+ else {
1449
+ if (!claudeSidechain || !root)
1450
+ throw new Error('Claude workflow sidechain is unavailable');
1451
+ await claudeSidechain.createWorkflow(root, {
1452
+ agentType: options.agentType ?? 'workflow-subagent',
1453
+ spawnDepth: 1,
1454
+ });
1455
+ }
1456
+ const runWithLease = (lease) => this.runSidechain({
1457
+ ...lease,
1458
+ sessionId: options.sessionId,
1350
1459
  input,
1351
1460
  provider,
1352
1461
  agentId: options.agentId,
@@ -1360,7 +1469,15 @@ export class ClaudeSubagentExecutor {
1360
1469
  ...(options.effort ? { effort: options.effort } : {}),
1361
1470
  ...(options.signal ? { signal: options.signal } : {}),
1362
1471
  cwd: agentCwd,
1363
- }));
1472
+ });
1473
+ if (nativeSidechain) {
1474
+ run = await nativeSidechain.withLease((nativeLease) => runWithLease({ nativeLease }));
1475
+ }
1476
+ else {
1477
+ if (!claudeSidechain)
1478
+ throw new Error('Claude workflow sidechain is unavailable');
1479
+ run = await claudeSidechain.withLease((claudeLease) => runWithLease({ claudeLease }));
1480
+ }
1364
1481
  }
1365
1482
  finally {
1366
1483
  cleanup = await isolation?.cleanup();
@@ -1465,7 +1582,7 @@ export class ClaudeSubagentExecutor {
1465
1582
  }
1466
1583
  const agentId = String(call.input.to);
1467
1584
  const summary = typeof call.input.summary === 'string' ? call.input.summary : undefined;
1468
- const owned = this.options.sendOwnedBackgroundAgent?.(sessionId, agentId, String(call.input.message), summary, call.id);
1585
+ const owned = await this.options.sendOwnedBackgroundAgent?.(sessionId, agentId, String(call.input.message), summary, call.id);
1469
1586
  if (owned !== undefined && owned !== null) {
1470
1587
  return { content: owned, isError: false };
1471
1588
  }
@@ -1528,34 +1645,48 @@ export class ClaudeSubagentExecutor {
1528
1645
  if (!sidechainPaths)
1529
1646
  return;
1530
1647
  const agentId = sidechainPaths.agentId;
1531
- const sidechain = new ClaudeSidechainStore(sidechainPaths, join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`), this.schema);
1532
- let snapshot;
1648
+ const lockFile = join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`);
1649
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
1650
+ ? new NativeSidechainTranscript({ ...sidechainPaths, lockFile })
1651
+ : undefined;
1652
+ const claudeSidechain = nativeSidechain
1653
+ ? undefined
1654
+ : new ClaudeSidechainStore(sidechainPaths, lockFile, this.schema);
1655
+ let metadata = null;
1656
+ let root;
1657
+ let projected;
1533
1658
  try {
1534
- snapshot = await sidechain.loadReadOnly();
1659
+ if (nativeSidechain) {
1660
+ await nativeSidechain.loadReadOnly();
1661
+ metadata = await nativeSidechain.metadata();
1662
+ projected = await nativeSidechain.withLease(async (lease) => lease.activeMessages());
1663
+ }
1664
+ else {
1665
+ if (!claudeSidechain)
1666
+ throw new Error('Claude persisted sidechain is unavailable');
1667
+ const snapshot = await claudeSidechain.loadReadOnly();
1668
+ metadata = await claudeSidechain.metadata().catch((error) => {
1669
+ if (error.code === 'ENOENT')
1670
+ return null;
1671
+ throw error;
1672
+ });
1673
+ root = snapshot.entries[0];
1674
+ if (!root || root.type !== 'user') {
1675
+ throw new Error(`Background agent ${agentId} has no sidechain root`);
1676
+ }
1677
+ projected = projectClaudeModelMessages(snapshot.entries);
1678
+ }
1535
1679
  }
1536
1680
  catch (error) {
1537
1681
  if (error.code === 'ENOENT')
1538
1682
  return;
1539
1683
  throw error;
1540
1684
  }
1541
- let metadata = null;
1542
- try {
1543
- metadata = await sidechain.metadata();
1544
- }
1545
- catch (error) {
1546
- if (error.code !== 'ENOENT')
1547
- throw error;
1548
- }
1549
- const root = snapshot.entries[0];
1550
- if (!root || root.type !== 'user') {
1685
+ const rootMessage = projected[0];
1686
+ if (rootMessage?.role !== 'user' || rootMessage.content.trim() === '') {
1551
1687
  throw new Error(`Background agent ${agentId} has no sidechain root`);
1552
1688
  }
1553
- const prompt = typeof root.message === 'object' &&
1554
- root.message !== null &&
1555
- typeof root.message.content === 'string'
1556
- ? String(root.message.content)
1557
- : '';
1558
- const projected = projectClaudeModelMessages(snapshot.entries);
1689
+ const prompt = rootMessage.content;
1559
1690
  const lastAssistant = projected.at(-1);
1560
1691
  const completed = lastAssistant?.role === 'assistant' &&
1561
1692
  lastAssistant.content.trim().length > 0 &&
@@ -1621,7 +1752,9 @@ export class ClaudeSubagentExecutor {
1621
1752
  }
1622
1753
  }
1623
1754
  else {
1624
- const persistedCwd = root.cwd;
1755
+ const persistedCwd = nativeSidechain
1756
+ ? metadata.cwd
1757
+ : root?.cwd;
1625
1758
  if (typeof persistedCwd === 'string' &&
1626
1759
  persistedCwd.length > 0 &&
1627
1760
  !persistedCwd.includes('\0') &&
@@ -1656,6 +1789,9 @@ export class ClaudeSubagentExecutor {
1656
1789
  runInBackground: true,
1657
1790
  };
1658
1791
  const provider = this.options.provider;
1792
+ const recoveredPromptId = nativeSidechain
1793
+ ? metadata.promptId
1794
+ : String(root?.promptId ?? randomUUID());
1659
1795
  const backgroundRun = this.createBackgroundAgentRun({
1660
1796
  input,
1661
1797
  parentCwd: restoredCwd,
@@ -1664,21 +1800,28 @@ export class ClaudeSubagentExecutor {
1664
1800
  initialLifecycleState: lifecycle.lifecycle.state,
1665
1801
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1666
1802
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1667
- execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
1668
- lease,
1669
- root,
1670
- input,
1671
- provider,
1672
- agentId,
1673
- spawnDepth,
1674
- promptId: String(root.promptId ?? randomUUID()),
1675
- toolUseId,
1676
- transcriptPath: sidechainPaths.transcriptFile,
1677
- toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
1678
- ...(continuation ? { continuationMessage: message } : {}),
1679
- signal,
1680
- cwd,
1681
- })),
1803
+ execute: async (cwd, message, signal, continuation) => {
1804
+ const run = (lease) => this.runSidechain({
1805
+ ...lease,
1806
+ sessionId,
1807
+ input,
1808
+ provider,
1809
+ agentId,
1810
+ spawnDepth,
1811
+ promptId: recoveredPromptId,
1812
+ toolUseId,
1813
+ transcriptPath: sidechainPaths.transcriptFile,
1814
+ toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
1815
+ ...(continuation ? { continuationMessage: message } : {}),
1816
+ signal,
1817
+ cwd,
1818
+ });
1819
+ if (nativeSidechain)
1820
+ return nativeSidechain.withLease((nativeLease) => run({ nativeLease }));
1821
+ if (!claudeSidechain)
1822
+ throw new Error('Claude persisted sidechain is unavailable');
1823
+ return claudeSidechain.withLease((claudeLease) => run({ claudeLease }));
1824
+ },
1682
1825
  });
1683
1826
  const spec = {
1684
1827
  agentId,
@@ -1861,42 +2004,77 @@ export class ClaudeSubagentExecutor {
1861
2004
  async runSidechain(options) {
1862
2005
  const cwd = options.cwd ?? this.cwd();
1863
2006
  const permissions = options.input.permissionMode && options.input.permissionMode !== 'default'
1864
- ? this.options.permissionResolverForMode?.(options.input.permissionMode)
2007
+ ? (() => {
2008
+ const mode = options.input.permissionMode;
2009
+ if (!mode)
2010
+ return undefined;
2011
+ const child = this.options.permissionResolverForMode?.(mode);
2012
+ return child
2013
+ ? composePermissionResolvers(this.options.permissions, child)
2014
+ : undefined;
2015
+ })()
1865
2016
  : this.options.permissions;
1866
2017
  if (!permissions) {
1867
2018
  throw new Error('Agent permission mode overrides are unavailable');
1868
2019
  }
1869
- let snapshot = await options.lease.load();
2020
+ if ((options.claudeLease === undefined) ===
2021
+ (options.nativeLease === undefined))
2022
+ throw new Error('Exactly one sidechain transcript lease is required');
2023
+ const claudeLease = options.claudeLease;
2024
+ const nativeLease = options.nativeLease;
2025
+ let snapshot = claudeLease
2026
+ ? await claudeLease.load()
2027
+ : undefined;
2028
+ const requireClaudeSnapshot = () => {
2029
+ if (!snapshot)
2030
+ throw new Error('Claude sidechain snapshot is unavailable');
2031
+ return snapshot;
2032
+ };
2033
+ const nativeInterruption = nativeLease?.interruption();
2034
+ if (nativeInterruption?.kind === 'indeterminate-tools')
2035
+ throw new Error(`Native sidechain tool execution is indeterminate: ${nativeInterruption.callIds.join(', ')}`);
2036
+ const nativeRecoveryCalls = options.continuationMessage === undefined &&
2037
+ nativeInterruption?.kind === 'recoverable-tools'
2038
+ ? [...nativeInterruption.calls]
2039
+ : [];
1870
2040
  if (options.continuationMessage !== undefined) {
1871
2041
  // Validate ambiguous tool linkage before the append-only sidechain is
1872
2042
  // mutated. The projection is rebuilt after appending the one explicit
1873
2043
  // continuation prompt so that prompt is visible to the provider.
1874
- projectClaudeSidechainContinuationMessages(snapshot.entries);
1875
- const [entry] = translateProviderEvents([
1876
- {
1877
- type: 'user-text-block',
1878
- text: `The coordinator sent a message while you were working:\n${options.continuationMessage}\n\nAddress this before completing your current task.`,
1879
- },
1880
- ], {
1881
- sessionId: String(options.root.sessionId),
1882
- parentUuid: snapshot.tail.lastUuid,
1883
- cwd,
1884
- claudeVersion: this.options.claudeVersion,
1885
- gitBranch: null,
1886
- history: snapshot.entries,
1887
- });
1888
- if (!entry)
1889
- throw new Error('Could not translate subagent message');
1890
- const sidechainEntry = toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType);
1891
- const appendResult = await options.lease.append(snapshot.tail, sidechainEntry);
1892
- if (appendResult.status === 'conflict') {
1893
- throw new Error(`Claude sidechain append conflict: ${appendResult.reason}`);
2044
+ const continuation = `The coordinator sent a message while you were working:\n${options.continuationMessage}\n\nAddress this before completing your current task.`;
2045
+ if (nativeLease) {
2046
+ projectNativeSidechainContinuationMessages(nativeLease.activeMessages());
2047
+ await nativeLease.appendMessages({
2048
+ messages: [{ role: 'user', content: continuation }],
2049
+ });
2050
+ }
2051
+ else {
2052
+ const current = requireClaudeSnapshot();
2053
+ projectClaudeSidechainContinuationMessages(current.entries);
2054
+ const [entry] = translateProviderEvents([{ type: 'user-text-block', text: continuation }], {
2055
+ sessionId: options.sessionId,
2056
+ parentUuid: current.tail.lastUuid,
2057
+ cwd,
2058
+ claudeVersion: this.options.claudeVersion,
2059
+ gitBranch: null,
2060
+ history: current.entries,
2061
+ });
2062
+ if (!entry)
2063
+ throw new Error('Could not translate subagent message');
2064
+ const sidechainEntry = toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType);
2065
+ if (!claudeLease)
2066
+ throw new Error('Claude sidechain continuation is unavailable');
2067
+ const appendResult = await claudeLease.append(current.tail, sidechainEntry);
2068
+ if (appendResult.status === 'conflict') {
2069
+ throw new Error(`Claude sidechain append conflict: ${appendResult.reason}`);
2070
+ }
2071
+ snapshot = {
2072
+ entries: [...current.entries, sidechainEntry],
2073
+ tail: appendResult.tail,
2074
+ };
1894
2075
  }
1895
- snapshot = {
1896
- entries: [...snapshot.entries, sidechainEntry],
1897
- tail: appendResult.tail,
1898
- };
1899
2076
  }
2077
+ const durableFollowUps = new DurableFollowUpTracker();
1900
2078
  let toolUseCount = 0;
1901
2079
  let taskTokenTotal = 0;
1902
2080
  let recordedUsage = { inputTokens: 0, outputTokens: 0 };
@@ -1907,29 +2085,37 @@ export class ClaudeSubagentExecutor {
1907
2085
  let recordedApiDurationSeen = false;
1908
2086
  let lastToolName;
1909
2087
  const taskStartedAt = Date.now();
1910
- const append = async (entry) => {
1911
- const result = await options.lease.append(snapshot.tail, entry);
2088
+ const appendClaude = async (entry) => {
2089
+ if (!claudeLease)
2090
+ throw new Error('Claude sidechain append is unavailable');
2091
+ const current = requireClaudeSnapshot();
2092
+ const result = await claudeLease.append(current.tail, entry);
1912
2093
  if (result.status === 'conflict') {
1913
2094
  throw new Error(`Claude sidechain append conflict: ${result.reason}`);
1914
2095
  }
1915
- snapshot = { entries: [...snapshot.entries, entry], tail: result.tail };
2096
+ snapshot = { entries: [...current.entries, entry], tail: result.tail };
2097
+ };
2098
+ const translationContext = () => {
2099
+ const current = requireClaudeSnapshot();
2100
+ return {
2101
+ sessionId: options.sessionId,
2102
+ parentUuid: current.tail.lastUuid,
2103
+ cwd,
2104
+ claudeVersion: this.options.claudeVersion,
2105
+ gitBranch: null,
2106
+ history: current.entries,
2107
+ };
1916
2108
  };
1917
- const translationContext = () => ({
1918
- sessionId: String(options.root.sessionId),
1919
- parentUuid: snapshot.tail.lastUuid,
1920
- cwd,
1921
- claudeVersion: this.options.claudeVersion,
1922
- gitBranch: null,
1923
- history: snapshot.entries,
1924
- });
1925
2109
  const recordHookOutcome = async (outcome) => {
2110
+ if (nativeLease)
2111
+ throw new Error('Claude hooks are unavailable for native sidechains');
1926
2112
  for (const entry of createClaudeHookAttachmentEntries(outcome, translationContext())) {
1927
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2113
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1928
2114
  }
1929
2115
  };
1930
2116
  const customAgent = this.agentDefinition(options.input);
1931
2117
  const hookSession = {
1932
- session_id: String(options.root.sessionId),
2118
+ session_id: options.sessionId,
1933
2119
  transcript_path: options.transcriptPath,
1934
2120
  cwd,
1935
2121
  permission_mode: options.input.permissionMode ?? 'default',
@@ -1979,7 +2165,7 @@ export class ClaudeSubagentExecutor {
1979
2165
  recordedDurationApiWithoutRetriesMs += withoutRetries;
1980
2166
  recordedApiDurationSeen = true;
1981
2167
  };
1982
- const nestedTools = this.registry(String(options.root.sessionId), options.spawnDepth, () => options.promptId, options.agentId);
2168
+ const nestedTools = this.registry(options.sessionId, options.spawnDepth, () => options.promptId, options.agentId);
1983
2169
  const agentMcp = customAgent?.mcpServers?.length
1984
2170
  ? await this.options.mcp?.connectAgent?.({
1985
2171
  specs: customAgent.mcpServers,
@@ -2074,8 +2260,29 @@ export class ClaudeSubagentExecutor {
2074
2260
  maxToolInputBytes: 1024 * 1024,
2075
2261
  });
2076
2262
  const observer = {
2263
+ toolExecutionStarted: async (call) => {
2264
+ await nativeLease?.beginToolExecution(call.id);
2265
+ },
2077
2266
  assistantCompleted: async (message) => {
2078
2267
  commitProviderUsage();
2268
+ if (nativeLease) {
2269
+ await nativeLease.appendMessages({
2270
+ messages: [
2271
+ {
2272
+ role: 'assistant',
2273
+ content: message.content,
2274
+ ...(message.thinkingBlocks
2275
+ ? { thinkingBlocks: message.thinkingBlocks }
2276
+ : {}),
2277
+ ...(message.toolCalls?.length
2278
+ ? { toolCalls: message.toolCalls }
2279
+ : {}),
2280
+ },
2281
+ ],
2282
+ model: options.provider.model ?? 'praxis/provider',
2283
+ });
2284
+ return;
2285
+ }
2079
2286
  const [entry] = translateProviderEvents([
2080
2287
  {
2081
2288
  type: 'assistant-message',
@@ -2090,7 +2297,7 @@ export class ClaudeSubagentExecutor {
2090
2297
  ], translationContext());
2091
2298
  if (!entry)
2092
2299
  throw new Error('Could not translate subagent response');
2093
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2300
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2094
2301
  },
2095
2302
  toolCompleted: async (call, result) => {
2096
2303
  if (result.usage) {
@@ -2114,6 +2321,16 @@ export class ClaudeSubagentExecutor {
2114
2321
  nestedToolUseCount > 0) {
2115
2322
  toolUseCount += nestedToolUseCount;
2116
2323
  }
2324
+ if (nativeLease) {
2325
+ await nativeLease.appendToolCompletion({
2326
+ callId: call.id,
2327
+ result,
2328
+ ...(result.followUpUserMessages?.length
2329
+ ? { followUpUserMessages: result.followUpUserMessages }
2330
+ : {}),
2331
+ });
2332
+ return;
2333
+ }
2117
2334
  const [entry] = translateProviderEvents([
2118
2335
  {
2119
2336
  type: 'tool-result',
@@ -2135,16 +2352,19 @@ export class ClaudeSubagentExecutor {
2135
2352
  ], translationContext());
2136
2353
  if (!entry)
2137
2354
  throw new Error('Could not translate subagent tool result');
2138
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2355
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2139
2356
  },
2140
2357
  followUpUserMessagesCompleted: async (messages) => {
2141
2358
  for (const text of messages) {
2142
- const [entry] = translateProviderEvents([{ type: 'user-text-block', text }], translationContext());
2143
- if (!entry)
2144
- throw new Error('Could not translate subagent follow-up');
2145
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2359
+ if (!nativeLease) {
2360
+ const [entry] = translateProviderEvents([{ type: 'user-text-block', text }], translationContext());
2361
+ if (!entry)
2362
+ throw new Error('Could not translate subagent follow-up');
2363
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2364
+ }
2146
2365
  await this.background.acknowledge([text]);
2147
2366
  }
2367
+ await durableFollowUps.followUpUserMessagesCompleted(messages);
2148
2368
  },
2149
2369
  };
2150
2370
  this.options.eventSink?.({
@@ -2212,11 +2432,16 @@ export class ClaudeSubagentExecutor {
2212
2432
  });
2213
2433
  const contextProjection = projectContextSnapshot(assembledContext);
2214
2434
  stableSystemMessageCount = contextProjection.stableSystemSectionCount;
2435
+ const transcriptMessages = nativeLease
2436
+ ? options.continuationMessage === undefined
2437
+ ? nativeLease.activeMessages()
2438
+ : projectNativeSidechainContinuationMessages(nativeLease.activeMessages())
2439
+ : options.continuationMessage === undefined
2440
+ ? projectClaudeModelMessages(requireClaudeSnapshot().entries)
2441
+ : projectClaudeSidechainContinuationMessages(requireClaudeSnapshot().entries);
2215
2442
  const messages = [
2216
2443
  ...contextProjection.systemMessages,
2217
- ...injectFirstUserMessageContext(options.continuationMessage === undefined
2218
- ? projectClaudeModelMessages(snapshot.entries)
2219
- : projectClaudeSidechainContinuationMessages(snapshot.entries), contextProjection.firstUserMessageContext),
2444
+ ...injectFirstUserMessageContext(transcriptMessages, contextProjection.firstUserMessageContext),
2220
2445
  ...preloadedSkills,
2221
2446
  ];
2222
2447
  observedMessages = messages;
@@ -2227,9 +2452,27 @@ export class ClaudeSubagentExecutor {
2227
2452
  };
2228
2453
  const configuredEffort = typeof customAgent?.effort === 'string' ? customAgent.effort : undefined;
2229
2454
  const effectiveEffort = options.effort ?? configuredEffort;
2455
+ if (nativeRecoveryCalls.length > 0) {
2456
+ if (!nativeLease)
2457
+ throw new Error('Native sidechain recovery lease is unavailable');
2458
+ await runtime.recoverToolCalls(nativeRecoveryCalls, {
2459
+ cwd,
2460
+ toolResultDirectory: options.toolResultDirectory,
2461
+ messages: nativeLease.activeMessages(),
2462
+ observer,
2463
+ ...(this.options.approveTool
2464
+ ? { approveTool: this.options.approveTool }
2465
+ : {}),
2466
+ permissionUpdates: this.options.permissionUpdates?.() ?? [],
2467
+ ...(this.options.onPermissionUpdates
2468
+ ? { onPermissionUpdates: this.options.onPermissionUpdates }
2469
+ : {}),
2470
+ ...(options.signal ? { signal: options.signal } : {}),
2471
+ });
2472
+ }
2230
2473
  const initialMessages = await assembleMessages();
2231
2474
  const runtimeRequest = {
2232
- sessionId: String(options.root.sessionId),
2475
+ sessionId: options.sessionId,
2233
2476
  messages: initialMessages,
2234
2477
  collectMetrics: true,
2235
2478
  stableSystemMessageCount,
@@ -2249,10 +2492,17 @@ export class ClaudeSubagentExecutor {
2249
2492
  ...(this.options.onPermissionUpdates
2250
2493
  ? { onPermissionUpdates: this.options.onPermissionUpdates }
2251
2494
  : {}),
2252
- ...(scopedHooks || this.options.backgroundTaskNotifications
2495
+ ...(scopedHooks ||
2496
+ this.options.backgroundTaskNotifications ||
2497
+ this.options.durableFollowUpSource
2253
2498
  ? {
2254
2499
  onStop: async (text) => {
2255
2500
  const messages = [];
2501
+ const durableBatch = await this.options.durableFollowUpSource?.();
2502
+ if (durableBatch) {
2503
+ durableFollowUps.register(durableBatch);
2504
+ messages.push(...durableBatch.messages);
2505
+ }
2256
2506
  const outcome = await scopedHooks?.run({
2257
2507
  ...hookSession,
2258
2508
  hook_event_name: 'SubagentStop',
@@ -2269,15 +2519,13 @@ export class ClaudeSubagentExecutor {
2269
2519
  messages.push(`SubagentStop hook error: ${outcome.blockedReason}`);
2270
2520
  }
2271
2521
  }
2272
- await this.hydratePersistedTasks(String(options.root.sessionId), options.cwd ?? this.cwd());
2522
+ await this.hydratePersistedTasks(options.sessionId, options.cwd ?? this.cwd());
2273
2523
  await this.reconcileDeliveredNotifications((notification) => {
2274
2524
  const markers = backgroundAgentNotificationMarkers(notification);
2275
- return snapshot.entries.some((entry) => {
2276
- if (entry.type !== 'user')
2277
- return false;
2278
- const source = JSON.stringify(entry.message);
2279
- return markers.every((marker) => source.includes(marker));
2280
- });
2525
+ const source = JSON.stringify(nativeLease
2526
+ ? nativeLease.activeMessages()
2527
+ : requireClaudeSnapshot().entries);
2528
+ return markers.every((marker) => source.includes(marker));
2281
2529
  });
2282
2530
  const background = await this.background.notifications({
2283
2531
  waitForRunning: false,