@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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 +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
|
@@ -3,17 +3,28 @@
|
|
|
3
3
|
* consumer, and consumers can attach again with their last seen sequence.
|
|
4
4
|
*/
|
|
5
5
|
export class ReplayEventLog {
|
|
6
|
-
|
|
6
|
+
retention;
|
|
7
|
+
entries = new Map();
|
|
8
|
+
nextIndex = 0;
|
|
9
|
+
firstIndex = 0;
|
|
10
|
+
readers = new Map();
|
|
7
11
|
signal = deferredSignal();
|
|
8
12
|
closed = false;
|
|
9
13
|
failure;
|
|
14
|
+
constructor(retention = Infinity) {
|
|
15
|
+
this.retention = retention;
|
|
16
|
+
if (retention !== Infinity && (!Number.isSafeInteger(retention) || retention < 1))
|
|
17
|
+
throw new Error('Invalid event retention');
|
|
18
|
+
}
|
|
10
19
|
get length() {
|
|
11
|
-
return this.
|
|
20
|
+
return this.nextIndex;
|
|
12
21
|
}
|
|
22
|
+
get retainedCount() { return this.entries.size; }
|
|
13
23
|
append(value) {
|
|
14
24
|
if (this.closed)
|
|
15
25
|
return;
|
|
16
|
-
this.entries.
|
|
26
|
+
this.entries.set(this.nextIndex++, value);
|
|
27
|
+
this.prune();
|
|
17
28
|
this.wake();
|
|
18
29
|
}
|
|
19
30
|
close(error) {
|
|
@@ -25,23 +36,62 @@ export class ReplayEventLog {
|
|
|
25
36
|
}
|
|
26
37
|
iterate(options = {}) {
|
|
27
38
|
const start = Math.max(0, options.from ?? 0);
|
|
28
|
-
|
|
39
|
+
const reader = {};
|
|
40
|
+
const cancelled = new AbortController();
|
|
41
|
+
const signal = options.signal ? AbortSignal.any([options.signal, cancelled.signal]) : cancelled.signal;
|
|
42
|
+
const release = () => { this.readers.delete(reader); this.prune(); };
|
|
43
|
+
// Reserve the cursor when subscribing, before a synchronous producer can
|
|
44
|
+
// prune events and before the consumer calls next(). close/return also
|
|
45
|
+
// release subscriptions which were never advanced.
|
|
46
|
+
if (!signal.aborted)
|
|
47
|
+
this.readers.set(reader, start);
|
|
48
|
+
signal.addEventListener('abort', release, { once: true });
|
|
49
|
+
const cleanup = () => { signal.removeEventListener('abort', release); release(); };
|
|
50
|
+
const iterator = this.readFrom(start, reader, signal, cleanup);
|
|
51
|
+
const finish = iterator.return.bind(iterator);
|
|
52
|
+
const fail = iterator.throw.bind(iterator);
|
|
53
|
+
iterator.return = async (value) => { cancelled.abort(); cleanup(); return finish(value); };
|
|
54
|
+
iterator.throw = async (error) => { cancelled.abort(); cleanup(); return fail(error); };
|
|
55
|
+
return iterator;
|
|
29
56
|
}
|
|
30
|
-
async *readFrom(start, abortSignal) {
|
|
57
|
+
async *readFrom(start, reader, abortSignal, cleanup) {
|
|
31
58
|
let index = start;
|
|
32
|
-
|
|
33
|
-
while (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
try {
|
|
60
|
+
while (true) {
|
|
61
|
+
while (index < this.nextIndex) {
|
|
62
|
+
if (abortSignal.aborted)
|
|
63
|
+
return;
|
|
64
|
+
if (index < this.firstIndex) {
|
|
65
|
+
throw new Error('Replay cursor expired; restore persisted history and subscribe from the latest sequence.');
|
|
66
|
+
}
|
|
67
|
+
const value = this.entries.get(index);
|
|
68
|
+
this.readers.set(reader, ++index);
|
|
69
|
+
this.prune();
|
|
70
|
+
yield value;
|
|
71
|
+
}
|
|
72
|
+
if (this.closed) {
|
|
73
|
+
if (this.failure !== undefined)
|
|
74
|
+
throw this.failure;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (abortSignal.aborted)
|
|
78
|
+
return;
|
|
79
|
+
const currentSignal = this.signal.promise;
|
|
80
|
+
await waitForSignal(currentSignal, abortSignal);
|
|
39
81
|
}
|
|
40
|
-
if (abortSignal?.aborted)
|
|
41
|
-
return;
|
|
42
|
-
const currentSignal = this.signal.promise;
|
|
43
|
-
await waitForSignal(currentSignal, abortSignal);
|
|
44
82
|
}
|
|
83
|
+
finally {
|
|
84
|
+
cleanup();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
prune() {
|
|
88
|
+
if (this.retention === Infinity)
|
|
89
|
+
return;
|
|
90
|
+
let before = this.nextIndex - this.retention;
|
|
91
|
+
for (const index of this.readers.values())
|
|
92
|
+
before = Math.min(before, index);
|
|
93
|
+
while (this.firstIndex < before)
|
|
94
|
+
this.entries.delete(this.firstIndex++);
|
|
45
95
|
}
|
|
46
96
|
wake() {
|
|
47
97
|
const previous = this.signal;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionManager } from "./session.js";
|
|
2
|
+
/** The revision of a host's resident history, not the manager's refreshed log.
|
|
3
|
+
* Reads and audit/metadata refreshes never legitimize a foreign conversation.
|
|
4
|
+
* Local clear/rewind/manual compaction replace history explicitly; other local
|
|
5
|
+
* writes advance the fence only when they extend the host's existing snapshot.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SessionContextFence {
|
|
8
|
+
private readonly session;
|
|
9
|
+
private revision;
|
|
10
|
+
private readonly unsubscribe;
|
|
11
|
+
constructor(session: SessionManager);
|
|
12
|
+
getRevision(): string;
|
|
13
|
+
/** Call only alongside replacing the host's resident messages. */
|
|
14
|
+
reloadHistory(): import("./types.js").Message[];
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The revision of a host's resident history, not the manager's refreshed log.
|
|
2
|
+
* Reads and audit/metadata refreshes never legitimize a foreign conversation.
|
|
3
|
+
* Local clear/rewind/manual compaction replace history explicitly; other local
|
|
4
|
+
* writes advance the fence only when they extend the host's existing snapshot.
|
|
5
|
+
*/
|
|
6
|
+
export class SessionContextFence {
|
|
7
|
+
session;
|
|
8
|
+
revision;
|
|
9
|
+
unsubscribe;
|
|
10
|
+
constructor(session) {
|
|
11
|
+
this.session = session;
|
|
12
|
+
this.revision = session.getRevision();
|
|
13
|
+
this.unsubscribe = session.subscribeContextCommits((previous, revision, replacement) => {
|
|
14
|
+
if (replacement || previous === this.revision)
|
|
15
|
+
this.revision = revision;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
getRevision() { return this.revision; }
|
|
19
|
+
/** Call only alongside replacing the host's resident messages. */
|
|
20
|
+
reloadHistory() {
|
|
21
|
+
const messages = this.session.getMessages();
|
|
22
|
+
this.revision = this.session.getRevision();
|
|
23
|
+
return messages;
|
|
24
|
+
}
|
|
25
|
+
dispose() { this.unsubscribe(); }
|
|
26
|
+
}
|
package/dist/session-log.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { Message } from "./types.js";
|
|
2
|
-
import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionProviderErrorEntry
|
|
2
|
+
import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionProviderErrorEntry } from "./session-types.js";
|
|
3
3
|
import type { SanitizedProviderError } from "./provider-error-record.js";
|
|
4
|
+
/** Records that change conversational context or its execution state.
|
|
5
|
+
* Keep this shared by revision hashing and checkpoint receipt supersession.
|
|
6
|
+
* Unknown/new markers are conservative: only known diagnostics are excluded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function affectsContextRevision(entry: SessionLogEntry): boolean;
|
|
4
9
|
export declare class SessionLog {
|
|
5
10
|
private entries;
|
|
11
|
+
private nextId;
|
|
12
|
+
private contextRevision;
|
|
13
|
+
private allocateId;
|
|
14
|
+
appendEntries(entries: SessionLogEntry[]): void;
|
|
15
|
+
getRevision(): string;
|
|
6
16
|
load(lines: string[]): void;
|
|
7
17
|
replace(entries: SessionLogEntry[]): void;
|
|
8
18
|
list(): SessionLogEntry[];
|
|
9
19
|
getMetadata(): SessionMetadata;
|
|
10
20
|
setMetadata(metadata: SessionMetadata): SessionLogEntry[];
|
|
11
21
|
appendMessage(message: Message): SessionLogEntry[];
|
|
12
|
-
appendSummary(summary: string): SessionSummaryEntry;
|
|
13
22
|
appendMarker(kind: SessionMarkerKind, value: string): SessionLogEntry;
|
|
14
23
|
appendProviderError(error: SanitizedProviderError): SessionProviderErrorEntry;
|
|
15
24
|
toMessages(): Message[];
|
package/dist/session-log.js
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
|
|
3
|
+
import { tryCheckpointMessages } from "./context/checkpoint.js";
|
|
4
|
+
import { isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
5
|
+
/** Records that change conversational context or its execution state.
|
|
6
|
+
* Keep this shared by revision hashing and checkpoint receipt supersession.
|
|
7
|
+
* Unknown/new markers are conservative: only known diagnostics are excluded.
|
|
8
|
+
*/
|
|
9
|
+
export function affectsContextRevision(entry) {
|
|
10
|
+
if (entry.type === "metadata" || entry.type === "provider_error")
|
|
11
|
+
return false;
|
|
12
|
+
if (entry.type === "marker") {
|
|
13
|
+
return !["task_started", "task_finished", "task_killed"].includes(entry.kind);
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
2
17
|
export class SessionLog {
|
|
3
18
|
entries = [];
|
|
19
|
+
nextId = 1;
|
|
20
|
+
contextRevision = "empty";
|
|
21
|
+
allocateId() { return String(this.nextId++); }
|
|
22
|
+
appendEntries(entries) {
|
|
23
|
+
this.entries.push(...entries);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
this.nextId = Math.max(this.nextId, (parseInt(entry.id, 10) || 0) + 1);
|
|
26
|
+
if (affectsContextRevision(entry)) {
|
|
27
|
+
this.contextRevision = createHash("sha256").update(this.contextRevision).update(JSON.stringify(entry)).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
getRevision() { return this.contextRevision; }
|
|
4
32
|
load(lines) {
|
|
5
33
|
this.entries = [];
|
|
34
|
+
this.nextId = 1;
|
|
35
|
+
this.contextRevision = "empty";
|
|
6
36
|
for (const line of lines) {
|
|
7
37
|
try {
|
|
8
38
|
const raw = JSON.parse(line);
|
|
9
|
-
this.
|
|
39
|
+
this.appendEntries(normalizeEntry(raw));
|
|
10
40
|
}
|
|
11
41
|
catch {
|
|
12
42
|
// skip corrupt lines
|
|
@@ -14,13 +44,23 @@ export class SessionLog {
|
|
|
14
44
|
}
|
|
15
45
|
}
|
|
16
46
|
replace(entries) {
|
|
17
|
-
this.entries =
|
|
47
|
+
this.entries = [];
|
|
48
|
+
this.nextId = 1;
|
|
49
|
+
this.contextRevision = "empty";
|
|
50
|
+
this.appendEntries(entries);
|
|
18
51
|
}
|
|
19
52
|
list() {
|
|
20
53
|
return [...this.entries];
|
|
21
54
|
}
|
|
22
55
|
getMetadata() {
|
|
23
|
-
|
|
56
|
+
let entry;
|
|
57
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
58
|
+
const item = this.entries[i];
|
|
59
|
+
if (item.type === "metadata") {
|
|
60
|
+
entry = item;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
24
64
|
const metadata = entry?.metadata ?? {};
|
|
25
65
|
return {
|
|
26
66
|
...metadata,
|
|
@@ -46,50 +86,49 @@ export class SessionLog {
|
|
|
46
86
|
return next;
|
|
47
87
|
}
|
|
48
88
|
appendMessage(message) {
|
|
49
|
-
const normalized = normalizeMessageToEntries(message,
|
|
50
|
-
this.
|
|
89
|
+
const normalized = normalizeMessageToEntries(message, this.allocateId(), Date.now());
|
|
90
|
+
this.appendEntries(normalized);
|
|
51
91
|
return normalized;
|
|
52
92
|
}
|
|
53
|
-
appendSummary(summary) {
|
|
54
|
-
const entry = {
|
|
55
|
-
id: nextEntryId(this.entries),
|
|
56
|
-
type: "summary",
|
|
57
|
-
summary,
|
|
58
|
-
timestamp: Date.now(),
|
|
59
|
-
};
|
|
60
|
-
this.entries.push(entry);
|
|
61
|
-
return entry;
|
|
62
|
-
}
|
|
63
93
|
appendMarker(kind, value) {
|
|
64
94
|
const entry = {
|
|
65
|
-
id:
|
|
95
|
+
id: this.allocateId(),
|
|
66
96
|
type: "marker",
|
|
67
97
|
kind,
|
|
68
98
|
value,
|
|
69
99
|
timestamp: Date.now(),
|
|
70
100
|
};
|
|
71
|
-
this.
|
|
101
|
+
this.appendEntries([entry]);
|
|
72
102
|
return entry;
|
|
73
103
|
}
|
|
74
104
|
appendProviderError(error) {
|
|
75
105
|
const entry = {
|
|
76
|
-
id:
|
|
106
|
+
id: this.allocateId(),
|
|
77
107
|
type: "provider_error",
|
|
78
108
|
error,
|
|
79
109
|
timestamp: Date.now(),
|
|
80
110
|
};
|
|
81
|
-
this.
|
|
111
|
+
this.appendEntries([entry]);
|
|
82
112
|
return entry;
|
|
83
113
|
}
|
|
84
114
|
toMessages() {
|
|
85
115
|
const messages = [];
|
|
86
116
|
let latestSummaryIndex = -1;
|
|
87
117
|
let latestClearIndex = -1;
|
|
118
|
+
// An unreadable checkpoint is skipped, not fatal: its originals are still in
|
|
119
|
+
// the log, so replay continues from the previous readable boundary.
|
|
120
|
+
let checkpointProjection;
|
|
88
121
|
for (let index = this.entries.length - 1; index >= 0; index--) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
const entry = this.entries[index];
|
|
123
|
+
if (entry.type === "context_checkpoint") {
|
|
124
|
+
checkpointProjection = tryCheckpointMessages(entry.checkpoint);
|
|
125
|
+
if (!checkpointProjection)
|
|
126
|
+
continue;
|
|
92
127
|
}
|
|
128
|
+
else if (entry.type !== "summary")
|
|
129
|
+
continue;
|
|
130
|
+
latestSummaryIndex = index;
|
|
131
|
+
break;
|
|
93
132
|
}
|
|
94
133
|
for (let index = this.entries.length - 1; index >= 0; index--) {
|
|
95
134
|
const entry = this.entries[index];
|
|
@@ -99,17 +138,49 @@ export class SessionLog {
|
|
|
99
138
|
}
|
|
100
139
|
}
|
|
101
140
|
if (latestSummaryIndex > latestClearIndex) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
141
|
+
const entry = this.entries[latestSummaryIndex];
|
|
142
|
+
if (entry.type === "context_checkpoint") {
|
|
143
|
+
messages.push(...(checkpointProjection ?? []));
|
|
144
|
+
}
|
|
145
|
+
else if (entry.type === "summary") {
|
|
146
|
+
// Legacy summaries remain readable; already discarded originals cannot be reconstructed.
|
|
147
|
+
messages.push({ role: "system", content: `Previous conversation summary: ${entry.summary}` });
|
|
148
|
+
}
|
|
107
149
|
}
|
|
108
150
|
const startIndex = Math.max(latestSummaryIndex > latestClearIndex ? latestSummaryIndex + 1 : 0, latestClearIndex + 1);
|
|
151
|
+
const committedPrefixLength = latestSummaryIndex > latestClearIndex
|
|
152
|
+
&& this.entries[latestSummaryIndex].type === "context_checkpoint" ? messages.length : 0;
|
|
153
|
+
// Every checkpoint past the readable boundary is unreadable by construction.
|
|
154
|
+
// Its projection is unusable, but its position still proves the originals
|
|
155
|
+
// before it were committed at a complete-tool-group model boundary.
|
|
156
|
+
let unreadableBoundaryLength = 0;
|
|
157
|
+
// Permission-mode switches become mode reminders where they happened, so
|
|
158
|
+
// the model reads the mode timeline in order: a later "plan mode is
|
|
159
|
+
// active" outranks an earlier tool result saying the mode went back to
|
|
160
|
+
// default. A switch is written out before the next user message (never
|
|
161
|
+
// inside a tool group); one from before a compaction/clear boundary still
|
|
162
|
+
// applies and opens the replayed history.
|
|
163
|
+
let pendingMode = lastModeSwitchBefore(this.entries, startIndex);
|
|
164
|
+
const flushMode = () => {
|
|
165
|
+
if (pendingMode === undefined)
|
|
166
|
+
return;
|
|
167
|
+
messages.push({ role: "meta", kind: "system-reminder", content: reminderForMode(pendingMode) });
|
|
168
|
+
pendingMode = undefined;
|
|
169
|
+
};
|
|
109
170
|
for (let index = startIndex; index < this.entries.length; index++) {
|
|
110
171
|
const entry = this.entries[index];
|
|
111
172
|
switch (entry.type) {
|
|
173
|
+
case "marker": {
|
|
174
|
+
const mode = entry.kind === "mode_switch" ? permissionModeOf(entry.value) : undefined;
|
|
175
|
+
if (mode)
|
|
176
|
+
pendingMode = mode;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "context_checkpoint":
|
|
180
|
+
unreadableBoundaryLength = messages.length;
|
|
181
|
+
break;
|
|
112
182
|
case "user_message":
|
|
183
|
+
flushMode();
|
|
113
184
|
messages.push(cloneMessage(entry.message));
|
|
114
185
|
break;
|
|
115
186
|
case "assistant_message":
|
|
@@ -120,7 +191,7 @@ export class SessionLog {
|
|
|
120
191
|
reasoning: entry.message.reasoning !== undefined
|
|
121
192
|
? sanitizeInternalReasoningText(entry.message.reasoning)
|
|
122
193
|
: undefined,
|
|
123
|
-
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata)),
|
|
194
|
+
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata), entry.message.modelId ?? entry.message.model),
|
|
124
195
|
});
|
|
125
196
|
break;
|
|
126
197
|
case "tool_call": {
|
|
@@ -145,6 +216,34 @@ export class SessionLog {
|
|
|
145
216
|
break;
|
|
146
217
|
}
|
|
147
218
|
}
|
|
219
|
+
flushMode();
|
|
220
|
+
retireStaleModeReminders(messages);
|
|
221
|
+
// A committed checkpoint may end in completed tools without a final text
|
|
222
|
+
// response. It is a resumable model boundary, not an interrupted user turn.
|
|
223
|
+
const boundary = Math.max(committedPrefixLength, unreadableBoundaryLength);
|
|
224
|
+
if (boundary > 0) {
|
|
225
|
+
let head = messages.slice(0, boundary);
|
|
226
|
+
if (unreadableBoundaryLength > committedPrefixLength) {
|
|
227
|
+
// Replayed originals, not a validated projection: hold the turn the
|
|
228
|
+
// boundary split to complete-tool-group semantics, as its continuation is.
|
|
229
|
+
let turnStart = head.length;
|
|
230
|
+
for (let i = head.length - 1; i >= committedPrefixLength; i--) {
|
|
231
|
+
if (head[i].role === "user") {
|
|
232
|
+
turnStart = i;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
head = [...head.slice(0, turnStart), ...pruneIncompleteToolGroups(head.slice(turnStart))];
|
|
237
|
+
}
|
|
238
|
+
const tail = messages.slice(boundary);
|
|
239
|
+
const nextUser = tail.findIndex(message => message.role === "user");
|
|
240
|
+
const continuationEnd = nextUser < 0 ? tail.length : nextUser;
|
|
241
|
+
return [
|
|
242
|
+
...head,
|
|
243
|
+
...pruneIncompleteToolGroups(tail.slice(0, continuationEnd)),
|
|
244
|
+
...pruneIncompleteTail(tail.slice(continuationEnd)),
|
|
245
|
+
];
|
|
246
|
+
}
|
|
148
247
|
return pruneIncompleteTail(messages);
|
|
149
248
|
}
|
|
150
249
|
}
|
|
@@ -193,7 +292,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
193
292
|
usage: message.usage,
|
|
194
293
|
systemFingerprint: message.systemFingerprint,
|
|
195
294
|
error: message.error,
|
|
196
|
-
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(message.providerMetadata)),
|
|
295
|
+
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(message.providerMetadata), message.modelId ?? message.model),
|
|
197
296
|
},
|
|
198
297
|
timestamp,
|
|
199
298
|
};
|
|
@@ -221,6 +320,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
221
320
|
function isSessionLogEntry(entry) {
|
|
222
321
|
return [
|
|
223
322
|
"metadata",
|
|
323
|
+
"context_checkpoint",
|
|
224
324
|
"summary",
|
|
225
325
|
"marker",
|
|
226
326
|
"user_message",
|
|
@@ -230,16 +330,6 @@ function isSessionLogEntry(entry) {
|
|
|
230
330
|
"provider_error",
|
|
231
331
|
].includes(entry.type);
|
|
232
332
|
}
|
|
233
|
-
function nextEntryId(entries) {
|
|
234
|
-
let max = 0;
|
|
235
|
-
for (const entry of entries) {
|
|
236
|
-
const match = /^(\d+)/.exec(entry.id);
|
|
237
|
-
if (!match)
|
|
238
|
-
continue;
|
|
239
|
-
max = Math.max(max, Number(match[1]));
|
|
240
|
-
}
|
|
241
|
-
return `${max + 1}`;
|
|
242
|
-
}
|
|
243
333
|
function cloneMessage(message) {
|
|
244
334
|
if (message.role === "assistant") {
|
|
245
335
|
return {
|
|
@@ -264,6 +354,64 @@ function cloneProviderMetadata(metadata) {
|
|
|
264
354
|
return undefined;
|
|
265
355
|
return JSON.parse(JSON.stringify(metadata));
|
|
266
356
|
}
|
|
357
|
+
// A checkpoint can split a user turn. Its continuation has no user anchor,
|
|
358
|
+
// so validate complete tool groups directly rather than relying on turn pruning.
|
|
359
|
+
const PERMISSION_MODES = new Set(["default", "plan", "bypassPermissions"]);
|
|
360
|
+
function permissionModeOf(value) {
|
|
361
|
+
return PERMISSION_MODES.has(value) ? value : undefined;
|
|
362
|
+
}
|
|
363
|
+
function lastModeSwitchBefore(entries, end) {
|
|
364
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
365
|
+
const entry = entries[index];
|
|
366
|
+
if (entry.type === "marker" && entry.kind === "mode_switch") {
|
|
367
|
+
const mode = permissionModeOf(entry.value);
|
|
368
|
+
if (mode)
|
|
369
|
+
return mode;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
/** Only the latest mode reminder speaks to the model, as in a live agent. */
|
|
375
|
+
function retireStaleModeReminders(messages) {
|
|
376
|
+
let latest = true;
|
|
377
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
378
|
+
const message = messages[index];
|
|
379
|
+
if (message.role !== "meta" || message.kind !== "system-reminder" || !isPermissionModeReminder(message.content))
|
|
380
|
+
continue;
|
|
381
|
+
if (latest)
|
|
382
|
+
latest = false;
|
|
383
|
+
else
|
|
384
|
+
message.includeInLlm = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function pruneIncompleteToolGroups(messages) {
|
|
388
|
+
const pending = new Set();
|
|
389
|
+
let groupStart = -1;
|
|
390
|
+
for (let index = 0; index < messages.length; index++) {
|
|
391
|
+
const message = messages[index];
|
|
392
|
+
if (message.role === "system" || message.role === "meta")
|
|
393
|
+
continue;
|
|
394
|
+
if (message.role === "tool") {
|
|
395
|
+
if (!pending.delete(message.toolCallId))
|
|
396
|
+
return messages.slice(0, groupStart < 0 ? index : groupStart);
|
|
397
|
+
if (pending.size === 0)
|
|
398
|
+
groupStart = -1;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
if (pending.size)
|
|
402
|
+
return messages.slice(0, groupStart);
|
|
403
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
404
|
+
groupStart = index;
|
|
405
|
+
for (const call of message.toolCalls) {
|
|
406
|
+
if (!call.id || pending.has(call.id))
|
|
407
|
+
return messages.slice(0, groupStart);
|
|
408
|
+
pending.add(call.id);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return pending.size ? messages.slice(0, groupStart) : messages;
|
|
414
|
+
}
|
|
267
415
|
function pruneIncompleteTail(messages) {
|
|
268
416
|
let currentTurnStart = -1;
|
|
269
417
|
let hasCompletedAssistant = false;
|
package/dist/session-types.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export interface SessionMetadataEntry extends BaseSessionLogEntry {
|
|
|
41
41
|
type: "metadata";
|
|
42
42
|
metadata: SessionMetadata;
|
|
43
43
|
}
|
|
44
|
+
export interface SessionContextCheckpointEntry extends BaseSessionLogEntry {
|
|
45
|
+
type: "context_checkpoint";
|
|
46
|
+
checkpoint: import("./context/checkpoint.js").ContextCheckpoint;
|
|
47
|
+
}
|
|
44
48
|
export interface SessionSummaryEntry extends BaseSessionLogEntry {
|
|
45
49
|
type: "summary";
|
|
46
50
|
summary: string;
|
|
@@ -70,7 +74,7 @@ export interface SessionProviderErrorEntry extends BaseSessionLogEntry {
|
|
|
70
74
|
type: "provider_error";
|
|
71
75
|
error: SanitizedProviderError;
|
|
72
76
|
}
|
|
73
|
-
export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry | SessionProviderErrorEntry;
|
|
77
|
+
export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionContextCheckpointEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry | SessionProviderErrorEntry;
|
|
74
78
|
export interface LegacySessionEntry {
|
|
75
79
|
id: string;
|
|
76
80
|
type: "metadata" | "message" | "compaction";
|
package/dist/session.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { CheckpointStore } from "./checkpoints.js";
|
|
5
5
|
import { type CompactOptions, type CompactResult } from "./context/compact.js";
|
|
6
6
|
import type { Message } from "./types.js";
|
|
7
|
+
import { type ContextCheckpoint } from "./context/checkpoint.js";
|
|
7
8
|
import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
|
|
8
9
|
import type { SanitizedProviderError } from "./provider-error-record.js";
|
|
9
10
|
export interface SessionSummary {
|
|
@@ -27,6 +28,15 @@ export interface UserTurn {
|
|
|
27
28
|
text: string;
|
|
28
29
|
timestamp: number;
|
|
29
30
|
}
|
|
31
|
+
/** A fenced write found the session log diverged from the writer's resident
|
|
32
|
+
* snapshot — a foreign append, clear, or rewind landed while a turn was in
|
|
33
|
+
* flight. The write is refused; the host must roll its resident history back
|
|
34
|
+
* to the file's truth (SessionContextFence.reloadHistory, then replace the
|
|
35
|
+
* agent's messages) before writing again, or every later fenced write keeps
|
|
36
|
+
* rejecting on the stale revision. */
|
|
37
|
+
export declare class SessionHistoryDivergedError extends Error {
|
|
38
|
+
constructor(message?: string);
|
|
39
|
+
}
|
|
30
40
|
export interface RewindResult {
|
|
31
41
|
/** Number of log entries removed. */
|
|
32
42
|
removedEntries: number;
|
|
@@ -37,7 +47,18 @@ export declare class SessionManager {
|
|
|
37
47
|
private sessionFile;
|
|
38
48
|
private log;
|
|
39
49
|
private checkpoints?;
|
|
50
|
+
private diskRevision;
|
|
51
|
+
private pendingCompaction?;
|
|
52
|
+
/** Conversational revision excludes metadata-only writes (for example titles). */
|
|
53
|
+
getRevision(): string;
|
|
54
|
+
private refresh;
|
|
55
|
+
private currentDiskRevision;
|
|
56
|
+
private withWriteLock;
|
|
40
57
|
private readonly metadataListeners;
|
|
58
|
+
private readonly contextListeners;
|
|
59
|
+
/** Local commits only: disk refreshes must never advance a host's history fence. */
|
|
60
|
+
subscribeContextCommits(listener: (previous: string, revision: string, replacement: boolean) => void): () => void;
|
|
61
|
+
private publishContextCommit;
|
|
41
62
|
constructor(sessionFile: string);
|
|
42
63
|
static create(cwd: string, sessionName?: string): SessionManager;
|
|
43
64
|
static resume(cwd: string, sessionName?: string): SessionManager | undefined;
|
|
@@ -47,6 +68,8 @@ export declare class SessionManager {
|
|
|
47
68
|
static listAllSessions(): SessionSummary[];
|
|
48
69
|
private load;
|
|
49
70
|
private persist;
|
|
71
|
+
/** Refresh, derive the replacement from the refreshed log, and swap the file
|
|
72
|
+
* as one locked transaction; `derive` returning undefined leaves it untouched. */
|
|
50
73
|
private rewrite;
|
|
51
74
|
getMetadata(): SessionMetadata;
|
|
52
75
|
/**
|
|
@@ -57,13 +80,17 @@ export declare class SessionManager {
|
|
|
57
80
|
subscribeMetadata(listener: (metadata: SessionMetadata) => void): () => void;
|
|
58
81
|
getOrCreatePromptCacheKey(): string;
|
|
59
82
|
setMetadata(metadata: SessionMetadata): void;
|
|
83
|
+
/** Refresh, derive, and append as one locked transaction. Metadata records
|
|
84
|
+
* are full snapshots, so a snapshot built outside the lock would silently
|
|
85
|
+
* overwrite a concurrent writer's fields. */
|
|
86
|
+
mutateMetadata(build: (current: SessionMetadata) => SessionMetadata): void;
|
|
60
87
|
updateMetadata(patch: Partial<SessionMetadata>): void;
|
|
61
88
|
clearTitleMetadata(): void;
|
|
62
89
|
clearExternalRuntimeMetadata(): void;
|
|
63
|
-
appendMessage(message: Message): void;
|
|
64
|
-
|
|
65
|
-
appendMarker(kind: SessionMarkerKind, value: string): void;
|
|
66
|
-
appendProviderError(error: SanitizedProviderError): void;
|
|
90
|
+
appendMessage(message: Message, expectedRevision?: string): void;
|
|
91
|
+
private reloadAfterWriteFailure;
|
|
92
|
+
appendMarker(kind: SessionMarkerKind, value: string, expectedRevision?: string): void;
|
|
93
|
+
appendProviderError(error: SanitizedProviderError, expectedRevision?: string): void;
|
|
67
94
|
compact(options?: CompactOptions): CompactResult;
|
|
68
95
|
/**
|
|
69
96
|
* Inspect whether the session is large enough to compact and, if so, return
|
|
@@ -76,7 +103,7 @@ export declare class SessionManager {
|
|
|
76
103
|
} | null;
|
|
77
104
|
/**
|
|
78
105
|
* Apply a precomputed (typically LLM-generated) summary as the compaction
|
|
79
|
-
* checkpoint,
|
|
106
|
+
* checkpoint, appending an exact projection without deleting originals. Mirrors
|
|
80
107
|
* `compact()` but skips the built-in heuristic summarizer. Returns
|
|
81
108
|
* `{ compacted: false }` if the session is no longer compactable.
|
|
82
109
|
*/
|
|
@@ -87,7 +114,11 @@ export declare class SessionManager {
|
|
|
87
114
|
* Lives next to the session JSONL as `<session>.checkpoints/`.
|
|
88
115
|
*/
|
|
89
116
|
getCheckpoints(): CheckpointStore;
|
|
90
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Entry id of the most recent user turn (as listUserTurns counts them), or
|
|
119
|
+
* "0" before the first one. Checkpoints are keyed by it, so edits made after
|
|
120
|
+
* a tool screenshot or a harness-injected block stay with the user's turn.
|
|
121
|
+
*/
|
|
91
122
|
lastUserEntryId(): string;
|
|
92
123
|
/** User messages after the latest /clear, oldest first — the valid rewind anchors. */
|
|
93
124
|
listUserTurns(): UserTurn[];
|
|
@@ -98,6 +129,7 @@ export declare class SessionManager {
|
|
|
98
129
|
rewindToEntry(entryId: string): RewindResult | undefined;
|
|
99
130
|
getEntries(): SessionLogEntry[];
|
|
100
131
|
getSessionFile(): string;
|
|
101
|
-
|
|
132
|
+
/** Commit the exact conversational projection, never delete its source history. */
|
|
133
|
+
commitContextCheckpoint(checkpoint: ContextCheckpoint, expectedRevision?: string): void;
|
|
102
134
|
}
|
|
103
135
|
export declare function getSessionsDir(cwd: string): string;
|