antechamber 0.1.1 → 0.2.1
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/CHANGELOG.md +45 -0
- package/README.md +24 -14
- package/bin/opencode-antechamber +14 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +10 -6
- package/dist/pi-event-translation.js +237 -80
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +113 -65
- package/dist/routes-v2.js +407 -95
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +54 -13
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.d.ts +1 -1
- package/dist/session-history.js +110 -44
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +191 -38
- package/dist/session-tree.d.ts +10 -2
- package/dist/session-tree.js +55 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +331 -120
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/vcs.d.ts +5 -0
- package/dist/vcs.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/session-history.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultMetadata, resultText, } from "./tool-map.js";
|
|
4
4
|
import { TodoBridge } from "./todo-bridge.js";
|
|
5
|
+
import { contextMessageID } from "./prompt-parts.js";
|
|
5
6
|
import { bindMessageEntries } from "./session-tree.js";
|
|
6
7
|
import { taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
|
|
7
8
|
import { shellTitle, shellUserMessage } from "./session-commands.js";
|
|
@@ -38,9 +39,7 @@ export function parsePiSessionFile(s, agentName) {
|
|
|
38
39
|
}
|
|
39
40
|
return parsePiSessionText(s, agentName, raw);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
const sessionID = s.info.id;
|
|
43
|
-
const directory = s.info.directory;
|
|
42
|
+
function parseEntries(raw) {
|
|
44
43
|
const entries = [];
|
|
45
44
|
for (const line of raw.split("\n")) {
|
|
46
45
|
if (line.length === 0)
|
|
@@ -54,13 +53,44 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
54
53
|
/* torn trailing write — ignore */
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
function nativeTree(entries, leaf) {
|
|
59
|
+
return {
|
|
60
|
+
leafId: leaf?.id ?? null,
|
|
61
|
+
entries: entries.flatMap((entry) => {
|
|
62
|
+
if (entry.id === undefined || entry.type === undefined)
|
|
63
|
+
return [];
|
|
64
|
+
const message = entry.message;
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
type: entry.type,
|
|
68
|
+
id: entry.id,
|
|
69
|
+
parentId: entry.parentId ?? null,
|
|
70
|
+
...(message?.role === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
message: {
|
|
74
|
+
role: message.role,
|
|
75
|
+
...(message.clientMessageId === undefined ? {} : { clientMessageId: message.clientMessageId }),
|
|
76
|
+
...(message.role === "user" ? { content: [{ type: "text", text: textOf(message.content) }] } : {}),
|
|
77
|
+
...(message.command === undefined ? {} : { command: message.command }),
|
|
78
|
+
...(message.output === undefined ? {} : { output: message.output }),
|
|
79
|
+
...(message.exitCode === undefined ? {} : { exitCode: message.exitCode }),
|
|
80
|
+
...(message.cancelled === undefined ? {} : { cancelled: message.cancelled }),
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function activeChain(s, entries) {
|
|
59
89
|
const byId = new Map();
|
|
60
|
-
for (const
|
|
61
|
-
if (typeof
|
|
62
|
-
byId.set(
|
|
63
|
-
let leaf =
|
|
90
|
+
for (const entry of entries)
|
|
91
|
+
if (typeof entry.id === "string")
|
|
92
|
+
byId.set(entry.id, entry);
|
|
93
|
+
let leaf = entries.findLast((entry) => entry.type !== "session" && typeof entry.id === "string");
|
|
64
94
|
let selectedLeaf = s.tree.revert ? s.tree.revert.previousLeafId : s.tree.leafId;
|
|
65
95
|
if (!s.tree.revert && s.tree.localShellTurns?.length) {
|
|
66
96
|
// Follow a newly recorded descendant, never a sibling of the selected native branch.
|
|
@@ -77,40 +107,21 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
77
107
|
leaf = selectedLeaf === null ? undefined : byId.get(selectedLeaf);
|
|
78
108
|
const chain = [];
|
|
79
109
|
const seen = new Set();
|
|
80
|
-
for (let
|
|
81
|
-
seen.add(
|
|
82
|
-
chain.push(
|
|
83
|
-
|
|
110
|
+
for (let entry = leaf; entry !== undefined && !seen.has(entry);) {
|
|
111
|
+
seen.add(entry);
|
|
112
|
+
chain.push(entry);
|
|
113
|
+
entry = typeof entry.parentId === "string" ? byId.get(entry.parentId) : undefined;
|
|
84
114
|
}
|
|
85
115
|
chain.reverse();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id: entry.id,
|
|
96
|
-
parentId: entry.parentId ?? null,
|
|
97
|
-
...(m?.role === undefined
|
|
98
|
-
? {}
|
|
99
|
-
: {
|
|
100
|
-
message: {
|
|
101
|
-
role: m.role,
|
|
102
|
-
...(m.clientMessageId === undefined ? {} : { clientMessageId: m.clientMessageId }),
|
|
103
|
-
...(m.role === "user" ? { content: [{ type: "text", text: textOf(m.content) }] } : {}),
|
|
104
|
-
...(m.command === undefined ? {} : { command: m.command }),
|
|
105
|
-
...(m.output === undefined ? {} : { output: m.output }),
|
|
106
|
-
...(m.exitCode === undefined ? {} : { exitCode: m.exitCode }),
|
|
107
|
-
...(m.cancelled === undefined ? {} : { cancelled: m.cancelled }),
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
}),
|
|
113
|
-
};
|
|
116
|
+
return { chain, nativeTree: nativeTree(entries, leaf) };
|
|
117
|
+
}
|
|
118
|
+
export function parsePiSessionText(s, agentName, raw) {
|
|
119
|
+
const sessionID = s.info.id;
|
|
120
|
+
const directory = s.info.directory;
|
|
121
|
+
const entries = parseEntries(raw);
|
|
122
|
+
const indexOf = new Map();
|
|
123
|
+
entries.forEach((e, i) => indexOf.set(e, i));
|
|
124
|
+
const { chain, nativeTree } = activeChain(s, entries);
|
|
114
125
|
if (s.tree.localShellTurns?.length)
|
|
115
126
|
bindMessageEntries(s, nativeTree);
|
|
116
127
|
const out = [];
|
|
@@ -133,7 +144,41 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
133
144
|
}
|
|
134
145
|
if (e.type === "compaction" && typeof e.summary === "string" && typeof e.firstKeptEntryId === "string") {
|
|
135
146
|
// Not a run boundary: an automatic compaction can sit between steps of one turn.
|
|
136
|
-
out.push(...compactionMessages(s, e.
|
|
147
|
+
out.push(...compactionMessages(s, e.id ?? `r${indexOf.get(e) ?? 0}`, e.summary, entryTime(e), currentModel));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const contextID = contextMessageID(e);
|
|
151
|
+
if (contextID !== undefined) {
|
|
152
|
+
endRun();
|
|
153
|
+
const content = Array.isArray(e.content)
|
|
154
|
+
? e.content
|
|
155
|
+
: typeof e.content === "string"
|
|
156
|
+
? [{ type: "text", text: e.content }]
|
|
157
|
+
: [];
|
|
158
|
+
const time = entryTime(e);
|
|
159
|
+
if (e.id)
|
|
160
|
+
s.tree.messageEntries[contextID] = e.id;
|
|
161
|
+
out.push({
|
|
162
|
+
info: {
|
|
163
|
+
id: contextID,
|
|
164
|
+
sessionID,
|
|
165
|
+
role: "user",
|
|
166
|
+
time: { created: time },
|
|
167
|
+
agent: agentName,
|
|
168
|
+
model: currentModel,
|
|
169
|
+
synthetic: true,
|
|
170
|
+
},
|
|
171
|
+
parts: [
|
|
172
|
+
{
|
|
173
|
+
id: `prt_${contextID}_context`,
|
|
174
|
+
sessionID,
|
|
175
|
+
messageID: contextID,
|
|
176
|
+
type: "text",
|
|
177
|
+
text: textOf(content),
|
|
178
|
+
time: { start: time, end: time },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
137
182
|
continue;
|
|
138
183
|
}
|
|
139
184
|
if (e.type !== "message")
|
|
@@ -366,13 +411,15 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
366
411
|
}
|
|
367
412
|
// system / custom entries are not rendered
|
|
368
413
|
}
|
|
369
|
-
const lastAssistant =
|
|
414
|
+
const lastAssistant = chain.findLast((entry) => entry.message?.role === "assistant");
|
|
370
415
|
const stopReason = lastAssistant?.message?.stopReason;
|
|
371
416
|
if (lastAssistant !== undefined && ["stop", "length", "error", "aborted"].includes(stopReason ?? "")) {
|
|
372
417
|
finishProviderToolParts([...toolParts.values()].flat(), entryTime(lastAssistant), stopReason === "aborted" ? "Cancelled" : "No result");
|
|
373
418
|
}
|
|
374
419
|
s.todoBridge = new TodoBridge(sessionID);
|
|
375
|
-
|
|
420
|
+
// Tree-less snapshots predate branch selection and have no competing ancestor chain.
|
|
421
|
+
const todoEntries = chain.length === 0 && s.tree.leafId === undefined && !s.tree.revert ? entries : chain;
|
|
422
|
+
s.todoBridge.restore(todoEntries, new Map([...toolParts].flatMap(([callID, parts]) => (parts[0] ? [[callID, parts[0]]] : []))));
|
|
376
423
|
// Completions can be appended after the newest message, outside its ancestor chain.
|
|
377
424
|
for (const entry of entries) {
|
|
378
425
|
if (entry.customType !== "omo-senpi:wake")
|
|
@@ -417,5 +464,24 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
417
464
|
});
|
|
418
465
|
(s.inputs ??= new Map()).set(messageID, { text, delivery, accepted: true, delivered: false });
|
|
419
466
|
}
|
|
467
|
+
for (const message of out) {
|
|
468
|
+
const metadata = s.tree.promptMetadata?.[message.info.id];
|
|
469
|
+
if (message.info.role === "user" && metadata !== undefined)
|
|
470
|
+
message.info.metadata = metadata;
|
|
471
|
+
const inputParts = s.tree.promptParts?.[message.info.id];
|
|
472
|
+
if (message.info.role === "user" && inputParts !== undefined)
|
|
473
|
+
message.parts = inputParts.map((part, index) => ({
|
|
474
|
+
...part,
|
|
475
|
+
id: part.id ?? `prt_${message.info.id}_${index}`,
|
|
476
|
+
sessionID,
|
|
477
|
+
messageID: message.info.id,
|
|
478
|
+
}));
|
|
479
|
+
const identities = s.tree.messageParts?.[message.info.id];
|
|
480
|
+
for (const [index, part] of message.parts.entries()) {
|
|
481
|
+
const identity = identities?.[index];
|
|
482
|
+
if (identity !== undefined)
|
|
483
|
+
part.id = identity;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
420
486
|
return out;
|
|
421
487
|
}
|
package/dist/session-hosts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { once } from "node:events";
|
|
4
|
-
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
4
|
+
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
6
|
import { findPidfdPython } from "./pidfd.js";
|
|
7
7
|
/** Capability and warning state are captured once, before any shutdown work. */
|
|
@@ -144,7 +144,7 @@ async function stopSupportedSessionHosts(created, python) {
|
|
|
144
144
|
try {
|
|
145
145
|
// The original leader must still exist after the process-table scan:
|
|
146
146
|
// a recycled group number cannot confer its ownership on new members.
|
|
147
|
-
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === directory;
|
|
147
|
+
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === realpathSync(directory);
|
|
148
148
|
}
|
|
149
149
|
catch (error) {
|
|
150
150
|
if (error instanceof Error && "code" in error && ["ENOENT", "ESRCH"].includes(String(error.code)))
|
package/dist/session-store.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export interface SessionRegistryEntry {
|
|
|
37
37
|
connectorOwned?: true;
|
|
38
38
|
tree?: SessionTreeState;
|
|
39
39
|
parentID?: string | undefined;
|
|
40
|
+
parentMessageID?: string;
|
|
41
|
+
agent?: string;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
40
43
|
task?: ChildTask;
|
|
41
44
|
piSessionId: string;
|
|
42
45
|
piPath: string;
|
|
@@ -53,6 +56,7 @@ export interface SessionRegistryEntry {
|
|
|
53
56
|
model?: {
|
|
54
57
|
providerID: string;
|
|
55
58
|
modelID: string;
|
|
59
|
+
variant?: string;
|
|
56
60
|
};
|
|
57
61
|
created: number;
|
|
58
62
|
updated: number;
|
|
@@ -81,18 +85,18 @@ export declare function piSessionsRootMtime(): number | undefined;
|
|
|
81
85
|
export declare function scanAllPiSessions(): ScannedSession[];
|
|
82
86
|
/** All restorable pi sessions recorded for `directory`. Corrupt files are skipped. */
|
|
83
87
|
export declare function scanPiSessions(directory: string): ScannedSession[];
|
|
84
|
-
/**
|
|
85
|
-
* Persistent oc-session-id → metadata registry (title overrides, known pi
|
|
86
|
-
* paths). Atomic writes, lazy load, corrupt file degrades to an empty map.
|
|
87
|
-
*/
|
|
88
88
|
export declare class SessionStore {
|
|
89
89
|
readonly stateDir: string;
|
|
90
90
|
private readonly file;
|
|
91
91
|
private entries;
|
|
92
92
|
constructor(stateDir?: string);
|
|
93
93
|
private load;
|
|
94
|
+
private readEntries;
|
|
95
|
+
private withRegistryLock;
|
|
94
96
|
private persist;
|
|
97
|
+
private mutate;
|
|
95
98
|
get(ocSessionId: string): SessionRegistryEntry | undefined;
|
|
99
|
+
idFor(piSessionId: string): string | undefined;
|
|
96
100
|
childIds(): string[];
|
|
97
101
|
set(ocSessionId: string, entry: SessionRegistryEntry): void;
|
|
98
102
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
package/dist/session-store.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* the same OpenChamber id, even if the registry file is lost.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { piAgentDir } from "./agent-dir.js";
|
|
20
|
+
import { isPromptPart } from "./prompt-parts.js";
|
|
20
21
|
import { defaultStateDir, envSetting } from "./config.js";
|
|
21
22
|
/** pi's session directory for a given cwd (see pi docs/session-format.md). */
|
|
22
23
|
export function piSessionDir(cwd) {
|
|
@@ -38,6 +39,12 @@ export class SessionPersistenceError extends Error {
|
|
|
38
39
|
/** Read at most this many leading bytes when sniffing headers/titles. */
|
|
39
40
|
const SNIFF_BYTES = 256 * 1024;
|
|
40
41
|
const TITLE_MAX_LINES = 200;
|
|
42
|
+
const TITLE_MAX_CHARS = 60;
|
|
43
|
+
const REGISTRY_DIRECTORY_MODE = 0o700;
|
|
44
|
+
const REGISTRY_FILE_MODE = 0o600;
|
|
45
|
+
const REGISTRY_LOCK_RETRY_MS = 10;
|
|
46
|
+
const REGISTRY_LOCK_STALE_MS = 5_000;
|
|
47
|
+
const registryLockWaiter = new Int32Array(new SharedArrayBuffer(4));
|
|
41
48
|
function readHead(path, bytes = SNIFF_BYTES) {
|
|
42
49
|
const fd = openSync(path, "r");
|
|
43
50
|
try {
|
|
@@ -64,13 +71,16 @@ function titleFromHead(head, piSessionId) {
|
|
|
64
71
|
}
|
|
65
72
|
if (entry.type !== "message" || entry.message?.role !== "user")
|
|
66
73
|
continue;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
const content = entry.message.content ?? [];
|
|
75
|
+
// pi allows plain-string message content (extensions, append_user_message).
|
|
76
|
+
const text = (typeof content === "string"
|
|
77
|
+
? content
|
|
78
|
+
: content
|
|
79
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
80
|
+
.map((b) => b.text)
|
|
81
|
+
.join("\n")).trim();
|
|
72
82
|
if (text !== "")
|
|
73
|
-
return text.slice(0,
|
|
83
|
+
return text.slice(0, TITLE_MAX_CHARS);
|
|
74
84
|
}
|
|
75
85
|
return `pi session ${piSessionId.slice(0, 8)}`;
|
|
76
86
|
}
|
|
@@ -261,6 +271,105 @@ export function scanPiSessions(directory) {
|
|
|
261
271
|
* Persistent oc-session-id → metadata registry (title overrides, known pi
|
|
262
272
|
* paths). Atomic writes, lazy load, corrupt file degrades to an empty map.
|
|
263
273
|
*/
|
|
274
|
+
function record(value) {
|
|
275
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
276
|
+
}
|
|
277
|
+
function stringRecord(value) {
|
|
278
|
+
return record(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
279
|
+
}
|
|
280
|
+
function booleanRecord(value) {
|
|
281
|
+
return record(value) && Object.values(value).every((entry) => typeof entry === "boolean");
|
|
282
|
+
}
|
|
283
|
+
function persistedMessage(value) {
|
|
284
|
+
return (record(value) &&
|
|
285
|
+
record(value.info) &&
|
|
286
|
+
typeof value.info.id === "string" &&
|
|
287
|
+
typeof value.info.role === "string" &&
|
|
288
|
+
Array.isArray(value.parts));
|
|
289
|
+
}
|
|
290
|
+
function sessionTreeState(value) {
|
|
291
|
+
if (!record(value) || !stringRecord(value.messageEntries))
|
|
292
|
+
return false;
|
|
293
|
+
if (value.promptParts !== undefined &&
|
|
294
|
+
(!record(value.promptParts) ||
|
|
295
|
+
!Object.values(value.promptParts).every((parts) => Array.isArray(parts) && parts.every(isPromptPart))))
|
|
296
|
+
return false;
|
|
297
|
+
if (value.promptMetadata !== undefined &&
|
|
298
|
+
(!record(value.promptMetadata) || !Object.values(value.promptMetadata).every((metadata) => record(metadata))))
|
|
299
|
+
return false;
|
|
300
|
+
if (value.messageParts !== undefined &&
|
|
301
|
+
(!record(value.messageParts) ||
|
|
302
|
+
!Object.values(value.messageParts).every((parts) => Array.isArray(parts) && parts.every((part) => typeof part === "string"))))
|
|
303
|
+
return false;
|
|
304
|
+
if (value.shellDispatch !== undefined && !booleanRecord(value.shellDispatch))
|
|
305
|
+
return false;
|
|
306
|
+
if (value.leafId !== undefined && value.leafId !== null && typeof value.leafId !== "string")
|
|
307
|
+
return false;
|
|
308
|
+
if (value.revert !== undefined) {
|
|
309
|
+
if (!record(value.revert) ||
|
|
310
|
+
typeof value.revert.messageID !== "string" ||
|
|
311
|
+
(value.revert.previousLeafId !== null && typeof value.revert.previousLeafId !== "string") ||
|
|
312
|
+
(value.revert.targetId !== null && typeof value.revert.targetId !== "string"))
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
return (value.localShellTurns === undefined ||
|
|
316
|
+
(Array.isArray(value.localShellTurns) &&
|
|
317
|
+
value.localShellTurns.every((turn) => record(turn) &&
|
|
318
|
+
(turn.afterEntryId === null || typeof turn.afterEntryId === "string") &&
|
|
319
|
+
Array.isArray(turn.messages) &&
|
|
320
|
+
turn.messages.length === 2 &&
|
|
321
|
+
persistedMessage(turn.messages[0]) &&
|
|
322
|
+
persistedMessage(turn.messages[1]))));
|
|
323
|
+
}
|
|
324
|
+
function childTask(value) {
|
|
325
|
+
return (record(value) &&
|
|
326
|
+
typeof value.id === "string" &&
|
|
327
|
+
typeof value.parentPiId === "string" &&
|
|
328
|
+
record(value.input) &&
|
|
329
|
+
record(value.details) &&
|
|
330
|
+
(value.terminalAt === undefined || (typeof value.terminalAt === "number" && Number.isFinite(value.terminalAt))));
|
|
331
|
+
}
|
|
332
|
+
function titleSource(value) {
|
|
333
|
+
return value === "user" || value === "native" || value === "fallback" || value === "generated";
|
|
334
|
+
}
|
|
335
|
+
function registryEntry(value) {
|
|
336
|
+
if (!record(value) ||
|
|
337
|
+
typeof value.piPath !== "string" ||
|
|
338
|
+
typeof value.piSessionId !== "string" ||
|
|
339
|
+
typeof value.directory !== "string" ||
|
|
340
|
+
typeof value.created !== "number" ||
|
|
341
|
+
!Number.isFinite(value.created) ||
|
|
342
|
+
typeof value.updated !== "number" ||
|
|
343
|
+
!Number.isFinite(value.updated))
|
|
344
|
+
return false;
|
|
345
|
+
if (value.connectorOwned !== undefined && value.connectorOwned !== true)
|
|
346
|
+
return false;
|
|
347
|
+
if (value.tree !== undefined && !sessionTreeState(value.tree))
|
|
348
|
+
return false;
|
|
349
|
+
if (value.parentID !== undefined && typeof value.parentID !== "string")
|
|
350
|
+
return false;
|
|
351
|
+
if (value.parentMessageID !== undefined && typeof value.parentMessageID !== "string")
|
|
352
|
+
return false;
|
|
353
|
+
if (value.agent !== undefined && typeof value.agent !== "string")
|
|
354
|
+
return false;
|
|
355
|
+
if (value.metadata !== undefined && !record(value.metadata))
|
|
356
|
+
return false;
|
|
357
|
+
if (value.task !== undefined && !childTask(value.task))
|
|
358
|
+
return false;
|
|
359
|
+
if (value.title !== undefined && typeof value.title !== "string")
|
|
360
|
+
return false;
|
|
361
|
+
if (value.titleSource !== undefined && !titleSource(value.titleSource))
|
|
362
|
+
return false;
|
|
363
|
+
if (value.archived !== undefined && (typeof value.archived !== "number" || !Number.isFinite(value.archived)))
|
|
364
|
+
return false;
|
|
365
|
+
if (value.model !== undefined &&
|
|
366
|
+
(!record(value.model) || typeof value.model.providerID !== "string" || typeof value.model.modelID !== "string"))
|
|
367
|
+
return false;
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
function errorCode(error) {
|
|
371
|
+
return error instanceof Error && "code" in error ? String(error.code) : undefined;
|
|
372
|
+
}
|
|
264
373
|
export class SessionStore {
|
|
265
374
|
stateDir;
|
|
266
375
|
file;
|
|
@@ -273,64 +382,108 @@ export class SessionStore {
|
|
|
273
382
|
load() {
|
|
274
383
|
if (this.entries !== undefined)
|
|
275
384
|
return this.entries;
|
|
276
|
-
this.entries =
|
|
385
|
+
this.entries = this.readEntries();
|
|
386
|
+
return this.entries;
|
|
387
|
+
}
|
|
388
|
+
readEntries() {
|
|
389
|
+
const entries = new Map();
|
|
277
390
|
try {
|
|
278
391
|
const raw = JSON.parse(readFileSync(this.file, "utf8"));
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
(typeof v.model.providerID !== "string" || typeof v.model.modelID !== "string")) {
|
|
285
|
-
delete v.model;
|
|
286
|
-
}
|
|
287
|
-
this.entries.set(k, v);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
392
|
+
if (!record(raw))
|
|
393
|
+
return entries;
|
|
394
|
+
for (const [id, value] of Object.entries(raw))
|
|
395
|
+
if (registryEntry(value))
|
|
396
|
+
entries.set(id, value);
|
|
290
397
|
}
|
|
291
398
|
catch {
|
|
292
399
|
/* missing or corrupt — start empty */
|
|
293
400
|
}
|
|
294
|
-
return
|
|
401
|
+
return entries;
|
|
295
402
|
}
|
|
296
|
-
|
|
297
|
-
const
|
|
403
|
+
withRegistryLock(action) {
|
|
404
|
+
const lock = `${this.file}.lock`;
|
|
405
|
+
let fd;
|
|
298
406
|
try {
|
|
299
|
-
mkdirSync(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
407
|
+
mkdirSync(this.stateDir, { recursive: true, mode: REGISTRY_DIRECTORY_MODE });
|
|
408
|
+
chmodSync(this.stateDir, REGISTRY_DIRECTORY_MODE);
|
|
409
|
+
for (;;) {
|
|
410
|
+
try {
|
|
411
|
+
fd = openSync(lock, "wx", REGISTRY_FILE_MODE);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (errorCode(error) !== "EEXIST")
|
|
416
|
+
throw error;
|
|
417
|
+
try {
|
|
418
|
+
if (Date.now() - statSync(lock).mtimeMs >= REGISTRY_LOCK_STALE_MS) {
|
|
419
|
+
unlinkSync(lock);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch (staleLockError) {
|
|
424
|
+
if (errorCode(staleLockError) !== "ENOENT")
|
|
425
|
+
throw staleLockError;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
Atomics.wait(registryLockWaiter, 0, 0, REGISTRY_LOCK_RETRY_MS);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return action();
|
|
303
432
|
}
|
|
304
433
|
catch (error) {
|
|
305
434
|
throw new SessionPersistenceError(this.file, error);
|
|
306
435
|
}
|
|
307
|
-
|
|
436
|
+
finally {
|
|
437
|
+
if (fd !== undefined) {
|
|
438
|
+
closeSync(fd);
|
|
439
|
+
unlinkSync(lock);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
persist(entries) {
|
|
444
|
+
const obj = Object.fromEntries(entries);
|
|
445
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
446
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: REGISTRY_FILE_MODE });
|
|
447
|
+
chmodSync(tmp, REGISTRY_FILE_MODE);
|
|
448
|
+
renameSync(tmp, this.file);
|
|
449
|
+
}
|
|
450
|
+
mutate(change) {
|
|
451
|
+
this.withRegistryLock(() => {
|
|
452
|
+
const entries = this.readEntries();
|
|
453
|
+
if (change(entries))
|
|
454
|
+
this.persist(entries);
|
|
455
|
+
this.entries = entries;
|
|
456
|
+
});
|
|
308
457
|
}
|
|
309
458
|
get(ocSessionId) {
|
|
310
459
|
return this.load().get(ocSessionId);
|
|
311
460
|
}
|
|
461
|
+
idFor(piSessionId) {
|
|
462
|
+
return [...this.load()].find(([, entry]) => entry.piSessionId === piSessionId)?.[0];
|
|
463
|
+
}
|
|
312
464
|
childIds() {
|
|
313
465
|
return [...this.load()]
|
|
314
466
|
.filter(([, entry]) => entry.task !== undefined && entry.parentID !== undefined)
|
|
315
467
|
.map(([id]) => id);
|
|
316
468
|
}
|
|
317
469
|
set(ocSessionId, entry) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
470
|
+
this.mutate((entries) => {
|
|
471
|
+
entries.set(ocSessionId, entry);
|
|
472
|
+
return true;
|
|
473
|
+
});
|
|
321
474
|
}
|
|
322
475
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
323
476
|
update(ocSessionId, patch) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
477
|
+
this.mutate((entries) => {
|
|
478
|
+
const previous = entries.get(ocSessionId);
|
|
479
|
+
if (previous === undefined)
|
|
480
|
+
return false;
|
|
481
|
+
entries.set(ocSessionId, { ...previous, ...patch });
|
|
482
|
+
return true;
|
|
483
|
+
});
|
|
329
484
|
}
|
|
330
485
|
remove(ocSessionId) {
|
|
331
|
-
|
|
332
|
-
if (entries.delete(ocSessionId))
|
|
333
|
-
this.persist(entries);
|
|
486
|
+
this.mutate((entries) => entries.delete(ocSessionId));
|
|
334
487
|
}
|
|
335
488
|
/** Every directory the registry knows about — used to resolve bare session ids. */
|
|
336
489
|
directories() {
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { type PiRpcClient } from "./pi-rpc.js";
|
|
2
2
|
import type { ManagedSession } from "./sessions.js";
|
|
3
3
|
import type { OCMessageWithParts } from "./types.js";
|
|
4
|
+
import type { OCPromptPart } from "./types.js";
|
|
4
5
|
export interface SessionTreeState {
|
|
5
6
|
messageEntries: Record<string, string>;
|
|
7
|
+
/** Published part identities in native content order, keyed by durable message identity. */
|
|
8
|
+
messageParts?: Record<string, string[]>;
|
|
9
|
+
/** Original rich user input; native RPC stores only adapted text and images. */
|
|
10
|
+
promptParts?: Record<string, OCPromptPart[]>;
|
|
11
|
+
/** Client prompt metadata; native RPC does not store it. */
|
|
12
|
+
promptMetadata?: Record<string, Record<string, unknown>>;
|
|
6
13
|
/** Actual transport dispatch, not inferred from a card's terminal state. */
|
|
7
14
|
shellDispatch?: Record<string, boolean>;
|
|
8
15
|
/** Identity-only dispatched admissions; never rendered without a native entry. */
|
|
@@ -21,10 +28,12 @@ export interface TreeEntry {
|
|
|
21
28
|
type: string;
|
|
22
29
|
id: string;
|
|
23
30
|
parentId: string | null;
|
|
31
|
+
customType?: string;
|
|
32
|
+
details?: unknown;
|
|
24
33
|
message?: {
|
|
25
34
|
role: string;
|
|
26
35
|
clientMessageId?: string;
|
|
27
|
-
content?: Array<{
|
|
36
|
+
content?: string | Array<{
|
|
28
37
|
type: string;
|
|
29
38
|
text?: string;
|
|
30
39
|
}>;
|
|
@@ -48,7 +57,6 @@ export declare function recordTurn(s: ManagedSession): void;
|
|
|
48
57
|
export declare function branchEntries(tree: Entries, leafId?: string | null): TreeEntry[];
|
|
49
58
|
/** Live ids belong to the UI; restored ids belong to JSONL offsets. Save both. */
|
|
50
59
|
export declare function bindMessageEntries(s: ManagedSession, tree: Entries): void;
|
|
51
|
-
export declare function entryForMessage(s: ManagedSession, tree: Entries, messageID: string): TreeEntry;
|
|
52
60
|
export declare function forkConversation(s: ManagedSession, pi: PiRpcClient, tree: Entries, messageID?: string): Promise<{
|
|
53
61
|
sessionId: string;
|
|
54
62
|
sessionFile: string;
|