tinker-agent 2.5.0 → 2.7.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/CHANGELOG.md +42 -1
- package/README.md +30 -7
- package/package.json +1 -1
- package/src/agent/loop.ts +22 -0
- package/src/agent/runtime-session.ts +158 -0
- package/src/cli/main.ts +2 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/tui-runner.tsx +16 -1
- package/src/events/stdout-event-printer.ts +14 -0
- package/src/events/types.ts +9 -0
- package/src/memory/contracts.ts +46 -0
- package/src/memory/memory-coordinator.ts +445 -4
- package/src/memory/memory-create-tool.ts +117 -0
- package/src/memory/memory-delete-tool.ts +88 -0
- package/src/memory/memory-store.ts +239 -0
- package/src/memory/memory-update-tool.ts +142 -0
- package/src/observation/observation-builder.ts +70 -0
- package/src/session/session-clone-helpers.ts +249 -0
- package/src/session/session-compatibility-codec.ts +401 -0
- package/src/session/session-store-contracts.ts +304 -0
- package/src/session/session-store-filesystem.ts +245 -0
- package/src/session/session-store-record-codecs.ts +1064 -0
- package/src/session/session-store-value-codecs.ts +156 -0
- package/src/session/session-store.ts +234 -3023
- package/src/session/session-tool-result-codec.ts +594 -0
- package/src/tools/ask-user.ts +100 -0
- package/src/tools/grep.ts +1 -3
- package/src/tools/registry.ts +30 -0
- package/src/tools/types.ts +71 -0
- package/src/tui/app.tsx +35 -5
- package/src/tui/components/ask-user.tsx +61 -0
- package/src/tui/components/footer.tsx +14 -1
- package/src/tui/components/prompt-input.tsx +4 -0
- package/src/tui/components/resume-session-picker.tsx +118 -37
- package/src/tui/event-store.ts +57 -0
- package/src/tui/tui-session-controller.ts +8 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { chmod, lstat, open, readFile } from "node:fs/promises";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
5
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
6
|
+
import type { ProtocolContextView } from "../context/protocol-frame";
|
|
7
|
+
import {
|
|
8
|
+
canonicalSequenceHash,
|
|
9
|
+
renderedMessageHash,
|
|
10
|
+
} from "../context/compiled-context-hash";
|
|
11
|
+
import { ContextRevisionCompiler } from "../context/context-revision-compiler";
|
|
12
|
+
import type { AgentEvent } from "../events/types";
|
|
13
|
+
import { renderObservationLogEvent } from "../events/observation-text-log";
|
|
14
|
+
import type { CloneSessionFaultStage } from "./session-store-contracts";
|
|
15
|
+
import { validateSecureFile } from "./session-store-filesystem";
|
|
16
|
+
import {
|
|
17
|
+
decodeContextRevision,
|
|
18
|
+
decodeContextSurface,
|
|
19
|
+
decodeStoredSwapOverride,
|
|
20
|
+
decodeStoredToolCalls,
|
|
21
|
+
protocolPrefixView,
|
|
22
|
+
} from "./session-store-record-codecs";
|
|
23
|
+
|
|
24
|
+
export const SESSION_SCOPED_TABLES = [
|
|
25
|
+
"session_meta",
|
|
26
|
+
"turns",
|
|
27
|
+
"iterations",
|
|
28
|
+
"protocol_frames",
|
|
29
|
+
"messages",
|
|
30
|
+
"tool_results",
|
|
31
|
+
"context_surfaces",
|
|
32
|
+
"context_revisions",
|
|
33
|
+
"context_overrides",
|
|
34
|
+
"skill_activations",
|
|
35
|
+
"context_measurement_state",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export function rekeyStoredToolCalls(
|
|
39
|
+
database: Database,
|
|
40
|
+
targetSessionId: SessionId,
|
|
41
|
+
): void {
|
|
42
|
+
const rows = database
|
|
43
|
+
.query(
|
|
44
|
+
`SELECT message_id, tool_calls_json FROM messages
|
|
45
|
+
WHERE tool_calls_json IS NOT NULL ORDER BY ordinal`,
|
|
46
|
+
)
|
|
47
|
+
.all() as Array<{ message_id: string; tool_calls_json: string }>;
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const calls = decodeStoredToolCalls(row.tool_calls_json).map((call) => ({
|
|
50
|
+
...call,
|
|
51
|
+
sessionId: targetSessionId,
|
|
52
|
+
}));
|
|
53
|
+
database
|
|
54
|
+
.query("UPDATE messages SET tool_calls_json = ? WHERE message_id = ?")
|
|
55
|
+
.run(stableJsonStringify(calls), row.message_id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function rekeyProtocolView(
|
|
60
|
+
source: ProtocolContextView,
|
|
61
|
+
targetSessionId: SessionId,
|
|
62
|
+
): ProtocolContextView {
|
|
63
|
+
return {
|
|
64
|
+
sessionId: targetSessionId,
|
|
65
|
+
faulted: source.faulted,
|
|
66
|
+
frames: source.frames.map((frame) => ({
|
|
67
|
+
...frame,
|
|
68
|
+
sessionId: targetSessionId,
|
|
69
|
+
})),
|
|
70
|
+
messages: source.messages.map((message) => ({
|
|
71
|
+
...message,
|
|
72
|
+
sessionId: targetSessionId,
|
|
73
|
+
...(message.role === "assistant" && message.toolCalls !== undefined
|
|
74
|
+
? {
|
|
75
|
+
toolCalls: message.toolCalls.map((call) => ({
|
|
76
|
+
...call,
|
|
77
|
+
sessionId: targetSessionId,
|
|
78
|
+
})),
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
})),
|
|
82
|
+
toolResults: source.toolResults.map((result) => ({
|
|
83
|
+
...result,
|
|
84
|
+
sessionId: targetSessionId,
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function rewriteCloneRevisionHashes(
|
|
90
|
+
database: Database,
|
|
91
|
+
canonical: ProtocolContextView,
|
|
92
|
+
): void {
|
|
93
|
+
const surfaces = database
|
|
94
|
+
.query("SELECT * FROM context_surfaces")
|
|
95
|
+
.all()
|
|
96
|
+
.map(decodeContextSurface);
|
|
97
|
+
const surfacesById = new Map(surfaces.map((surface) => [surface.surfaceId, surface]));
|
|
98
|
+
const revisions = database
|
|
99
|
+
.query("SELECT * FROM context_revisions ORDER BY revision_number")
|
|
100
|
+
.all()
|
|
101
|
+
.map(decodeContextRevision);
|
|
102
|
+
const revisionNumberById = new Map(
|
|
103
|
+
revisions.map((revision) => [revision.revisionId, revision.revisionNumber]),
|
|
104
|
+
);
|
|
105
|
+
const overrides = database
|
|
106
|
+
.query(
|
|
107
|
+
`SELECT co.* FROM context_overrides co
|
|
108
|
+
JOIN context_revisions cr ON cr.revision_id = co.introduced_revision_id
|
|
109
|
+
ORDER BY cr.revision_number, co.ordinal`,
|
|
110
|
+
)
|
|
111
|
+
.all()
|
|
112
|
+
.map(decodeStoredSwapOverride);
|
|
113
|
+
const compiler = new ContextRevisionCompiler();
|
|
114
|
+
for (const revision of revisions) {
|
|
115
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
116
|
+
if (surface === undefined) {
|
|
117
|
+
throw new Error(`Cloned revision ${revision.revisionId} has no surface.`);
|
|
118
|
+
}
|
|
119
|
+
const activeOverrides = overrides.filter(
|
|
120
|
+
(override) =>
|
|
121
|
+
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
122
|
+
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
123
|
+
override.ordinal >= revision.keepFromOrdinal,
|
|
124
|
+
);
|
|
125
|
+
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
126
|
+
const compiled = compiler.compileForIdentityRekey({
|
|
127
|
+
canonical: prefix,
|
|
128
|
+
revisionId: revision.revisionId,
|
|
129
|
+
activeOverrides,
|
|
130
|
+
keepFromOrdinal: revision.keepFromOrdinal,
|
|
131
|
+
surface,
|
|
132
|
+
});
|
|
133
|
+
database
|
|
134
|
+
.query(
|
|
135
|
+
`UPDATE context_revisions
|
|
136
|
+
SET canonical_sequence_sha256 = ?, rendered_message_sha256 = ?
|
|
137
|
+
WHERE revision_id = ?`,
|
|
138
|
+
)
|
|
139
|
+
.run(
|
|
140
|
+
canonicalSequenceHash(canonical, revision.sourceThroughOrdinal),
|
|
141
|
+
renderedMessageHash(compiled.entries, revision.sourceThroughOrdinal),
|
|
142
|
+
revision.revisionId,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function cloneDiagnosticFiles(input: {
|
|
148
|
+
sourceDirectory: string;
|
|
149
|
+
stagingDirectory: string;
|
|
150
|
+
sourceSessionId: SessionId;
|
|
151
|
+
targetSessionId: SessionId;
|
|
152
|
+
nextEventSequence: number;
|
|
153
|
+
faultInjector?: (stage: CloneSessionFaultStage) => void;
|
|
154
|
+
}): Promise<void> {
|
|
155
|
+
const sourcePath = path.join(input.sourceDirectory, "events.jsonl");
|
|
156
|
+
try {
|
|
157
|
+
await lstat(sourcePath);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
await validateSecureFile(sourcePath, input.sourceSessionId);
|
|
165
|
+
|
|
166
|
+
const bytes = await readFile(sourcePath);
|
|
167
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
168
|
+
const rawLines = text.split("\n");
|
|
169
|
+
if (rawLines.at(-1) === "") {
|
|
170
|
+
rawLines.pop();
|
|
171
|
+
}
|
|
172
|
+
const events: AgentEvent[] = [];
|
|
173
|
+
let previousSequence = 0;
|
|
174
|
+
for (const [index, line] of rawLines.entries()) {
|
|
175
|
+
if (line === "") {
|
|
176
|
+
throw new Error(`Session event log contains an empty line at ${index + 1}.`);
|
|
177
|
+
}
|
|
178
|
+
let value: unknown;
|
|
179
|
+
try {
|
|
180
|
+
value = JSON.parse(line);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Session event log has invalid JSON at line ${index + 1}.`, {
|
|
183
|
+
cause: error,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (!isEventEnvelope(value)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Session event log has an invalid envelope at line ${index + 1}.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (value.sessionId !== input.sourceSessionId) {
|
|
192
|
+
throw new Error(`Session event log identity changed at line ${index + 1}.`);
|
|
193
|
+
}
|
|
194
|
+
if (value.eventSequence <= previousSequence) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Session event sequence is not strictly increasing at line ${index + 1}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (value.eventSequence >= input.nextEventSequence) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Session event sequence exceeds the canonical next counter at line ${index + 1}.`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
previousSequence = value.eventSequence;
|
|
205
|
+
events.push({ ...value, sessionId: input.targetSessionId });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const eventText = events.map((event) => JSON.stringify(event)).join("\n");
|
|
209
|
+
await writePrivateNewFile(
|
|
210
|
+
path.join(input.stagingDirectory, "events.jsonl"),
|
|
211
|
+
eventText === "" ? "" : `${eventText}\n`,
|
|
212
|
+
);
|
|
213
|
+
input.faultInjector?.("after_event_rewrite");
|
|
214
|
+
const observationText = events
|
|
215
|
+
.map((event) => renderObservationLogEvent(event))
|
|
216
|
+
.filter((block): block is string => block !== undefined)
|
|
217
|
+
.join("");
|
|
218
|
+
await writePrivateNewFile(
|
|
219
|
+
path.join(input.stagingDirectory, "observations.md"),
|
|
220
|
+
observationText,
|
|
221
|
+
);
|
|
222
|
+
input.faultInjector?.("after_observation_render");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isEventEnvelope(value: unknown): value is AgentEvent {
|
|
226
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const record = value as Record<string, unknown>;
|
|
230
|
+
return (
|
|
231
|
+
typeof record.sessionId === "string" &&
|
|
232
|
+
Number.isSafeInteger(record.eventSequence) &&
|
|
233
|
+
Number(record.eventSequence) >= 1 &&
|
|
234
|
+
typeof record.timestamp === "string" &&
|
|
235
|
+
typeof record.type === "string" &&
|
|
236
|
+
record.data !== null &&
|
|
237
|
+
typeof record.data === "object"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function writePrivateNewFile(filePath: string, content: string): Promise<void> {
|
|
242
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
243
|
+
try {
|
|
244
|
+
await handle.writeFile(content, "utf8");
|
|
245
|
+
} finally {
|
|
246
|
+
await handle.close();
|
|
247
|
+
}
|
|
248
|
+
await chmod(filePath, 0o600);
|
|
249
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import type { ContextRevisionId, SessionId } from "../ids/runtime-id";
|
|
2
|
+
import {
|
|
3
|
+
createModelContextProfile,
|
|
4
|
+
type ModelContextProfile,
|
|
5
|
+
} from "../model/model-context-profile";
|
|
6
|
+
import {
|
|
7
|
+
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
8
|
+
type ModelMessageProtocol,
|
|
9
|
+
} from "../model/model-client";
|
|
10
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
11
|
+
import { immutableCanonicalClone } from "../context/protocol-frame";
|
|
12
|
+
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
13
|
+
import {
|
|
14
|
+
IMAGE_INPUT_POLICY,
|
|
15
|
+
IMAGE_INPUT_POLICY_VERSION,
|
|
16
|
+
} from "../image/image-input-policy";
|
|
17
|
+
import type {
|
|
18
|
+
SessionCompatibilityContract,
|
|
19
|
+
StoredSessionMetaV10,
|
|
20
|
+
} from "./session-store-contracts";
|
|
21
|
+
import {
|
|
22
|
+
assertObjectKeys,
|
|
23
|
+
enumFromSql,
|
|
24
|
+
nullableNumberFromSql,
|
|
25
|
+
nullableStringFromSql,
|
|
26
|
+
numberFromJson,
|
|
27
|
+
numberFromSql,
|
|
28
|
+
parseJson,
|
|
29
|
+
recordFromSql,
|
|
30
|
+
sha256FromSql,
|
|
31
|
+
stringFromSql,
|
|
32
|
+
timestampFromSql,
|
|
33
|
+
} from "./session-store-value-codecs";
|
|
34
|
+
|
|
35
|
+
export function createSessionCompatibilityContract(input: {
|
|
36
|
+
modelName: string;
|
|
37
|
+
profileName?: string;
|
|
38
|
+
includeReasoningContent: boolean;
|
|
39
|
+
contextProfile: ModelContextProfile;
|
|
40
|
+
messageProtocol: ModelMessageProtocol;
|
|
41
|
+
inputModalities: readonly ("text" | "image")[];
|
|
42
|
+
toolResultModalities: readonly ("text" | "image")[];
|
|
43
|
+
}): SessionCompatibilityContract {
|
|
44
|
+
if (input.modelName.trim() === "") {
|
|
45
|
+
throw new Error("Session compatibility model name must not be empty.");
|
|
46
|
+
}
|
|
47
|
+
if (input.profileName !== undefined && input.profileName.trim() === "") {
|
|
48
|
+
throw new Error("Session compatibility profile name must not be empty.");
|
|
49
|
+
}
|
|
50
|
+
if (typeof input.includeReasoningContent !== "boolean") {
|
|
51
|
+
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
!MODEL_MESSAGE_PROTOCOL_ADAPTERS.includes(input.messageProtocol.adapter) ||
|
|
55
|
+
input.messageProtocol.serializationVersion.trim() === ""
|
|
56
|
+
) {
|
|
57
|
+
throw new Error("Session compatibility message protocol is invalid.");
|
|
58
|
+
}
|
|
59
|
+
const inputModalities = normalizeInputModalities(input.inputModalities);
|
|
60
|
+
const toolResultModalities = normalizeInputModalities(input.toolResultModalities);
|
|
61
|
+
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
62
|
+
throw new Error("Session compatibility image tool results require image input.");
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
modelName: input.modelName,
|
|
66
|
+
...(input.profileName === undefined ? {} : { profileName: input.profileName }),
|
|
67
|
+
includeReasoningContent: input.includeReasoningContent,
|
|
68
|
+
contextProfile: Object.freeze(createModelContextProfile(input.contextProfile)),
|
|
69
|
+
messageProtocol: immutableCanonicalClone(input.messageProtocol),
|
|
70
|
+
media: Object.freeze({
|
|
71
|
+
policyVersion: IMAGE_INPUT_POLICY_VERSION,
|
|
72
|
+
policySha256: sha256(
|
|
73
|
+
stableJsonStringify({
|
|
74
|
+
version: IMAGE_INPUT_POLICY_VERSION,
|
|
75
|
+
...IMAGE_INPUT_POLICY,
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
inputModalities,
|
|
79
|
+
toolResultModalities,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function normalizeSessionCompatibilityContract(
|
|
85
|
+
contract: SessionCompatibilityContract,
|
|
86
|
+
): SessionCompatibilityContract {
|
|
87
|
+
return createSessionCompatibilityContract({
|
|
88
|
+
modelName: contract.modelName,
|
|
89
|
+
...(contract.profileName === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { profileName: contract.profileName }),
|
|
92
|
+
includeReasoningContent: contract.includeReasoningContent,
|
|
93
|
+
contextProfile: contract.contextProfile,
|
|
94
|
+
messageProtocol: contract.messageProtocol,
|
|
95
|
+
inputModalities: contract.media.inputModalities,
|
|
96
|
+
toolResultModalities: contract.media.toolResultModalities,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeInputModalities(
|
|
101
|
+
modalities: readonly ("text" | "image")[],
|
|
102
|
+
): readonly ("text" | "image")[] {
|
|
103
|
+
if (
|
|
104
|
+
modalities.length === 0 ||
|
|
105
|
+
modalities.some((value) => value !== "text" && value !== "image") ||
|
|
106
|
+
new Set(modalities).size !== modalities.length ||
|
|
107
|
+
!modalities.includes("text")
|
|
108
|
+
) {
|
|
109
|
+
throw new Error("Session compatibility input modalities are invalid.");
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze(
|
|
112
|
+
modalities.includes("image") ? (["text", "image"] as const) : (["text"] as const),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function decodeMeta(
|
|
117
|
+
value: unknown,
|
|
118
|
+
expectedSessionId: SessionId,
|
|
119
|
+
): StoredSessionMetaV10 {
|
|
120
|
+
const row = recordFromSql(value, "session metadata");
|
|
121
|
+
const sessionId = stringFromSql(row.session_id, "session_id") as SessionId;
|
|
122
|
+
if (sessionId !== expectedSessionId) {
|
|
123
|
+
throw new Error(`Metadata session ID ${sessionId} does not match directory.`);
|
|
124
|
+
}
|
|
125
|
+
const schemaVersion = numberFromSql(row.schema_version, "schema_version");
|
|
126
|
+
if (schemaVersion !== 10) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Session metadata schema version must be 10; received ${schemaVersion}.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const projectInstructionFile = nullableStringFromSql(
|
|
132
|
+
row.project_instruction_file,
|
|
133
|
+
"project_instruction_file",
|
|
134
|
+
);
|
|
135
|
+
const projectInstructionByteLength = nullableNumberFromSql(
|
|
136
|
+
row.project_instruction_byte_length,
|
|
137
|
+
"project_instruction_byte_length",
|
|
138
|
+
);
|
|
139
|
+
const projectInstructionSha256 = nullableStringFromSql(
|
|
140
|
+
row.project_instruction_sha256,
|
|
141
|
+
"project_instruction_sha256",
|
|
142
|
+
);
|
|
143
|
+
if (
|
|
144
|
+
(projectInstructionFile === null) !== (projectInstructionByteLength === null) ||
|
|
145
|
+
(projectInstructionFile === null) !== (projectInstructionSha256 === null)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("Project instruction metadata must be entirely set or null.");
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
projectInstructionFile !== null &&
|
|
151
|
+
projectInstructionFile !== "AGENTS.md" &&
|
|
152
|
+
projectInstructionFile !== "CLAUDE.md"
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(`Invalid project instruction file ${projectInstructionFile}.`);
|
|
155
|
+
}
|
|
156
|
+
const projectInstruction: ProjectInstructionManifest | undefined =
|
|
157
|
+
projectInstructionFile === null ||
|
|
158
|
+
projectInstructionByteLength === null ||
|
|
159
|
+
projectInstructionSha256 === null
|
|
160
|
+
? undefined
|
|
161
|
+
: {
|
|
162
|
+
path: projectInstructionFile === "AGENTS.md" ? "AGENTS.md" : "CLAUDE.md",
|
|
163
|
+
byteLength: projectInstructionByteLength,
|
|
164
|
+
sha256: sha256FromSql(projectInstructionSha256, "project_instruction_sha256"),
|
|
165
|
+
};
|
|
166
|
+
const initializationState = enumFromSql(
|
|
167
|
+
row.initialization_state,
|
|
168
|
+
["creating", "ready"] as const,
|
|
169
|
+
"initialization_state",
|
|
170
|
+
);
|
|
171
|
+
const sessionCompatibilityJson = nullableStringFromSql(
|
|
172
|
+
row.session_compatibility_json,
|
|
173
|
+
"session_compatibility_json",
|
|
174
|
+
);
|
|
175
|
+
const sessionCompatibilitySha256 = nullableStringFromSql(
|
|
176
|
+
row.session_compatibility_sha256,
|
|
177
|
+
"session_compatibility_sha256",
|
|
178
|
+
);
|
|
179
|
+
const activeRevisionId = nullableStringFromSql(
|
|
180
|
+
row.active_revision_id,
|
|
181
|
+
"active_revision_id",
|
|
182
|
+
) as ContextRevisionId | null;
|
|
183
|
+
const modelName = stringFromSql(row.model_name, "model_name");
|
|
184
|
+
const storedContract =
|
|
185
|
+
sessionCompatibilityJson === null
|
|
186
|
+
? undefined
|
|
187
|
+
: decodeSessionCompatibilityContract(sessionCompatibilityJson);
|
|
188
|
+
if (
|
|
189
|
+
(sessionCompatibilityJson === null) !== (sessionCompatibilitySha256 === null) ||
|
|
190
|
+
(sessionCompatibilityJson !== null &&
|
|
191
|
+
sha256(sessionCompatibilityJson) !== sessionCompatibilitySha256) ||
|
|
192
|
+
(initializationState === "creating") !== (activeRevisionId === null) ||
|
|
193
|
+
(initializationState === "creating") !== (sessionCompatibilityJson === null) ||
|
|
194
|
+
(storedContract !== undefined &&
|
|
195
|
+
(storedContract.modelName !== modelName ||
|
|
196
|
+
stableJsonStringify(storedContract) !== sessionCompatibilityJson))
|
|
197
|
+
) {
|
|
198
|
+
throw new Error("Session compatibility or initialization metadata is invalid.");
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
schemaVersion,
|
|
202
|
+
schemaFingerprint: stringFromSql(row.schema_fingerprint, "schema_fingerprint"),
|
|
203
|
+
initializationState,
|
|
204
|
+
sessionId,
|
|
205
|
+
workspaceRoot: stringFromSql(row.workspace_root, "workspace_root"),
|
|
206
|
+
modelName,
|
|
207
|
+
systemPromptSha256: stringFromSql(row.system_prompt_sha256, "system_prompt_sha256"),
|
|
208
|
+
...(projectInstruction === undefined ? {} : { projectInstruction }),
|
|
209
|
+
sessionCompatibilityJson,
|
|
210
|
+
sessionCompatibilitySha256,
|
|
211
|
+
activeRevisionId,
|
|
212
|
+
nextTurnNumber: numberFromSql(row.next_turn_number, "next_turn_number"),
|
|
213
|
+
nextEventSequence: numberFromSql(row.next_event_sequence, "next_event_sequence"),
|
|
214
|
+
openCount: numberFromSql(row.open_count, "open_count"),
|
|
215
|
+
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
216
|
+
updatedAt: timestampFromSql(row.updated_at, "updated_at"),
|
|
217
|
+
lastOpenedAt: timestampFromSql(row.last_opened_at, "last_opened_at"),
|
|
218
|
+
lastClosedAt:
|
|
219
|
+
row.last_closed_at === null
|
|
220
|
+
? null
|
|
221
|
+
: timestampFromSql(row.last_closed_at, "last_closed_at"),
|
|
222
|
+
lastCloseReason:
|
|
223
|
+
row.last_close_reason === null
|
|
224
|
+
? null
|
|
225
|
+
: enumFromSql(
|
|
226
|
+
row.last_close_reason,
|
|
227
|
+
[
|
|
228
|
+
"oneshot_complete",
|
|
229
|
+
"tui_exit",
|
|
230
|
+
"session_switch",
|
|
231
|
+
"runner_failed",
|
|
232
|
+
"initialization_failed",
|
|
233
|
+
] as const,
|
|
234
|
+
"last_close_reason",
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function compatibilityContractDifferences(
|
|
240
|
+
storedJson: string | null,
|
|
241
|
+
current: SessionCompatibilityContract,
|
|
242
|
+
): string[] {
|
|
243
|
+
if (storedJson === null) {
|
|
244
|
+
return ["sessionCompatibility"];
|
|
245
|
+
}
|
|
246
|
+
const stored = parseJson(storedJson, "session_compatibility_json");
|
|
247
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
248
|
+
return ["sessionCompatibility"];
|
|
249
|
+
}
|
|
250
|
+
const record = stored as Record<string, unknown>;
|
|
251
|
+
const fields: readonly (keyof SessionCompatibilityContract)[] = [
|
|
252
|
+
"modelName",
|
|
253
|
+
"profileName",
|
|
254
|
+
"includeReasoningContent",
|
|
255
|
+
"contextProfile",
|
|
256
|
+
"messageProtocol",
|
|
257
|
+
"media",
|
|
258
|
+
];
|
|
259
|
+
return fields.filter((key) => {
|
|
260
|
+
const storedValue = record[key];
|
|
261
|
+
const currentValue = current[key];
|
|
262
|
+
if (storedValue === undefined || currentValue === undefined) {
|
|
263
|
+
return storedValue !== currentValue;
|
|
264
|
+
}
|
|
265
|
+
return stableJsonStringify(storedValue) !== stableJsonStringify(currentValue);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function decodeSessionCompatibilityContract(
|
|
270
|
+
json: string,
|
|
271
|
+
): SessionCompatibilityContract {
|
|
272
|
+
const record = recordFromSql(
|
|
273
|
+
parseJson(json, "session_compatibility_json"),
|
|
274
|
+
"session compatibility contract",
|
|
275
|
+
);
|
|
276
|
+
assertObjectKeys(
|
|
277
|
+
record,
|
|
278
|
+
[
|
|
279
|
+
"modelName",
|
|
280
|
+
"profileName",
|
|
281
|
+
"includeReasoningContent",
|
|
282
|
+
"contextProfile",
|
|
283
|
+
"messageProtocol",
|
|
284
|
+
"media",
|
|
285
|
+
],
|
|
286
|
+
[
|
|
287
|
+
"modelName",
|
|
288
|
+
"includeReasoningContent",
|
|
289
|
+
"contextProfile",
|
|
290
|
+
"messageProtocol",
|
|
291
|
+
"media",
|
|
292
|
+
],
|
|
293
|
+
"session compatibility contract",
|
|
294
|
+
);
|
|
295
|
+
const contextProfile = recordFromSql(
|
|
296
|
+
record.contextProfile,
|
|
297
|
+
"session compatibility context profile",
|
|
298
|
+
);
|
|
299
|
+
assertObjectKeys(
|
|
300
|
+
contextProfile,
|
|
301
|
+
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
302
|
+
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
303
|
+
"session compatibility context profile",
|
|
304
|
+
);
|
|
305
|
+
const messageProtocol = recordFromSql(
|
|
306
|
+
record.messageProtocol,
|
|
307
|
+
"session compatibility message protocol",
|
|
308
|
+
);
|
|
309
|
+
assertObjectKeys(
|
|
310
|
+
messageProtocol,
|
|
311
|
+
["adapter", "serializationVersion"],
|
|
312
|
+
["adapter", "serializationVersion"],
|
|
313
|
+
"session compatibility message protocol",
|
|
314
|
+
);
|
|
315
|
+
const media = recordFromSql(record.media, "session compatibility media");
|
|
316
|
+
assertObjectKeys(
|
|
317
|
+
media,
|
|
318
|
+
["policyVersion", "policySha256", "inputModalities", "toolResultModalities"],
|
|
319
|
+
["policyVersion", "policySha256", "inputModalities", "toolResultModalities"],
|
|
320
|
+
"session compatibility media",
|
|
321
|
+
);
|
|
322
|
+
const inputModalities = decodeCompatibilityModalities(media.inputModalities, "input");
|
|
323
|
+
const toolResultModalities = decodeCompatibilityModalities(
|
|
324
|
+
media.toolResultModalities,
|
|
325
|
+
"tool result",
|
|
326
|
+
);
|
|
327
|
+
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
328
|
+
throw new Error("Session compatibility image tool results require image input.");
|
|
329
|
+
}
|
|
330
|
+
if (typeof record.includeReasoningContent !== "boolean") {
|
|
331
|
+
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
332
|
+
}
|
|
333
|
+
const modelName = stringFromSql(record.modelName, "compatibility modelName");
|
|
334
|
+
const profileName =
|
|
335
|
+
record.profileName === undefined
|
|
336
|
+
? undefined
|
|
337
|
+
: stringFromSql(record.profileName, "compatibility profileName");
|
|
338
|
+
const context = createModelContextProfile({
|
|
339
|
+
contextWindowTokens: numberFromJson(
|
|
340
|
+
contextProfile.contextWindowTokens,
|
|
341
|
+
"compatibility contextWindowTokens",
|
|
342
|
+
),
|
|
343
|
+
maxSupportedOutputTokens: numberFromJson(
|
|
344
|
+
contextProfile.maxSupportedOutputTokens,
|
|
345
|
+
"compatibility maxSupportedOutputTokens",
|
|
346
|
+
),
|
|
347
|
+
});
|
|
348
|
+
const protocol: ModelMessageProtocol = Object.freeze({
|
|
349
|
+
adapter: enumFromSql(
|
|
350
|
+
messageProtocol.adapter,
|
|
351
|
+
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
352
|
+
"compatibility message adapter",
|
|
353
|
+
),
|
|
354
|
+
serializationVersion: stringFromSql(
|
|
355
|
+
messageProtocol.serializationVersion,
|
|
356
|
+
"compatibility serializationVersion",
|
|
357
|
+
),
|
|
358
|
+
});
|
|
359
|
+
const policyVersion = stringFromSql(
|
|
360
|
+
media.policyVersion,
|
|
361
|
+
"compatibility image policyVersion",
|
|
362
|
+
);
|
|
363
|
+
const policySha256 = stringFromSql(
|
|
364
|
+
media.policySha256,
|
|
365
|
+
"compatibility image policySha256",
|
|
366
|
+
);
|
|
367
|
+
if (!/^[0-9a-f]{64}$/.test(policySha256)) {
|
|
368
|
+
throw new Error("Session image policy hash is invalid.");
|
|
369
|
+
}
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
modelName,
|
|
372
|
+
...(record.profileName === undefined ? {} : { profileName: profileName! }),
|
|
373
|
+
includeReasoningContent: record.includeReasoningContent,
|
|
374
|
+
contextProfile: Object.freeze(context),
|
|
375
|
+
messageProtocol: protocol,
|
|
376
|
+
media: Object.freeze({
|
|
377
|
+
policyVersion,
|
|
378
|
+
policySha256,
|
|
379
|
+
inputModalities,
|
|
380
|
+
toolResultModalities,
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function decodeCompatibilityModalities(
|
|
386
|
+
value: unknown,
|
|
387
|
+
label: string,
|
|
388
|
+
): readonly ("text" | "image")[] {
|
|
389
|
+
if (!Array.isArray(value)) {
|
|
390
|
+
throw new Error(`Session compatibility ${label} modalities must be an array.`);
|
|
391
|
+
}
|
|
392
|
+
return normalizeInputModalities(
|
|
393
|
+
value.map((modality) =>
|
|
394
|
+
enumFromSql(
|
|
395
|
+
modality,
|
|
396
|
+
["text", "image"] as const,
|
|
397
|
+
`compatibility ${label} modality`,
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
}
|