praxis-agent 0.67.1 → 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.
@@ -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);
@@ -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;
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:dc131bb8a371077329b97b3da87c4af7c15301f2","source_dirty":false,"artifact_sha256":"sha256:ea1838f9bf68da8966f2b3dbc5a994e2719c55bc511f3b064e71fdd64cc82f7f"}
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.1",
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",