praxis-agent 0.32.0 → 0.34.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 (36) hide show
  1. package/dist/application/background-agent-manager.d.ts +48 -3
  2. package/dist/application/background-agent-manager.js +249 -73
  3. package/dist/application/background-task-runtime.d.ts +20 -1
  4. package/dist/application/background-task-runtime.js +45 -6
  5. package/dist/application/managed-worktree.d.ts +8 -0
  6. package/dist/application/managed-worktree.js +54 -2
  7. package/dist/application/session-service.d.ts +38 -0
  8. package/dist/application/session-service.js +590 -70
  9. package/dist/application/subagent-service.d.ts +34 -7
  10. package/dist/application/subagent-service.js +694 -135
  11. package/dist/cli/interactive.d.ts +1 -0
  12. package/dist/cli/interactive.js +26 -1
  13. package/dist/cli/tui/task-panel.d.ts +1 -1
  14. package/dist/cli/tui/task-panel.js +2 -1
  15. package/dist/cli-runtime.d.ts +1 -0
  16. package/dist/cli-runtime.js +111 -31
  17. package/dist/compatibility/claude/history.d.ts +8 -0
  18. package/dist/compatibility/claude/history.js +54 -0
  19. package/dist/compatibility/claude/interruption.d.ts +16 -0
  20. package/dist/compatibility/claude/interruption.js +147 -0
  21. package/dist/compatibility/claude/projection.d.ts +4 -0
  22. package/dist/compatibility/claude/projection.js +82 -0
  23. package/dist/compatibility/claude/schema.js +22 -0
  24. package/dist/compatibility/claude/session-metadata.d.ts +44 -0
  25. package/dist/compatibility/claude/session-metadata.js +180 -0
  26. package/dist/compatibility/claude/sidechain.d.ts +5 -0
  27. package/dist/compatibility/claude/sidechain.js +13 -4
  28. package/dist/persistence/claude-session-index.d.ts +39 -0
  29. package/dist/persistence/claude-session-index.js +175 -0
  30. package/dist/persistence/claude-sidechain-store.js +19 -3
  31. package/dist/persistence/claude-transcript-store.d.ts +6 -2
  32. package/dist/persistence/claude-transcript-store.js +78 -3
  33. package/dist/persistence/in-memory-transcript-store.js +1 -0
  34. package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
  35. package/dist/persistence/subagent-lifecycle-store.js +299 -0
  36. package/package.json +2 -1
@@ -2,8 +2,11 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, open, readFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
  import { TextDecoder } from 'node:util';
5
+ import { isClaudeDurableLastPromptSnapshot } from '../compatibility/claude/history.js';
6
+ import { isClaudeDurableMetadataType } from '../compatibility/claude/session-metadata.js';
5
7
  import { getClaudeContentBlocks, indexClaudeToolLinks, recoverClaudeToolResultLinks, } from '../compatibility/claude/tool-links.js';
6
8
  import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
