praxis-agent 0.67.0 → 0.67.2

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.
@@ -378,7 +378,6 @@ export declare class ClaudeSessionService {
378
378
  private toolCapabilities;
379
379
  private capabilityToolNames;
380
380
  private teamRegistry;
381
- private logicalTailUuid;
382
381
  }
383
382
  export {};
384
383
  //# sourceMappingURL=session-service.d.ts.map
@@ -33,7 +33,7 @@ import { lastUserPrompt, projectTranscriptDisplay, unresolvedActiveToolCallIds,
33
33
  import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
34
34
  import { NativeTranscriptStore, } from '../persistence/native-transcript-store.js';
35
35
  import { NativeSessionTranscript, } from './native-session-transcript.js';
36
- import { TurnPersistence } from './turn-persistence.js';
36
+ import { deriveTurnProjectionCursor, TurnPersistence, } from './turn-persistence.js';
37
37
  import { TurnAccounting } from './turn-accounting.js';
38
38
  import { CompactionAccounting } from './compaction-accounting.js';
39
39
  import { classifyCompactionError } from './compaction-errors.js';
@@ -901,7 +901,7 @@ export class ClaudeSessionService {
901
901
  }
902
902
  async close() {
903
903
  this.closing = true;
904
- this.turnCoordinator.close();
904
+ await this.turnCoordinator.close();
905
905
  await this.fileChangeWatcher?.close(5_000);
906
906
  await this.hookLifecycle.close();
907
907
  await this.drainDetachedHookRuns(5_000);
@@ -2590,7 +2590,7 @@ export class ClaudeSessionService {
2590
2590
  return tracker.snapshot();
2591
2591
  }
2592
2592
  async executeTurn(request) {
2593
- const { activation, submission, signal } = request;
2593
+ const { activation, submission } = request;
2594
2594
  const sessionId = activation.sessionId;
2595
2595
  const requireExisting = activation.kind === 'resume';
2596
2596
  const name = activation.name;
@@ -2604,7 +2604,8 @@ export class ClaudeSessionService {
2604
2604
  const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
2605
2605
  const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
2606
2606
  const skipUserPrompt = submission.kind === 'retry';
2607
- return this.turnCoordinator.run(request, async ({ emit, steering }) => {
2607
+ return this.turnCoordinator.run(request, async (scope) => {
2608
+ const { emit, signal, steering } = scope;
2608
2609
  this.assertTurnWritable();
2609
2610
  await this.activateSessionCostTracker(sessionId);
2610
2611
  await this.ensureFileResources(sessionId, signal);
@@ -2636,7 +2637,7 @@ export class ClaudeSessionService {
2636
2637
  const view = persistence.view();
2637
2638
  return {
2638
2639
  entries: [...view.projectionEntries],
2639
- tail: view.projectionTail,
2640
+ cursor: view.projectionCursor,
2640
2641
  };
2641
2642
  };
2642
2643
  const commitProjection = async (entries) => {
@@ -2797,7 +2798,7 @@ export class ClaudeSessionService {
2797
2798
  const flushRecoveryHookOutcomes = async () => {
2798
2799
  const entries = [];
2799
2800
  let history = projectionSnapshot().entries;
2800
- let parentUuid = this.logicalTailUuid(projectionSnapshot().tail);
2801
+ let parentUuid = projectionSnapshot().cursor.lastEntryId;
2801
2802
  for (const outcome of pendingRecoveryHookOutcomes) {
2802
2803
  const outcomeEntries = createClaudeHookAttachmentEntries(outcome, {
2803
2804
  ...this.translationContext(sessionId, projectionSnapshot()),
@@ -2806,9 +2807,8 @@ export class ClaudeSessionService {
2806
2807
  });
2807
2808
  entries.push(...outcomeEntries);
2808
2809
  history = [...history, ...outcomeEntries];
2809
- const lastEntry = outcomeEntries.at(-1);
2810
- if (typeof lastEntry?.uuid === 'string')
2811
- parentUuid = lastEntry.uuid;
2810
+ const batchCursor = deriveTurnProjectionCursor(outcomeEntries);
2811
+ parentUuid = batchCursor.lastEntryId ?? parentUuid;
2812
2812
  }
2813
2813
  if (entries.length === 0) {
2814
2814
  pendingRecoveryHookOutcomes.length = 0;
@@ -4479,7 +4479,7 @@ export class ClaudeSessionService {
4479
4479
  translationContext(sessionId, snapshot) {
4480
4480
  return {
4481
4481
  sessionId,
4482
- parentUuid: this.logicalTailUuid(snapshot.tail),
4482
+ parentUuid: snapshot.cursor.lastEntryId,
4483
4483
  cwd: this.activeCwd(),
4484
4484
  claudeVersion: this.options.claudeVersion,
4485
4485
  gitBranch: null,
@@ -5254,8 +5254,5 @@ export class ClaudeSessionService {
5254
5254
  }
5255
5255
  return factory(base, operations, sessionId, enabledTools);
5256
5256
  }
5257
- logicalTailUuid(tail) {
5258
- return tail.branchParentId ?? tail.lastEventId;
5259
- }
5260
5257
  }
5261
5258
  //# sourceMappingURL=session-service.js.map
@@ -29,6 +29,7 @@ export interface TurnRequest {
29
29
  }
30
30
  export interface TurnScope {
31
31
  readonly emit: RuntimeEventSink;
32
+ readonly signal: AbortSignal;
32
33
  readonly steering?: ActiveTurnInputPort;
33
34
  }
34
35
  export interface TurnCoordinatorOptions {
@@ -39,11 +40,13 @@ export interface TurnCoordinatorOptions {
39
40
  export declare class TurnCoordinator {
40
41
  private readonly options;
41
42
  private readonly activeTurns;
43
+ private closing;
44
+ private closePromise;
42
45
  constructor(options: TurnCoordinatorOptions);
43
46
  run<T>(request: TurnRequest, work: (scope: TurnScope) => Promise<T>): Promise<T>;
44
47
  steer(sessionId: string, content: string): ActiveTurnInputCommandResult;
45
48
  withdrawSteering(sessionId: string, id: string): ActiveTurnInputCommandResult;
46
- close(): void;
49
+ close(): Promise<void>;
47
50
  private validateRequest;
48
51
  private terminalState;
49
52
  private transition;
@@ -4,6 +4,8 @@ import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
4
4
  export class TurnCoordinator {
5
5
  options;
6
6
  activeTurns = new Map();
7
+ closing = false;
8
+ closePromise;
7
9
  constructor(options) {
8
10
  this.options = options;
9
11
  }
@@ -12,13 +14,23 @@ export class TurnCoordinator {
12
14
  const mailbox = request.submission.kind === 'shell'
13
15
  ? undefined
14
16
  : new ActiveTurnInputMailbox(this.options.createSteeringId);
17
+ let settle;
18
+ const settled = new Promise((resolve) => {
19
+ settle = resolve;
20
+ });
21
+ const controller = new AbortController();
15
22
  const record = {
16
23
  ...(mailbox ? { mailbox } : {}),
24
+ controller,
25
+ settled,
26
+ settle,
17
27
  terminal: false,
18
28
  };
19
29
  let terminalState = 'failed';
20
30
  let pendingFailure;
31
+ let callerAbort;
21
32
  const scope = {
33
+ signal: controller.signal,
22
34
  emit: (event) => {
23
35
  if (event.type === 'state' &&
24
36
  (event.state === 'completed' ||
@@ -31,19 +43,29 @@ export class TurnCoordinator {
31
43
  ...(mailbox ? { steering: mailbox } : {}),
32
44
  };
33
45
  try {
46
+ if (request.signal?.aborted)
47
+ controller.abort(request.signal.reason);
34
48
  this.validateRequest(request);
35
49
  if (this.activeTurns.has(sessionId)) {
36
50
  throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
37
51
  }
52
+ if (this.closing)
53
+ throw new Error('turn coordinator is closed');
38
54
  this.activeTurns.set(sessionId, record);
55
+ if (request.signal && !request.signal.aborted) {
56
+ callerAbort = () => controller.abort(request.signal?.reason);
57
+ request.signal.addEventListener('abort', callerAbort, { once: true });
58
+ }
39
59
  const result = await work(scope);
60
+ if (controller.signal.aborted)
61
+ throw new AgentRunCancelledError();
40
62
  terminalState = 'completed';
41
63
  this.transition(record, 'completed');
42
64
  return result;
43
65
  }
44
66
  catch (error) {
45
67
  if (!record.terminal) {
46
- terminalState = this.terminalState(error, request.signal);
68
+ terminalState = this.terminalState(error, controller.signal);
47
69
  this.transition(record, terminalState);
48
70
  }
49
71
  throw error;
@@ -55,10 +77,14 @@ export class TurnCoordinator {
55
77
  }
56
78
  }
57
79
  finally {
80
+ if (callerAbort && request.signal) {
81
+ request.signal.removeEventListener('abort', callerAbort);
82
+ }
58
83
  if (this.activeTurns.get(sessionId) === record) {
59
84
  this.activeTurns.delete(sessionId);
60
85
  }
61
86
  }
87
+ record.settle();
62
88
  if (pendingFailure) {
63
89
  // A rejected-input sink failure intentionally retains its prior precedence.
64
90
  // eslint-disable-next-line no-unsafe-finally -- compatibility is covered by the sink-error regression
@@ -86,16 +112,33 @@ export class TurnCoordinator {
86
112
  const result = active.mailbox.withdraw(id);
87
113
  return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
88
114
  }
89
- close() {
115
+ async close() {
116
+ if (this.closePromise)
117
+ return this.closePromise;
118
+ this.closing = true;
119
+ const snapshot = [...this.activeTurns.values()];
90
120
  let firstFailure;
91
- for (const active of this.activeTurns.values()) {
92
- if (!active.mailbox)
93
- continue;
94
- const failure = this.rejectPending(active.mailbox.close(), 'closed');
95
- firstFailure ??= failure;
96
- }
97
- if (firstFailure)
98
- throw firstFailure.error;
121
+ let resolveClose;
122
+ let rejectClose;
123
+ this.closePromise = new Promise((resolve, reject) => {
124
+ resolveClose = resolve;
125
+ rejectClose = reject;
126
+ });
127
+ void (async () => {
128
+ for (const active of snapshot) {
129
+ if (active.mailbox) {
130
+ const failure = this.rejectPending(active.mailbox.close(), 'closed');
131
+ firstFailure ??= failure;
132
+ }
133
+ active.controller.abort();
134
+ }
135
+ await Promise.allSettled(snapshot.map((active) => active.settled));
136
+ if (firstFailure)
137
+ rejectClose(firstFailure.error);
138
+ else
139
+ resolveClose();
140
+ })();
141
+ return this.closePromise;
99
142
  }
100
143
  validateRequest(request) {
101
144
  const { activation, submission } = request;
@@ -2,10 +2,13 @@ import type { ModelMessage } from '../core/runtime.js';
2
2
  import type { TranscriptEvent } from '../core/transcript-event.js';
3
3
  import type { NativeTranscriptEntry } from '../native/schema.js';
4
4
  import type { NativeCompactionAppend, NativeInterruption, NativeMessageAppend, NativeSessionTranscriptLease } from './native-session-transcript.js';
5
- import type { NativeTranscriptTail } from '../persistence/native-transcript-store.js';
5
+ export interface TurnProjectionCursor {
6
+ readonly lastEntryId: string | null;
7
+ readonly entryCount: number;
8
+ }
6
9
  export interface TurnPersistenceView {
7
10
  readonly projectionEntries: readonly NativeTranscriptEntry[];
8
- readonly projectionTail: NativeTranscriptTail;
11
+ readonly projectionCursor: TurnProjectionCursor;
9
12
  readonly activeEvents: readonly TranscriptEvent[];
10
13
  readonly activeMessages: readonly ModelMessage[];
11
14
  readonly interruption: NativeInterruption;
@@ -47,7 +50,7 @@ export type TurnPersistenceReceipt = {
47
50
  export declare class TurnPersistence {
48
51
  private readonly input;
49
52
  private projectionEntries;
50
- private projectionTail;
53
+ private projectionCursor;
51
54
  private commitQueue;
52
55
  constructor(input: {
53
56
  readonly native: NativeSessionTranscriptLease;
@@ -58,5 +61,7 @@ export declare class TurnPersistence {
58
61
  commit(command: TurnPersistenceCommand): Promise<TurnPersistenceReceipt>;
59
62
  private commitCommand;
60
63
  private stageProjection;
64
+ private replaceProjection;
61
65
  }
66
+ export declare const deriveTurnProjectionCursor: (entries: readonly NativeTranscriptEntry[]) => TurnProjectionCursor;
62
67
  //# sourceMappingURL=turn-persistence.d.ts.map
@@ -1,32 +1,28 @@
1
1
  import { projectNativeSessionEntries } from './native-session-projection.js';
2
- const emptyProjectionTail = () => ({
3
- byteLength: 0,
4
- lastLineHash: null,
5
- lastEventId: null,
6
- newlineTerminated: true,
7
- });
8
2
  export class TurnPersistence {
9
3
  input;
10
4
  projectionEntries;
11
- projectionTail = emptyProjectionTail();
5
+ projectionCursor;
12
6
  commitQueue = Promise.resolve();
13
7
  constructor(input) {
14
8
  this.input = input;
15
9
  this.projectionEntries = structuredClone(input.initialProjectionEntries
16
10
  ? [...input.initialProjectionEntries]
17
11
  : projectNativeSessionEntries(input.native.activeEvents()));
12
+ this.projectionCursor = deriveTurnProjectionCursor(this.projectionEntries);
18
13
  }
19
14
  view() {
20
15
  return {
21
16
  projectionEntries: structuredClone(this.projectionEntries),
22
- projectionTail: structuredClone(this.projectionTail),
17
+ projectionCursor: structuredClone(this.projectionCursor),
23
18
  activeEvents: structuredClone(this.input.native.activeEvents()),
24
19
  activeMessages: structuredClone(this.input.native.activeMessages()),
25
20
  interruption: structuredClone(this.input.native.interruption()),
26
21
  };
27
22
  }
28
23
  refresh() {
29
- this.projectionEntries = structuredClone(projectNativeSessionEntries(this.input.native.activeEvents()));
24
+ const entries = projectNativeSessionEntries(this.input.native.activeEvents());
25
+ this.replaceProjection(entries);
30
26
  return this.view();
31
27
  }
32
28
  commit(command) {
@@ -49,14 +45,10 @@ export class TurnPersistence {
49
45
  switch (command.kind) {
50
46
  case 'projection': {
51
47
  const staged = this.stageProjection(command.entries);
52
- this.projectionEntries = staged.entries;
53
- this.projectionTail = staged.tail;
54
- const lastProjectionId = [...command.entries]
55
- .reverse()
56
- .find((entry) => typeof entry.uuid === 'string')?.uuid;
48
+ this.replaceProjection(staged.entries, staged.cursor);
57
49
  return {
58
50
  kind: 'projection',
59
- lastProjectionId: typeof lastProjectionId === 'string' ? lastProjectionId : null,
51
+ lastProjectionId: staged.cursor.lastEntryId,
60
52
  };
61
53
  }
62
54
  case 'messages': {
@@ -67,8 +59,7 @@ export class TurnPersistence {
67
59
  : this.stageProjection(command.projectionEntries);
68
60
  const eventId = await this.input.native.appendMessages(command.input);
69
61
  if (staged) {
70
- this.projectionEntries = staged.entries;
71
- this.projectionTail = staged.tail;
62
+ this.replaceProjection(staged.entries, staged.cursor);
72
63
  }
73
64
  return { kind: 'messages', eventId };
74
65
  }
@@ -90,25 +81,22 @@ export class TurnPersistence {
90
81
  if (entries.length === 0)
91
82
  throw new Error('Cannot append an empty projection');
92
83
  const stagedEntries = [...this.projectionEntries, ...entries];
93
- const lastUuidValue = [...entries]
94
- .reverse()
95
- .find((entry) => typeof entry.uuid === 'string')?.uuid;
96
- const lastUuid = typeof lastUuidValue === 'string' ? lastUuidValue : undefined;
97
- const byteLength = this.projectionTail.byteLength + entries.length;
98
84
  return {
99
85
  entries: stagedEntries,
100
- tail: {
101
- ...this.projectionTail,
102
- byteLength,
103
- lastLineHash: `projection:${byteLength}`,
104
- lastEventId: typeof lastUuid === 'string'
105
- ? lastUuid
106
- : this.projectionTail.lastEventId,
107
- ...(this.projectionTail.branchParentId === undefined
108
- ? {}
109
- : { branchParentId: null }),
110
- },
86
+ cursor: deriveTurnProjectionCursor(stagedEntries),
111
87
  };
112
88
  }
89
+ replaceProjection(entries, cursor = deriveTurnProjectionCursor(entries)) {
90
+ this.projectionEntries = structuredClone([...entries]);
91
+ this.projectionCursor = structuredClone(cursor);
92
+ }
113
93
  }
94
+ export const deriveTurnProjectionCursor = (entries) => {
95
+ let lastEntryId = null;
96
+ for (const entry of entries) {
97
+ if (typeof entry.uuid === 'string' && entry.uuid.length > 0)
98
+ lastEntryId = entry.uuid;
99
+ }
100
+ return { lastEntryId, entryCount: entries.length };
101
+ };
114
102
  //# sourceMappingURL=turn-persistence.js.map
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:044ea290901a55dd5e343e78bdcd6a63d0455a00","source_dirty":false,"artifact_sha256":"sha256:272099586a6fe68b899e7ec80e182b404afe590341a24cb90196eebe90d3ce29"}
1
+ {"schema_version":"1.0","source_revision":"git:cdfb04df03516521ee1f366319665ef07c41fdb7","source_dirty":false,"artifact_sha256":"sha256:506df1f26b80d8092b1748de2a1130ad9b4b9fa423776d31f911d81cd5976610"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.67.0",
3
+ "version": "0.67.2",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",