praxis-agent 0.36.0 → 0.38.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.
- package/README.md +42 -4
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +17 -0
- package/dist/application/session-service.js +612 -70
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +391 -143
- package/dist/application/team-agent-runtime.d.ts +32 -0
- package/dist/application/team-agent-runtime.js +186 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-decision-surface.d.ts +22 -0
- package/dist/application/team-lead-decision-surface.js +16 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-observability.d.ts +113 -0
- package/dist/application/team-observability.js +312 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/transcript-projection.d.ts +2 -0
- package/dist/application/transcript-projection.js +30 -1
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +783 -277
- package/dist/cli/tui/btw-surface-model.d.ts +22 -0
- package/dist/cli/tui/btw-surface-model.js +10 -0
- package/dist/cli/tui/claude-style.d.ts +58 -111
- package/dist/cli/tui/claude-style.js +252 -235
- package/dist/cli/tui/command-palette-model.d.ts +33 -0
- package/dist/cli/tui/command-palette-model.js +44 -0
- package/dist/cli/tui/config-dashboard.d.ts +3 -9
- package/dist/cli/tui/config-dashboard.js +2 -1
- package/dist/cli/tui/config-surface-model.d.ts +24 -0
- package/dist/cli/tui/config-surface-model.js +16 -0
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/diff-surface-model.d.ts +83 -0
- package/dist/cli/tui/diff-surface-model.js +142 -0
- package/dist/cli/tui/doctor-dashboard.d.ts +3 -4
- package/dist/cli/tui/doctor-dashboard.js +2 -1
- package/dist/cli/tui/doctor-surface-model.d.ts +13 -0
- package/dist/cli/tui/doctor-surface-model.js +9 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/hooks-surface-model.d.ts +18 -0
- package/dist/cli/tui/hooks-surface-model.js +11 -0
- package/dist/cli/tui/leaf-surface-model.d.ts +68 -0
- package/dist/cli/tui/leaf-surface-model.js +55 -0
- package/dist/cli/tui/list-surface-model.d.ts +27 -0
- package/dist/cli/tui/list-surface-model.js +22 -0
- package/dist/cli/tui/mcp-elicitation-surface-model.d.ts +27 -0
- package/dist/cli/tui/mcp-elicitation-surface-model.js +25 -0
- package/dist/cli/tui/mcp-elicitation.d.ts +7 -1
- package/dist/cli/tui/mcp-elicitation.js +6 -2
- package/dist/cli/tui/mcp-panel.d.ts +3 -4
- package/dist/cli/tui/mcp-panel.js +3 -3
- package/dist/cli/tui/mcp-surface-model.d.ts +11 -0
- package/dist/cli/tui/mcp-surface-model.js +4 -0
- package/dist/cli/tui/memory-surface-model.d.ts +20 -0
- package/dist/cli/tui/memory-surface-model.js +12 -0
- package/dist/cli/tui/mention-picker-model.d.ts +41 -0
- package/dist/cli/tui/mention-picker-model.js +53 -0
- package/dist/cli/tui/model-effort-surface-model.d.ts +29 -0
- package/dist/cli/tui/model-effort-surface-model.js +38 -0
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/rewind-surface-model.d.ts +53 -0
- package/dist/cli/tui/rewind-surface-model.js +54 -0
- package/dist/cli/tui/sandbox-dashboard.d.ts +3 -4
- package/dist/cli/tui/sandbox-dashboard.js +2 -1
- package/dist/cli/tui/sandbox-surface-model.d.ts +13 -0
- package/dist/cli/tui/sandbox-surface-model.js +4 -0
- package/dist/cli/tui/session-picker-model.d.ts +41 -0
- package/dist/cli/tui/session-picker-model.js +69 -0
- package/dist/cli/tui/task-panel.d.ts +3 -3
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli/tui/task-surface-model.d.ts +11 -0
- package/dist/cli/tui/task-surface-model.js +4 -0
- package/dist/cli/tui/theme-surface-model.d.ts +85 -0
- package/dist/cli/tui/theme-surface-model.js +52 -0
- package/dist/cli/tui/tool-permission.js +7 -3
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/cli-runtime.js +547 -33
- package/dist/compatibility/claude/team.d.ts +77 -0
- package/dist/compatibility/claude/team.js +219 -0
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +3 -0
- package/dist/core/runtime.js +5 -1
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/hooks/claude-file-change-watcher.d.ts +3 -0
- package/dist/hooks/claude-file-change-watcher.js +70 -8
- package/dist/hooks/claude-hook-tools.js +6 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-migration.d.ts +37 -0
- package/dist/persistence/native-transcript-migration.js +430 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/local-tools.js +10 -1
- package/dist/tools/team-lead-tools.d.ts +33 -0
- package/dist/tools/team-lead-tools.js +600 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +8 -2
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ Praxis does not use Claude subscription authentication. Claude Code
|
|
|
38
38
|
interoperability covers local sessions, configuration, permissions, memory,
|
|
39
39
|
skills, hooks, agents, plugins, and MCP data.
|
|
40
40
|
|
|
41
|
+
Claude Team compatibility is explicit and fail-closed: supported delete/send
|
|
42
|
+
wire shapes route through native Team operations, while lossy or unsupported
|
|
43
|
+
Team surfaces are rejected rather than silently simplified.
|
|
44
|
+
|
|
41
45
|
## Install
|
|
42
46
|
|
|
43
47
|
```sh
|
|
@@ -122,7 +126,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
122
126
|
for supported local terminals,
|
|
123
127
|
`Ctrl+V` text/image clipboard paste, `Ctrl+Z` shell suspension and `fg`
|
|
124
128
|
recovery, permission-gated `!` shell turns, navigable current/per-turn Git
|
|
125
|
-
diff views,
|
|
129
|
+
diff views, semantic plan/question decision panels with complete
|
|
130
|
+
screen-reader actions, semantic screen projection across selectable surfaces,
|
|
131
|
+
deterministic resize-aware URL/form elicitation rendering, and measured
|
|
132
|
+
context budgets; print mode,
|
|
126
133
|
structured JSON/JSONL, context compaction, tool loops, and bounded execution.
|
|
127
134
|
- **Built-in tools** — read, write, edit, glob, search, shell, notebook, PDF,
|
|
128
135
|
image, web, scheduled prompts, workflows, and worktrees.
|
|
@@ -152,7 +159,28 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
152
159
|
Claude-compatible main-thread agent definitions with native prompt, model,
|
|
153
160
|
tool, memory, first-turn, and resume behavior. Agent execution uses one
|
|
154
161
|
durable lifecycle vocabulary with bounded cancellation and drain,
|
|
155
|
-
continuation, notifications, and single-owner orphan recovery.
|
|
162
|
+
continuation, notifications, and single-owner orphan recovery. Experimental
|
|
163
|
+
local Teams (`PRAXIS_ENABLE_TEAMS=true`) stay absent from ordinary startup by
|
|
164
|
+
default and add durable task ownership plus one ordered mailbox with stable
|
|
165
|
+
identities, fixed broadcast recipients, durable cursors, bounded retention,
|
|
166
|
+
and bounded model-context projection. Teams are explicitly experimental and
|
|
167
|
+
opt-in: `PRAXIS_ENABLE_TEAMS=true` is required; otherwise Team code is not
|
|
168
|
+
loaded, discovered, or exposed. New Teams default to Hybrid lead control and
|
|
169
|
+
sequential execution, with optional Coordinator and Swarm policies. Commits
|
|
170
|
+
remain Lead-owned. Swarm admits only independent, dependency-ready,
|
|
171
|
+
non-conflicting tasks, within durable agent, concurrency, token, duration,
|
|
172
|
+
and shutdown-drain budgets.
|
|
173
|
+
Child permissions can only tighten the parent; concurrent asks form one FIFO
|
|
174
|
+
Lead Decision queue with provenance. Coordinator leads are restricted to
|
|
175
|
+
orchestration, and custom Team agents receive no MCP capability. The native
|
|
176
|
+
CLI also exposes `praxis team status`, `logs`, and `attach` in human or JSON
|
|
177
|
+
form; durable-local attach does not require tmux. The Claude Team adapter is
|
|
178
|
+
removable and currently limited to fixture-verified delete/send; Claude create
|
|
179
|
+
is decoded but rejected when the payload cannot represent native roster/task
|
|
180
|
+
claims,
|
|
181
|
+
shutdown, and plan-response shapes; native task, notification, context, and
|
|
182
|
+
Team resume/inbox seams are implemented, while their Claude-facing zero-skip
|
|
183
|
+
qualification remains explicitly separate.
|
|
156
184
|
- **Claude-compatible ecosystem** — shared instructions with recursive `@`
|
|
157
185
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
158
186
|
plugins, and transcript data.
|
|
@@ -240,9 +268,19 @@ npm ci
|
|
|
240
268
|
npm run check
|
|
241
269
|
```
|
|
242
270
|
|
|
243
|
-
`npm run build:native` compiles the
|
|
244
|
-
|
|
271
|
+
`npm run build:native` compiles the implemented Praxis-owned core, provider,
|
|
272
|
+
native transcript, and session profile without the Claude compatibility adapter.
|
|
273
|
+
`npm run test:native:deletion` adds an emitted-output deletion gate. These are
|
|
274
|
+
implemented profile checks, not qualification of the full native package.
|
|
275
|
+
Native transcript migration remains explicit and recoverable. The built-CLI
|
|
276
|
+
smoke gate `npm run verify:native-migration-cli` exercises all-session dry-run,
|
|
277
|
+
publication, text/JSON idempotence, rollback, and Claude-data-plane exclusion.
|
|
278
|
+
The isolated active-stream regression proof is available through
|
|
279
|
+
`npm run test:performance:active-stream`; the product p95 budget remains 50 ms.
|
|
245
280
|
`npm run check` also enforces the corresponding source dependency direction.
|
|
281
|
+
`npm run test:core-completion` runs the 56-story #402 audit and reports
|
|
282
|
+
implemented, qualified, blocked, deferred, and out-of-scope states separately;
|
|
283
|
+
it never treats missing live prerequisites as a pass.
|
|
246
284
|
|
|
247
285
|
Contributions use Conventional Commit pull-request titles and the protected
|
|
248
286
|
squash-merge workflow. Read
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DurableFollowUpBatch {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly messages: readonly string[];
|
|
4
|
+
acknowledge(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare class DurableFollowUpTracker {
|
|
7
|
+
private readonly pending;
|
|
8
|
+
register(batch: DurableFollowUpBatch): void;
|
|
9
|
+
add(batch: DurableFollowUpBatch): void;
|
|
10
|
+
track(batch: DurableFollowUpBatch): void;
|
|
11
|
+
followUpUserMessagesCompleted(messages: readonly string[]): Promise<void>;
|
|
12
|
+
size(): number;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=durable-follow-up.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function multiset(values) {
|
|
2
|
+
const result = new Map();
|
|
3
|
+
for (const value of values)
|
|
4
|
+
result.set(value, (result.get(value) ?? 0) + 1);
|
|
5
|
+
return result;
|
|
6
|
+
}
|
|
7
|
+
function containsAll(container, required) {
|
|
8
|
+
const available = multiset(container);
|
|
9
|
+
for (const [value, count] of multiset(required)) {
|
|
10
|
+
if ((available.get(value) ?? 0) < count)
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
export class DurableFollowUpTracker {
|
|
16
|
+
pending = new Map();
|
|
17
|
+
register(batch) {
|
|
18
|
+
if (!batch || typeof batch.id !== 'string' || batch.id.trim() === '')
|
|
19
|
+
throw new Error('Invalid durable follow-up batch');
|
|
20
|
+
if (!Array.isArray(batch.messages) || batch.messages.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
if (batch.messages.some((message) => typeof message !== 'string'))
|
|
23
|
+
throw new Error('Invalid durable follow-up messages');
|
|
24
|
+
const existing = this.pending.get(batch.id);
|
|
25
|
+
if (existing) {
|
|
26
|
+
if (existing.messages.length !== batch.messages.length ||
|
|
27
|
+
existing.messages.some((message, index) => message !== batch.messages[index]))
|
|
28
|
+
throw new Error(`Conflicting durable follow-up batch: ${batch.id}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.pending.set(batch.id, batch);
|
|
32
|
+
}
|
|
33
|
+
add(batch) {
|
|
34
|
+
this.register(batch);
|
|
35
|
+
}
|
|
36
|
+
track(batch) {
|
|
37
|
+
this.register(batch);
|
|
38
|
+
}
|
|
39
|
+
async followUpUserMessagesCompleted(messages) {
|
|
40
|
+
const completed = [...messages];
|
|
41
|
+
for (const [id, batch] of this.pending) {
|
|
42
|
+
if (!containsAll(completed, batch.messages))
|
|
43
|
+
continue;
|
|
44
|
+
await batch.acknowledge();
|
|
45
|
+
this.pending.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
size() {
|
|
49
|
+
return this.pending.size;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=durable-follow-up.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ModelMessage, ModelToolCall, ToolExecutionResult } from '../core/runtime.js';
|
|
2
|
+
import type { TranscriptMessagesEvent } from '../core/transcript-event.js';
|
|
3
|
+
import type { NativeTranscriptStore } from '../persistence/native-transcript-store.js';
|
|
4
|
+
export type NativeTranscriptActivation = {
|
|
5
|
+
readonly kind: 'start';
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'resume';
|
|
8
|
+
readonly atEventId?: string;
|
|
9
|
+
};
|
|
10
|
+
export interface NativeMessageAppend {
|
|
11
|
+
readonly messages: readonly ModelMessage[];
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
readonly terminalReason?: TranscriptMessagesEvent['terminalReason'];
|
|
14
|
+
}
|
|
15
|
+
export interface NativeCompactionAppend {
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
readonly trigger: 'auto' | 'manual';
|
|
18
|
+
readonly preTokens: number;
|
|
19
|
+
readonly postTokens: number;
|
|
20
|
+
readonly durationMs: number;
|
|
21
|
+
readonly preservedMessages?: readonly ModelMessage[];
|
|
22
|
+
}
|
|
23
|
+
export type NativeInterruption = {
|
|
24
|
+
readonly kind: 'complete' | 'none';
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: 'interrupted-prompt';
|
|
27
|
+
readonly prompt: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'interrupted-turn';
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'recoverable-tools';
|
|
32
|
+
readonly calls: readonly ModelToolCall[];
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'indeterminate-tools';
|
|
35
|
+
readonly callIds: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
export interface NativeSessionTranscriptLease {
|
|
38
|
+
activeMessages(): ModelMessage[];
|
|
39
|
+
interruption(): NativeInterruption;
|
|
40
|
+
beginToolExecution(callId: string): Promise<void>;
|
|
41
|
+
appendToolCompletion(input: {
|
|
42
|
+
callId: string;
|
|
43
|
+
result: ToolExecutionResult;
|
|
44
|
+
followUpUserMessages?: readonly string[];
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
appendMessages(input: NativeMessageAppend): Promise<string>;
|
|
47
|
+
appendCompaction(input: NativeCompactionAppend): Promise<{
|
|
48
|
+
boundaryId: string;
|
|
49
|
+
summaryId: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export interface NativeForkOptions {
|
|
53
|
+
readonly atEventId?: string;
|
|
54
|
+
readonly recordCount?: number;
|
|
55
|
+
readonly ensureExisting?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface NativeSessionTranscriptOptions {
|
|
58
|
+
readonly sessionId: string;
|
|
59
|
+
readonly store: NativeTranscriptStore;
|
|
60
|
+
readonly createId?: () => string;
|
|
61
|
+
readonly now?: () => string;
|
|
62
|
+
}
|
|
63
|
+
export declare class NativeSessionTranscript {
|
|
64
|
+
private readonly sessionId;
|
|
65
|
+
private readonly store;
|
|
66
|
+
private readonly createId;
|
|
67
|
+
private readonly now;
|
|
68
|
+
constructor(options: NativeSessionTranscriptOptions);
|
|
69
|
+
forkTo(target: NativeSessionTranscript, options?: NativeForkOptions): Promise<void>;
|
|
70
|
+
withLease<T>(activation: NativeTranscriptActivation, operation: (lease: NativeSessionTranscriptLease) => Promise<T>): Promise<T>;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=native-session-transcript.d.ts.map
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { activeEvents, projectActiveMessages, unresolvedActiveToolCallIds, } from './transcript-projection.js';
|
|
3
|
+
export class NativeSessionTranscript {
|
|
4
|
+
sessionId;
|
|
5
|
+
store;
|
|
6
|
+
createId;
|
|
7
|
+
now;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.sessionId = options.sessionId;
|
|
10
|
+
this.store = options.store;
|
|
11
|
+
this.createId = options.createId ?? randomUUID;
|
|
12
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
13
|
+
}
|
|
14
|
+
async forkTo(target, options = {}) {
|
|
15
|
+
if (this.sessionId === target.sessionId)
|
|
16
|
+
throw new Error('native fork source and target session IDs must differ');
|
|
17
|
+
const sourceResult = await this.store.withLease((lease) => lease.load());
|
|
18
|
+
if (sourceResult.status === 'conflict')
|
|
19
|
+
throw new Error(`native fork source lease conflict: ${sourceResult.reason}`);
|
|
20
|
+
const source = sourceResult.value;
|
|
21
|
+
if (source.records.length === 0)
|
|
22
|
+
throw new Error('native fork source session is missing or empty');
|
|
23
|
+
if (source.records.some((record) => record.event.sessionId !== this.sessionId))
|
|
24
|
+
throw new Error('native fork source sessionId does not match');
|
|
25
|
+
const recordCount = options.recordCount;
|
|
26
|
+
if (recordCount !== undefined &&
|
|
27
|
+
(!Number.isSafeInteger(recordCount) ||
|
|
28
|
+
recordCount <= 0 ||
|
|
29
|
+
recordCount > source.records.length))
|
|
30
|
+
throw new Error('native fork source recordCount is invalid');
|
|
31
|
+
const prefix = source.records.slice(0, recordCount);
|
|
32
|
+
const selected = activeEvents(prefix.map((record) => record.event), options.atEventId);
|
|
33
|
+
const terminal = prefix.at(-1)?.event;
|
|
34
|
+
if (terminal?.kind === 'context-boundary' ||
|
|
35
|
+
selected.at(-1)?.kind === 'context-boundary')
|
|
36
|
+
throw new Error('native fork source ends with an incomplete context boundary');
|
|
37
|
+
const expected = selected.map((event) => ({
|
|
38
|
+
...event,
|
|
39
|
+
sessionId: target.sessionId,
|
|
40
|
+
}));
|
|
41
|
+
const ensureExisting = options.ensureExisting === true;
|
|
42
|
+
const targetResult = await target.store.withLease(async (lease) => {
|
|
43
|
+
const reservation = await lease.reserve();
|
|
44
|
+
if (reservation.status === 'reserved') {
|
|
45
|
+
const empty = await lease.load();
|
|
46
|
+
const appended = await lease.appendMany(empty.tail, expected);
|
|
47
|
+
if (appended.status === 'conflict')
|
|
48
|
+
throw new Error(`native fork target append conflict: ${appended.reason}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!ensureExisting)
|
|
52
|
+
throw new Error('native fork target already exists');
|
|
53
|
+
const existing = await lease.load();
|
|
54
|
+
if (existing.records.length < expected.length ||
|
|
55
|
+
expected.some((event, index) => JSON.stringify(existing.records[index]?.event) !==
|
|
56
|
+
JSON.stringify(event)))
|
|
57
|
+
throw new Error('native fork target is not the expected native fork');
|
|
58
|
+
});
|
|
59
|
+
if (targetResult.status === 'conflict')
|
|
60
|
+
throw new Error(`native fork target lease conflict: ${targetResult.reason}`);
|
|
61
|
+
}
|
|
62
|
+
async withLease(activation, operation) {
|
|
63
|
+
let prepared;
|
|
64
|
+
let selectedId;
|
|
65
|
+
const result = await this.store.withLease(async (nativeLease) => {
|
|
66
|
+
if (activation.kind === 'start') {
|
|
67
|
+
const reserved = await nativeLease.reserve();
|
|
68
|
+
if (reserved.status === 'conflict')
|
|
69
|
+
throw new Error('native transcript session already exists');
|
|
70
|
+
}
|
|
71
|
+
prepared = await nativeLease.load();
|
|
72
|
+
if (activation.kind === 'resume') {
|
|
73
|
+
if (prepared.records.length === 0)
|
|
74
|
+
throw new Error('native transcript session is missing or empty');
|
|
75
|
+
if (prepared.records.some((record) => record.event.sessionId !== this.sessionId))
|
|
76
|
+
throw new Error('native transcript sessionId does not match');
|
|
77
|
+
const events = prepared.records.map((record) => record.event);
|
|
78
|
+
const selected = activeEvents(events, activation.atEventId);
|
|
79
|
+
if (selected.length === 0)
|
|
80
|
+
throw new Error('native transcript session has no active events');
|
|
81
|
+
selectedId = selected.at(-1)?.id;
|
|
82
|
+
}
|
|
83
|
+
const initial = prepared;
|
|
84
|
+
const currentId = selectedId ?? initial.records.at(-1)?.event.id ?? null;
|
|
85
|
+
const records = [...initial.records];
|
|
86
|
+
let tail = currentId === null
|
|
87
|
+
? initial.tail
|
|
88
|
+
: { ...initial.tail, branchParentId: currentId };
|
|
89
|
+
let activeId = currentId;
|
|
90
|
+
let mutationQueue = Promise.resolve();
|
|
91
|
+
const enqueue = (operation) => {
|
|
92
|
+
const result = mutationQueue.then(operation, operation);
|
|
93
|
+
mutationQueue = result.then(() => undefined, () => undefined);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
const lease = {
|
|
97
|
+
activeMessages: () => projectActiveMessages(records.map((record) => record.event), activeId ?? undefined).slice(),
|
|
98
|
+
interruption: () => {
|
|
99
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
100
|
+
const boundaryIndex = Math.max(-1, ...events
|
|
101
|
+
.map((event, index) => event.kind === 'context-boundary' ? index : -1)
|
|
102
|
+
.filter((index) => index >= 0));
|
|
103
|
+
const active = events.slice(boundaryIndex + 1);
|
|
104
|
+
const calls = new Map();
|
|
105
|
+
const results = new Set();
|
|
106
|
+
const claimed = new Set();
|
|
107
|
+
for (const event of active) {
|
|
108
|
+
if (event.kind === 'tool-execution-started') {
|
|
109
|
+
claimed.add(event.callId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (event.kind !== 'messages')
|
|
113
|
+
continue;
|
|
114
|
+
for (const message of event.messages) {
|
|
115
|
+
if (message.role === 'assistant')
|
|
116
|
+
for (const call of message.toolCalls ?? [])
|
|
117
|
+
calls.set(call.id, call);
|
|
118
|
+
else if (message.role === 'tool')
|
|
119
|
+
results.add(message.toolCallId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const unresolved = [...calls.entries()].filter(([id]) => !results.has(id));
|
|
123
|
+
const indeterminate = unresolved
|
|
124
|
+
.map(([id]) => id)
|
|
125
|
+
.filter((id) => claimed.has(id));
|
|
126
|
+
if (indeterminate.length > 0)
|
|
127
|
+
return { kind: 'indeterminate-tools', callIds: indeterminate };
|
|
128
|
+
if (unresolved.length > 0)
|
|
129
|
+
return {
|
|
130
|
+
kind: 'recoverable-tools',
|
|
131
|
+
calls: unresolved.map(([, call]) => call),
|
|
132
|
+
};
|
|
133
|
+
let lastMessages;
|
|
134
|
+
for (const event of active) {
|
|
135
|
+
if (event.kind !== 'messages')
|
|
136
|
+
continue;
|
|
137
|
+
lastMessages = event.messages;
|
|
138
|
+
}
|
|
139
|
+
const lastMessage = lastMessages?.at(-1);
|
|
140
|
+
if (lastMessage?.role === 'assistant')
|
|
141
|
+
return { kind: 'complete' };
|
|
142
|
+
if (lastMessages?.some((message) => message.role === 'tool'))
|
|
143
|
+
return { kind: 'interrupted-turn' };
|
|
144
|
+
if (lastMessage?.role === 'user')
|
|
145
|
+
return { kind: 'interrupted-prompt', prompt: lastMessage.content };
|
|
146
|
+
return { kind: 'none' };
|
|
147
|
+
},
|
|
148
|
+
beginToolExecution: (callId) => enqueue(async () => {
|
|
149
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
150
|
+
const activeMessages = projectActiveMessages(records.map((record) => record.event), activeId ?? undefined);
|
|
151
|
+
const call = activeMessages
|
|
152
|
+
.filter((message) => message.role === 'assistant')
|
|
153
|
+
.flatMap((message) => message.toolCalls ?? [])
|
|
154
|
+
.find((candidate) => candidate.id === callId);
|
|
155
|
+
if (!call)
|
|
156
|
+
throw new Error(`Unknown native tool call: ${callId}`);
|
|
157
|
+
const boundaryIndex = Math.max(-1, ...events.map((event, index) => event.kind === 'context-boundary' ? index : -1));
|
|
158
|
+
const postBoundary = events.slice(boundaryIndex + 1);
|
|
159
|
+
const resolved = new Set(projectActiveMessages(records.map((record) => record.event), activeId ?? undefined)
|
|
160
|
+
.filter((message) => message.role === 'tool')
|
|
161
|
+
.map((message) => message.toolCallId));
|
|
162
|
+
if (resolved.has(callId))
|
|
163
|
+
throw new Error(`Native tool call is already resolved: ${callId}`);
|
|
164
|
+
if (postBoundary.some((event) => event.kind === 'tool-execution-started' &&
|
|
165
|
+
event.callId === callId))
|
|
166
|
+
throw new Error(`Native tool call already claimed: ${callId}`);
|
|
167
|
+
const event = {
|
|
168
|
+
kind: 'tool-execution-started',
|
|
169
|
+
id: this.createId(),
|
|
170
|
+
parentId: activeId,
|
|
171
|
+
sessionId: this.sessionId,
|
|
172
|
+
timestamp: this.now(),
|
|
173
|
+
callId,
|
|
174
|
+
};
|
|
175
|
+
const appended = await nativeLease.append(tail, event);
|
|
176
|
+
if (appended.status === 'conflict')
|
|
177
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
178
|
+
records.push({ event });
|
|
179
|
+
tail = appended.tail;
|
|
180
|
+
activeId = event.id;
|
|
181
|
+
}),
|
|
182
|
+
appendToolCompletion: (input) => enqueue(async () => {
|
|
183
|
+
const { callId, result, followUpUserMessages = [] } = input;
|
|
184
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
185
|
+
const messages = projectActiveMessages(records.map((record) => record.event), activeId ?? undefined);
|
|
186
|
+
const call = messages
|
|
187
|
+
.filter((message) => message.role === 'assistant')
|
|
188
|
+
.flatMap((message) => message.toolCalls ?? [])
|
|
189
|
+
.find((candidate) => candidate.id === callId);
|
|
190
|
+
if (!call)
|
|
191
|
+
throw new Error(`Unknown native tool call: ${callId}`);
|
|
192
|
+
if (!events.some((event) => event.kind === 'tool-execution-started' &&
|
|
193
|
+
event.callId === callId))
|
|
194
|
+
throw new Error(`Native tool call was not claimed: ${callId}`);
|
|
195
|
+
if (messages.some((message) => message.role === 'tool' && message.toolCallId === callId))
|
|
196
|
+
throw new Error(`Native tool call is already resolved: ${callId}`);
|
|
197
|
+
const event = {
|
|
198
|
+
kind: 'messages',
|
|
199
|
+
id: this.createId(),
|
|
200
|
+
parentId: activeId,
|
|
201
|
+
sessionId: this.sessionId,
|
|
202
|
+
timestamp: this.now(),
|
|
203
|
+
messages: [
|
|
204
|
+
{
|
|
205
|
+
role: 'tool',
|
|
206
|
+
toolCallId: callId,
|
|
207
|
+
content: result.content,
|
|
208
|
+
...(result.contentBlocks?.length
|
|
209
|
+
? { contentBlocks: result.contentBlocks }
|
|
210
|
+
: {}),
|
|
211
|
+
...(result.images?.length ? { images: result.images } : {}),
|
|
212
|
+
...(result.documents?.length
|
|
213
|
+
? { documents: result.documents }
|
|
214
|
+
: {}),
|
|
215
|
+
isError: result.isError,
|
|
216
|
+
},
|
|
217
|
+
...followUpUserMessages.map((content) => ({
|
|
218
|
+
role: 'user',
|
|
219
|
+
content,
|
|
220
|
+
})),
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
const appended = await nativeLease.append(tail, event);
|
|
224
|
+
if (appended.status === 'conflict')
|
|
225
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
226
|
+
records.push({ event });
|
|
227
|
+
tail = appended.tail;
|
|
228
|
+
activeId = event.id;
|
|
229
|
+
}),
|
|
230
|
+
appendMessages: (input) => enqueue(async () => {
|
|
231
|
+
if (input.messages.length === 0)
|
|
232
|
+
throw new Error('native transcript cannot append empty messages');
|
|
233
|
+
const event = {
|
|
234
|
+
kind: 'messages',
|
|
235
|
+
id: this.createId(),
|
|
236
|
+
parentId: activeId,
|
|
237
|
+
sessionId: this.sessionId,
|
|
238
|
+
timestamp: this.now(),
|
|
239
|
+
messages: [...input.messages],
|
|
240
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
241
|
+
...(input.terminalReason === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { terminalReason: input.terminalReason }),
|
|
244
|
+
};
|
|
245
|
+
const appended = await nativeLease.append(tail, event);
|
|
246
|
+
if (appended.status === 'conflict')
|
|
247
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
248
|
+
records.push({ event });
|
|
249
|
+
tail = appended.tail;
|
|
250
|
+
activeId = event.id;
|
|
251
|
+
return event.id;
|
|
252
|
+
}),
|
|
253
|
+
appendCompaction: (input) => enqueue(async () => {
|
|
254
|
+
if (records.length === 0 || activeId === null)
|
|
255
|
+
throw new Error('Cannot compact an empty native transcript');
|
|
256
|
+
if (input.summary.trim().length === 0)
|
|
257
|
+
throw new Error('Native compact summary must not be blank');
|
|
258
|
+
for (const value of [
|
|
259
|
+
input.preTokens,
|
|
260
|
+
input.postTokens,
|
|
261
|
+
input.durationMs,
|
|
262
|
+
]) {
|
|
263
|
+
if (!Number.isFinite(value) || value < 0)
|
|
264
|
+
throw new Error('Native compact accounting values must be non-negative');
|
|
265
|
+
}
|
|
266
|
+
const events = records.map((record) => record.event);
|
|
267
|
+
if (unresolvedActiveToolCallIds(projectActiveMessages(events, activeId)).length > 0)
|
|
268
|
+
throw new Error('Cannot compact a native transcript with unresolved tool calls');
|
|
269
|
+
const boundaryId = this.createId();
|
|
270
|
+
const summaryId = this.createId();
|
|
271
|
+
const boundary = {
|
|
272
|
+
kind: 'context-boundary',
|
|
273
|
+
id: boundaryId,
|
|
274
|
+
parentId: null,
|
|
275
|
+
sessionId: this.sessionId,
|
|
276
|
+
timestamp: this.now(),
|
|
277
|
+
logicalParentId: activeId,
|
|
278
|
+
trigger: input.trigger,
|
|
279
|
+
preTokens: input.preTokens,
|
|
280
|
+
postTokens: input.postTokens,
|
|
281
|
+
durationMs: input.durationMs,
|
|
282
|
+
};
|
|
283
|
+
const summary = {
|
|
284
|
+
kind: 'context-summary',
|
|
285
|
+
id: summaryId,
|
|
286
|
+
parentId: boundaryId,
|
|
287
|
+
sessionId: this.sessionId,
|
|
288
|
+
timestamp: this.now(),
|
|
289
|
+
summary: input.summary,
|
|
290
|
+
};
|
|
291
|
+
const suffix = input.preservedMessages ?? [];
|
|
292
|
+
const replayToolCallIds = new Map();
|
|
293
|
+
for (const message of suffix) {
|
|
294
|
+
if (message.role !== 'assistant')
|
|
295
|
+
continue;
|
|
296
|
+
for (const call of message.toolCalls ?? [])
|
|
297
|
+
replayToolCallIds.set(call.id, this.createId());
|
|
298
|
+
}
|
|
299
|
+
const replaySuffix = suffix.map((message) => {
|
|
300
|
+
if (message.role === 'assistant')
|
|
301
|
+
return {
|
|
302
|
+
...message,
|
|
303
|
+
...(message.toolCalls === undefined
|
|
304
|
+
? {}
|
|
305
|
+
: {
|
|
306
|
+
toolCalls: message.toolCalls.map((call) => ({
|
|
307
|
+
...call,
|
|
308
|
+
id: replayToolCallIds.get(call.id) ?? call.id,
|
|
309
|
+
})),
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
312
|
+
if (message.role === 'tool')
|
|
313
|
+
return {
|
|
314
|
+
...message,
|
|
315
|
+
toolCallId: replayToolCallIds.get(message.toolCallId) ??
|
|
316
|
+
message.toolCallId,
|
|
317
|
+
};
|
|
318
|
+
return { ...message };
|
|
319
|
+
});
|
|
320
|
+
const suffixEvent = replaySuffix.length
|
|
321
|
+
? {
|
|
322
|
+
kind: 'messages',
|
|
323
|
+
id: this.createId(),
|
|
324
|
+
parentId: summaryId,
|
|
325
|
+
sessionId: this.sessionId,
|
|
326
|
+
timestamp: this.now(),
|
|
327
|
+
messages: replaySuffix,
|
|
328
|
+
}
|
|
329
|
+
: undefined;
|
|
330
|
+
const compactedEvents = [
|
|
331
|
+
boundary,
|
|
332
|
+
summary,
|
|
333
|
+
...(suffixEvent === undefined ? [] : [suffixEvent]),
|
|
334
|
+
];
|
|
335
|
+
const appended = await nativeLease.appendMany(tail, compactedEvents);
|
|
336
|
+
if (appended.status === 'conflict')
|
|
337
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
338
|
+
records.push({ event: boundary }, { event: summary }, ...(suffixEvent === undefined ? [] : [{ event: suffixEvent }]));
|
|
339
|
+
tail = appended.tail;
|
|
340
|
+
activeId = suffixEvent?.id ?? summaryId;
|
|
341
|
+
return { boundaryId, summaryId };
|
|
342
|
+
}),
|
|
343
|
+
};
|
|
344
|
+
return operation(lease);
|
|
345
|
+
});
|
|
346
|
+
if (result.status === 'conflict')
|
|
347
|
+
throw new Error(`native transcript lease conflict: ${result.reason}`);
|
|
348
|
+
return result.value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=native-session-transcript.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type NativeSessionTranscriptLease } from './native-session-transcript.js';
|
|
2
|
+
export type NativeSidechainPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan';
|
|
3
|
+
export interface NativeSidechainMetadata {
|
|
4
|
+
readonly agentType: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly toolUseId: string;
|
|
7
|
+
readonly spawnDepth: number;
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly promptId: string;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly permissionMode?: NativeSidechainPermissionMode;
|
|
12
|
+
readonly isolation?: 'worktree';
|
|
13
|
+
readonly parentAgentId?: string;
|
|
14
|
+
readonly worktreePath?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NativeSidechainPaths {
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly agentId: string;
|
|
19
|
+
readonly directory: string;
|
|
20
|
+
readonly transcriptFile: string;
|
|
21
|
+
readonly metadataFile: string;
|
|
22
|
+
readonly lockFile?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class NativeSidechainTranscript {
|
|
25
|
+
private readonly paths;
|
|
26
|
+
private readonly transcript;
|
|
27
|
+
private readonly store;
|
|
28
|
+
constructor(paths: NativeSidechainPaths);
|
|
29
|
+
create(prompt: string, metadata: NativeSidechainMetadata): Promise<void>;
|
|
30
|
+
withLease<T>(operation: (lease: NativeSessionTranscriptLease) => Promise<T>): Promise<T>;
|
|
31
|
+
loadReadOnly(): Promise<import("../persistence/native-transcript-store.js").NativeTranscriptRecovery>;
|
|
32
|
+
metadata(): Promise<NativeSidechainMetadata>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=native-sidechain-transcript.d.ts.map
|