9
+ import { readClaudeSessionIndex, } from './claude-session-index.js';
7
10
  export class ClaudeTranscriptParseError extends Error {
8
11
  lineNumber;
9
12
  byteOffset;
@@ -19,6 +22,7 @@ const NON_TAIL_ENTRY_TYPES = new Set([
19
22
  'agent-color',
20
23
  'agent-name',
21
24
  'agent-setting',
25
+ 'ai-title',
22
26
  'custom-title',
23
27
  'file-history-delta',
24
28
  'file-history-snapshot',
@@ -26,6 +30,8 @@ const NON_TAIL_ENTRY_TYPES = new Set([
26
30
  'pr-link',
27
31
  'queue-operation',
28
32
  'relocated',
33
+ 'mode',
34
+ 'tag',
29
35
  'worktree-state',
30
36
  ]);
31
37
  export function classifyTranscriptAppend(written, previousByteLength, encodedLine) {
@@ -63,7 +69,9 @@ function tailsMatch(left, right) {
63
69
  left.newlineTerminated === right.newlineTerminated);
64
70
  }
65
71
  function tailLogicalUuid(tail) {
66
- return tail.branchParentUuid ?? tail.lastUuid;
72
+ return 'branchParentUuid' in tail
73
+ ? (tail.branchParentUuid ?? null)
74
+ : tail.lastUuid;
67
75
  }
68
76
  function getEntryUuid(entry) {
69
77
  if (typeof entry.uuid === 'string')
@@ -77,6 +85,10 @@ function findLogicalTailUuid(entries) {
77
85
  const entry = entries[index];
78
86
  if (!entry)
79
87
  continue;
88
+ if (entry.type === 'last-prompt' &&
89
+ isClaudeDurableLastPromptSnapshot(entries, index)) {
90
+ continue;
91
+ }
80
92
  const uuid = getEntryUuid(entry);
81
93
  if (uuid)
82
94
  return uuid;
@@ -173,7 +185,9 @@ export class ClaudeTranscriptStore {
173
185
  message: error instanceof Error ? error.message : String(error),
174
186
  };
175
187
  if (!recover) {
176
- throw new ClaudeTranscriptParseError(issue.lineNumber, issue.byteOffset, { cause: error });
188
+ throw new ClaudeTranscriptParseError(index + 1, byteOffset, {
189
+ cause: error,
190
+ });
177
191
  }
178
192
  break;
179
193
  }
@@ -266,8 +280,10 @@ export class ClaudeTranscriptStore {
266
280
  try {
267
281
  const value = await operation({
268
282
  load: () => this.load(),
283
+ loadIndex: () => readClaudeSessionIndex(this.sessionFile, this.schema),
269
284
  append: (expectedTail, entry) => this.appendUnderLease(expectedTail, entry),
270
285
  appendMany: (expectedTail, entries) => this.appendManyUnderLease(expectedTail, entries),
286
+ appendMetadataSnapshot: (expectedTail, entries) => this.appendMetadataSnapshotUnderLease(expectedTail, entries),
271
287
  });
272
288
  return { status: 'completed', value };
273
289
  }
@@ -314,7 +330,11 @@ export class ClaudeTranscriptStore {
314
330
  }
315
331
  }
316
332
  else if (entry.type === 'last-prompt') {
317
- if (entry.leafUuid !== logicalTailUuid) {
333
+ const isDurableSnapshot = history.some((candidate) => candidate.type === 'last-prompt' &&
334
+ candidate.sessionId === entry.sessionId &&
335
+ candidate.leafUuid === entry.leafUuid &&
336
+ candidate.lastPrompt === entry.lastPrompt);
337
+ if (entry.leafUuid !== logicalTailUuid && !isDurableSnapshot) {
318
338
  staleLastPromptLeaf = true;
319
339
  continue;
320
340
  }
@@ -378,6 +398,61 @@ export class ClaudeTranscriptStore {
378
398
  : { ...tail, branchParentUuid },
379
399
  };
380
400
  }
401
+ async appendMetadataSnapshotUnderLease(expectedTail, entries) {
402
+ if (entries.length === 0) {
403
+ throw new Error('Cannot append an empty Claude metadata snapshot');
404
+ }
405
+ if (entries.some((entry) => !isClaudeDurableMetadataType(entry.type))) {
406
+ throw new Error('Claude metadata snapshot contains a structural entry');
407
+ }
408
+ if (new Set(entries.map((entry) => entry.type)).size !== entries.length ||
409
+ new Set(entries.map((entry) => entry.sessionId)).size !== 1 ||
410
+ typeof entries[0]?.sessionId !== 'string') {
411
+ throw new Error('Claude metadata snapshot must contain one session record per type');
412
+ }
413
+ const current = await readClaudeSessionIndex(this.sessionFile, this.schema);
414
+ if (current.issue !== null) {
415
+ throw new Error('Cannot append metadata to a corrupt Claude transcript');
416
+ }
417
+ if (current.byteLength !== expectedTail.byteLength ||
418
+ current.tail.lastLineHash !== expectedTail.lastLineHash ||
419
+ current.newlineTerminated !== expectedTail.newlineTerminated) {
420
+ return { status: 'conflict', reason: 'tail-changed' };
421
+ }
422
+ if (!current.newlineTerminated) {
423
+ throw new Error('Claude transcript is not newline-terminated');
424
+ }
425
+ const lines = entries.map((entry) => this.schema.serializeForAppend(entry));
426
+ const encoded = Buffer.from(`${lines.join('\n')}\n`);
427
+ const sessionHandle = await open(this.sessionFile, 'a');
428
+ try {
429
+ const before = await sessionHandle.stat();
430
+ if (before.size !== expectedTail.byteLength) {
431
+ return { status: 'conflict', reason: 'tail-changed' };
432
+ }
433
+ await sessionHandle.writeFile(encoded);
434
+ await sessionHandle.sync();
435
+ const after = await sessionHandle.stat();
436
+ if (after.size !== expectedTail.byteLength + encoded.length) {
437
+ return { status: 'conflict', reason: 'interleaved-write' };
438
+ }
439
+ }
440
+ finally {
441
+ await sessionHandle.close();
442
+ }
443
+ const last = entries.at(-1);
444
+ return {
445
+ status: 'appended',
446
+ tail: {
447
+ byteLength: expectedTail.byteLength + encoded.length,
448
+ lastLineHash: hashLine(Buffer.from(lines.at(-1) ?? '')),
449
+ lastUuid: typeof last?.leafUuid === 'string'
450
+ ? last.leafUuid
451
+ : expectedTail.lastUuid,
452
+ newlineTerminated: true,
453
+ },
454
+ };
455
+ }
381
456
  serializeNewSidechain(entries) {
382
457
  const lines = [];
383
458
  const history = [];
@@ -47,6 +47,7 @@ export class InMemoryTranscriptStore {
47
47
  load: () => this.load(),
48
48
  append: (expectedTail, entry) => this.appendMany(expectedTail, [entry]),
49
49
  appendMany: (expectedTail, entries) => this.appendMany(expectedTail, entries),
50
+ appendMetadataSnapshot: (expectedTail, entries) => this.appendMany(expectedTail, entries),
50
51
  });
51
52
  return { status: 'completed', value };
52
53
  }
@@ -0,0 +1,66 @@
1
+ export type PersistedSubagentStatus = 'running' | 'completed' | 'failed' | 'killed';
2
+ export interface PersistedSubagentRunResult {
3
+ text: string;
4
+ usage: {
5
+ inputTokens: number;
6
+ outputTokens: number;
7
+ cacheReadInputTokens?: number;
8
+ cacheCreationInputTokens?: number;
9
+ webSearchRequests?: number;
10
+ contextWindow?: number;
11
+ maxOutputTokens?: number;
12
+ };
13
+ modelUsage?: Record<string, PersistedSubagentRunResult['usage']>;
14
+ durationApiMs?: number;
15
+ durationApiWithoutRetriesMs?: number;
16
+ toolUseCount: number;
17
+ durationMs: number;
18
+ isolationPath?: string;
19
+ isolationRetained?: boolean;
20
+ isolationWarning?: string;
21
+ }
22
+ export interface PersistedSubagentNotification {
23
+ id: string;
24
+ status: 'completed' | 'failed' | 'killed';
25
+ toolUseId: string;
26
+ result: PersistedSubagentRunResult | null;
27
+ error: string | null;
28
+ consumed: boolean;
29
+ accounting?: {
30
+ kind: 'detached';
31
+ model: string;
32
+ delivered: boolean;
33
+ };
34
+ }
35
+ export interface PersistedSubagentLifecycle {
36
+ version: 1;
37
+ sessionId: string;
38
+ agentId: string;
39
+ status: PersistedSubagentStatus;
40
+ updatedAt: string;
41
+ transcriptBytes?: number;
42
+ detail?: string;
43
+ result?: PersistedSubagentRunResult;
44
+ notifications?: PersistedSubagentNotification[];
45
+ }
46
+ /** Praxis-private state for a retained Claude-compatible sidechain. */
47
+ export declare class SubagentLifecycleStore {
48
+ private readonly sessionId;
49
+ private readonly agentId;
50
+ private readonly transcriptPath?;
51
+ private readonly path;
52
+ private mutation;
53
+ constructor(praxisRoot: string, sessionId: string, agentId: string, transcriptPath?: string | undefined);
54
+ read(): Promise<PersistedSubagentLifecycle | null>;
55
+ write(status: PersistedSubagentStatus, detail?: string, terminal?: {
56
+ result: PersistedSubagentRunResult;
57
+ notification?: Omit<PersistedSubagentNotification, 'result' | 'consumed'>;
58
+ }): Promise<void>;
59
+ acknowledgeNotification(id: string): Promise<void>;
60
+ prepareNotificationDetached(id: string, model: string): Promise<void>;
61
+ confirmNotificationDetached(id: string): Promise<void>;
62
+ matchesTranscript(record: PersistedSubagentLifecycle): Promise<boolean>;
63
+ private record;
64
+ private mutate;
65
+ }
66
+ //# sourceMappingURL=subagent-lifecycle-store.d.ts.map
@@ -0,0 +1,299 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isClaudeSessionId } from '../compatibility/claude/paths.js';
4
+ import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
5
+ import { writeFileAtomically } from '../platform/atomic-write.js';
6
+ const statuses = new Set([
7
+ 'running',
8
+ 'completed',
9
+ 'failed',
10
+ 'killed',
11
+ ]);
12
+ function assertIdentity(sessionId, agentId) {
13
+ if (!isClaudeSessionId(sessionId)) {
14
+ throw new Error(`Invalid subagent lifecycle session ID: ${sessionId}`);
15
+ }
16
+ if (!isClaudeAgentId(agentId)) {
17
+ throw new Error(`Invalid subagent lifecycle agent ID: ${agentId}`);
18
+ }
19
+ }
20
+ function isNonnegativeInteger(value) {
21
+ return Number.isSafeInteger(value) && Number(value) >= 0;
22
+ }
23
+ function isNonnegativeNumber(value) {
24
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
25
+ }
26
+ function isUsage(value) {
27
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
28
+ return false;
29
+ }
30
+ const usage = value;
31
+ if (!isNonnegativeInteger(usage.inputTokens) ||
32
+ !isNonnegativeInteger(usage.outputTokens)) {
33
+ return false;
34
+ }
35
+ for (const field of [
36
+ 'cacheReadInputTokens',
37
+ 'cacheCreationInputTokens',
38
+ 'webSearchRequests',
39
+ ]) {
40
+ if (usage[field] !== undefined && !isNonnegativeInteger(usage[field])) {
41
+ return false;
42
+ }
43
+ }
44
+ for (const field of ['contextWindow', 'maxOutputTokens']) {
45
+ if (usage[field] !== undefined &&
46
+ (!Number.isSafeInteger(usage[field]) || Number(usage[field]) < 1)) {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+ function isRunResult(value) {
53
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
54
+ return false;
55
+ }
56
+ const result = value;
57
+ if (typeof result.text !== 'string' ||
58
+ !isUsage(result.usage) ||
59
+ !isNonnegativeInteger(result.toolUseCount) ||
60
+ !isNonnegativeInteger(result.durationMs)) {
61
+ return false;
62
+ }
63
+ if (result.modelUsage !== undefined &&
64
+ (typeof result.modelUsage !== 'object' ||
65
+ result.modelUsage === null ||
66
+ Array.isArray(result.modelUsage) ||
67
+ !Object.values(result.modelUsage).every(isUsage))) {
68
+ return false;
69
+ }
70
+ for (const field of ['durationApiMs', 'durationApiWithoutRetriesMs']) {
71
+ if (result[field] !== undefined && !isNonnegativeNumber(result[field])) {
72
+ return false;
73
+ }
74
+ }
75
+ return ((result.isolationPath === undefined ||
76
+ typeof result.isolationPath === 'string') &&
77
+ (result.isolationRetained === undefined ||
78
+ typeof result.isolationRetained === 'boolean') &&
79
+ (result.isolationWarning === undefined ||
80
+ typeof result.isolationWarning === 'string'));
81
+ }
82
+ function isNotification(value) {
83
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
84
+ return false;
85
+ }
86
+ const notification = value;
87
+ const accounting = notification.accounting;
88
+ return (typeof notification.id === 'string' &&
89
+ /^[0-9a-f-]{36}$/iu.test(notification.id) &&
90
+ ['completed', 'failed', 'killed'].includes(String(notification.status)) &&
91
+ typeof notification.toolUseId === 'string' &&
92
+ notification.toolUseId.length > 0 &&
93
+ (notification.result === null || isRunResult(notification.result)) &&
94
+ (notification.error === null || typeof notification.error === 'string') &&
95
+ typeof notification.consumed === 'boolean' &&
96
+ (accounting === undefined ||
97
+ (typeof accounting === 'object' &&
98
+ accounting !== null &&
99
+ !Array.isArray(accounting) &&
100
+ accounting.kind === 'detached' &&
101
+ typeof accounting.model === 'string' &&
102
+ String(accounting.model).trim().length >
103
+ 0 &&
104
+ typeof accounting.delivered === 'boolean')));
105
+ }
106
+ /** Praxis-private state for a retained Claude-compatible sidechain. */
107
+ export class SubagentLifecycleStore {
108
+ sessionId;
109
+ agentId;
110
+ transcriptPath;
111
+ path;
112
+ mutation = Promise.resolve();
113
+ constructor(praxisRoot, sessionId, agentId, transcriptPath) {
114
+ this.sessionId = sessionId;
115
+ this.agentId = agentId;
116
+ this.transcriptPath = transcriptPath;
117
+ assertIdentity(sessionId, agentId);
118
+ this.path = join(praxisRoot, 'subagent-lifecycle', sessionId, `${agentId}.json`);
119
+ }
120
+ async read() {
121
+ let source;
122
+ try {
123
+ source = await readFile(this.path, 'utf8');
124
+ }
125
+ catch (error) {
126
+ if (error.code === 'ENOENT')
127
+ return null;
128
+ throw error;
129
+ }
130
+ let value;
131
+ try {
132
+ value = JSON.parse(source);
133
+ }
134
+ 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}`);
141
+ }
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}`);
158
+ }
159
+ return record;
160
+ }
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
+ });
170
+ }
171
+ return this.record({
172
+ status,
173
+ ...(detail === undefined ? {} : { detail }),
174
+ ...(terminal === undefined ? {} : { result: terminal.result }),
175
+ ...(notifications.length === 0 ? {} : { notifications }),
176
+ });
177
+ });
178
+ }
179
+ acknowledgeNotification(id) {
180
+ return this.mutate(async (current) => {
181
+ if (!current) {
182
+ throw new Error(`Missing subagent lifecycle state: ${this.path}`);
183
+ }
184
+ const notifications = current.notifications ?? [];
185
+ const matched = notifications.find((notification) => notification.id === id && !notification.consumed);
186
+ if (!matched)
187
+ 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
+ });
197
+ }
198
+ prepareNotificationDetached(id, model) {
199
+ if (model.trim().length === 0) {
200
+ throw new Error('Detached notification model must not be blank');
201
+ }
202
+ return this.mutate(async (current) => {
203
+ if (!current) {
204
+ throw new Error(`Missing subagent lifecycle state: ${this.path}`);
205
+ }
206
+ const notifications = current.notifications ?? [];
207
+ const matched = notifications.find((notification) => notification.id === id);
208
+ if (!matched || matched.accounting?.kind === 'detached')
209
+ 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
215
+ ? {
216
+ ...notification,
217
+ accounting: {
218
+ kind: 'detached',
219
+ model,
220
+ delivered: false,
221
+ },
222
+ }
223
+ : notification),
224
+ }, { transcriptBytes: current.transcriptBytes });
225
+ });
226
+ }
227
+ confirmNotificationDetached(id) {
228
+ return this.mutate(async (current) => {
229
+ if (!current) {
230
+ throw new Error(`Missing subagent lifecycle state: ${this.path}`);
231
+ }
232
+ const notifications = current.notifications ?? [];
233
+ const matched = notifications.find((notification) => notification.id === id);
234
+ if (!matched?.accounting || matched.accounting.delivered)
235
+ 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
+ });
248
+ }
249
+ async matchesTranscript(record) {
250
+ if (record.transcriptBytes === undefined ||
251
+ this.transcriptPath === undefined) {
252
+ return true;
253
+ }
254
+ try {
255
+ return (await stat(this.transcriptPath)).size === record.transcriptBytes;
256
+ }
257
+ catch (error) {
258
+ if (error.code === 'ENOENT')
259
+ return false;
260
+ throw error;
261
+ }
262
+ }
263
+ async record(value, boundary) {
264
+ let transcriptBytes = boundary?.transcriptBytes;
265
+ if (boundary === undefined && this.transcriptPath !== undefined) {
266
+ try {
267
+ transcriptBytes = (await stat(this.transcriptPath)).size;
268
+ }
269
+ catch (error) {
270
+ if (error.code !== 'ENOENT')
271
+ throw error;
272
+ }
273
+ }
274
+ return {
275
+ version: 1,
276
+ sessionId: this.sessionId,
277
+ 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
284
+ ? {}
285
+ : { notifications: value.notifications }),
286
+ };
287
+ }
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
+ });
294
+ });
295
+ this.mutation = operation.catch(() => undefined);
296
+ return operation;
297
+ }
298
+ }
299
+ //# sourceMappingURL=subagent-lifecycle-store.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -80,6 +80,7 @@
80
80
  "test:pr-link-compat": "npm run build && node scripts/verify-pr-link-compatibility.mjs",
81
81
  "test:resume-at-compat": "npm run build && node scripts/verify-resume-session-at-compatibility.mjs",
82
82
  "test:resume-selector-compat": "npm run build && node scripts/verify-resume-selector-compatibility.mjs",
83
+ "test:session-metadata-compat": "npm run build && node scripts/verify-session-metadata-compatibility.mjs",
83
84
  "test:thinking-compat": "npm run build && node scripts/verify-thinking-controls.mjs",
84
85
  "test:memory-import-compat": "npm run build && node scripts/verify-memory-import-compatibility.mjs",
85
86
  "test:tui-compat": "npm run build && node scripts/verify-tui-compatibility.mjs",