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.
- package/dist/application/session-service.d.ts +0 -1
- package/dist/application/session-service.js +10 -13
- package/dist/application/turn-lifecycle.d.ts +4 -1
- package/dist/application/turn-lifecycle.js +53 -10
- package/dist/application/turn-persistence.d.ts +8 -3
- package/dist/application/turn-persistence.js +21 -33
- package/dist/build-identity.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2810
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
package/dist/build-identity.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schema_version":"1.0","source_revision":"git:
|
|
1
|
+
{"schema_version":"1.0","source_revision":"git:cdfb04df03516521ee1f366319665ef07c41fdb7","source_dirty":false,"artifact_sha256":"sha256:506df1f26b80d8092b1748de2a1130ad9b4b9fa423776d31f911d81cd5976610"}
|