tinker-agent 1.9.0 → 1.11.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 +36 -1
- package/README.md +64 -6
- package/package.json +1 -1
- package/src/agent/loop.ts +17 -0
- package/src/agent/runtime-session.ts +341 -1
- package/src/agent/session-ledger.ts +100 -3
- package/src/cli/config.ts +11 -2
- package/src/cli/model-profiles.ts +58 -0
- package/src/cli/public-config-contract.ts +73 -7
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/runner-dependencies.ts +28 -4
- package/src/cli/tui-memory.ts +4 -0
- package/src/cli/tui-runner.tsx +8 -1
- package/src/context/context-automation-policy.ts +22 -21
- package/src/context/context-manager.ts +91 -15
- package/src/context/context-policy.ts +0 -2
- package/src/context/context-swap-renderer.ts +1 -1
- package/src/context/prefix-retirement-planner.ts +58 -8
- package/src/context/recall-retirement-contract.ts +5 -4
- package/src/context/swap-planner.ts +33 -27
- package/src/events/observation-text-log.ts +4 -0
- package/src/events/stdout-event-printer.ts +5 -0
- package/src/events/types.ts +5 -1
- package/src/model/fake-model-client.ts +55 -16
- package/src/model/model-api.ts +12 -0
- package/src/model/model-client.ts +9 -1
- package/src/model/moonshot-input-token-estimator.ts +5 -1
- package/src/model/openai-chat-mapping.ts +2 -24
- package/src/model/openai-chat-model-client.ts +18 -294
- package/src/model/openai-image-mapping.ts +20 -0
- package/src/model/openai-model-utils.ts +304 -0
- package/src/model/openai-responses-mapping.ts +532 -0
- package/src/model/openai-responses-model-client.ts +295 -0
- package/src/model/openai-responses-stream.ts +96 -0
- package/src/model/openai-responses-token-estimator.ts +155 -0
- package/src/model/reasoning-effort.ts +60 -0
- package/src/session/session-catalog.ts +2 -2
- package/src/session/session-history-reader.ts +6 -1
- package/src/session/session-schema.ts +268 -4
- package/src/session/session-store.ts +134 -26
- package/src/skills/skill-context.ts +2 -2
- package/src/tools/bounded-output-preview.ts +276 -0
- package/src/tools/recall.ts +67 -36
- package/src/tools/registry.ts +7 -2
- package/src/tools/task-output-snapshot.ts +6 -22
- package/src/tools/task-output.ts +23 -27
- package/src/tui/app.tsx +153 -11
- package/src/tui/components/footer.tsx +6 -1
- package/src/tui/components/prompt-input.tsx +9 -1
- package/src/tui/event-store.ts +15 -0
- package/src/tui/slash-commands.ts +20 -0
- package/src/tui/tui-session-controller.ts +14 -0
|
@@ -442,7 +442,9 @@ const schemaDefinitions: readonly SchemaDefinition[] = [
|
|
|
442
442
|
WHERE role IN ('user', 'assistant', 'tool')
|
|
443
443
|
AND content IS NOT NULL
|
|
444
444
|
AND length(content) > 0
|
|
445
|
-
AND NOT (
|
|
445
|
+
AND NOT (
|
|
446
|
+
role = 'tool' AND name IN ('Recall', 'RecallSearch', 'RecallGet')
|
|
447
|
+
)`,
|
|
446
448
|
},
|
|
447
449
|
{
|
|
448
450
|
type: "table",
|
|
@@ -462,7 +464,9 @@ const schemaDefinitions: readonly SchemaDefinition[] = [
|
|
|
462
464
|
WHEN NEW.role IN ('user', 'assistant', 'tool')
|
|
463
465
|
AND NEW.content IS NOT NULL
|
|
464
466
|
AND length(NEW.content) > 0
|
|
465
|
-
AND NOT (
|
|
467
|
+
AND NOT (
|
|
468
|
+
NEW.role = 'tool' AND NEW.name IN ('Recall', 'RecallSearch', 'RecallGet')
|
|
469
|
+
)
|
|
466
470
|
BEGIN
|
|
467
471
|
INSERT INTO message_fts(rowid, content)
|
|
468
472
|
VALUES (NEW.rowid, NEW.content);
|
|
@@ -595,7 +599,12 @@ const schemaDefinitions: readonly SchemaDefinition[] = [
|
|
|
595
599
|
AND pf.kind = 'user' AND pf.state = 'closed'
|
|
596
600
|
AND pf.first_ordinal = NEW.keep_from_ordinal
|
|
597
601
|
AND pf.last_ordinal = NEW.keep_from_ordinal
|
|
598
|
-
AND
|
|
602
|
+
AND (
|
|
603
|
+
t.status <> 'open' OR (
|
|
604
|
+
NOT EXISTS (SELECT 1 FROM iterations WHERE outcome = 'open') AND
|
|
605
|
+
NOT EXISTS (SELECT 1 FROM protocol_frames WHERE state = 'open')
|
|
606
|
+
)
|
|
607
|
+
)
|
|
599
608
|
) AND
|
|
600
609
|
EXISTS (
|
|
601
610
|
SELECT 1 FROM messages m
|
|
@@ -850,6 +859,170 @@ export const SESSION_SCHEMA_V9_FINGERPRINT = sha256(
|
|
|
850
859
|
}),
|
|
851
860
|
);
|
|
852
861
|
|
|
862
|
+
const PRE_SPLIT_RECALL_SCHEMA_V9_FINGERPRINT =
|
|
863
|
+
"27c61d806778689f88211b6eaa6dd9dc3a730dfd4133c862006140576b2ecd10";
|
|
864
|
+
|
|
865
|
+
const PRE_ACTIVE_TURN_RETIREMENT_SCHEMA_V9_FINGERPRINT =
|
|
866
|
+
"263a0415b343a922efc65aab4a3387a8b31de18e82245ce67b9296b7a02f4a26";
|
|
867
|
+
|
|
868
|
+
const ACTIVE_TURN_RETIREMENT_TRIGGER_FRAGMENT = `AND (
|
|
869
|
+
t.status <> 'open' OR (
|
|
870
|
+
NOT EXISTS (SELECT 1 FROM iterations WHERE outcome = 'open') AND
|
|
871
|
+
NOT EXISTS (SELECT 1 FROM protocol_frames WHERE state = 'open')
|
|
872
|
+
)
|
|
873
|
+
)`;
|
|
874
|
+
|
|
875
|
+
const preActiveTurnRetirementSchemaDefinitions = replaceSchemaDefinitionSql(
|
|
876
|
+
schemaDefinitions,
|
|
877
|
+
"trigger",
|
|
878
|
+
"context_revisions_validate_insert",
|
|
879
|
+
(sql) =>
|
|
880
|
+
replaceExactSqlFragment(
|
|
881
|
+
sql,
|
|
882
|
+
ACTIVE_TURN_RETIREMENT_TRIGGER_FRAGMENT,
|
|
883
|
+
"AND t.status <> 'open'",
|
|
884
|
+
),
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const preSplitRecallSchemaDefinitions = replaceSchemaDefinitionSql(
|
|
888
|
+
replaceSchemaDefinitionSql(
|
|
889
|
+
preActiveTurnRetirementSchemaDefinitions,
|
|
890
|
+
"view",
|
|
891
|
+
"recall_documents",
|
|
892
|
+
() => `CREATE VIEW recall_documents AS
|
|
893
|
+
SELECT rowid AS docid, content
|
|
894
|
+
FROM messages
|
|
895
|
+
WHERE role IN ('user', 'assistant', 'tool')
|
|
896
|
+
AND content IS NOT NULL
|
|
897
|
+
AND length(content) > 0
|
|
898
|
+
AND NOT (role = 'tool' AND name = 'Recall')`,
|
|
899
|
+
),
|
|
900
|
+
"trigger",
|
|
901
|
+
"messages_recall_index",
|
|
902
|
+
() => `CREATE TRIGGER messages_recall_index
|
|
903
|
+
AFTER INSERT ON messages
|
|
904
|
+
WHEN NEW.role IN ('user', 'assistant', 'tool')
|
|
905
|
+
AND NEW.content IS NOT NULL
|
|
906
|
+
AND length(NEW.content) > 0
|
|
907
|
+
AND NOT (NEW.role = 'tool' AND NEW.name = 'Recall')
|
|
908
|
+
BEGIN
|
|
909
|
+
INSERT INTO message_fts(rowid, content)
|
|
910
|
+
VALUES (NEW.rowid, NEW.content);
|
|
911
|
+
END`,
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
export function upgradeRecallIndexContract(database: Database): boolean {
|
|
915
|
+
const applicationId = Number(singlePragmaValue(database, "PRAGMA application_id"));
|
|
916
|
+
const userVersion = Number(singlePragmaValue(database, "PRAGMA user_version"));
|
|
917
|
+
if (
|
|
918
|
+
applicationId !== SESSION_APPLICATION_ID ||
|
|
919
|
+
userVersion !== SESSION_SCHEMA_VERSION
|
|
920
|
+
) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
const meta = database
|
|
924
|
+
.query(
|
|
925
|
+
`SELECT schema_version, schema_fingerprint
|
|
926
|
+
FROM session_meta WHERE singleton = 1`,
|
|
927
|
+
)
|
|
928
|
+
.get() as { schema_version: unknown; schema_fingerprint: unknown } | null;
|
|
929
|
+
if (
|
|
930
|
+
meta === null ||
|
|
931
|
+
Number(meta.schema_version) !== SESSION_SCHEMA_VERSION ||
|
|
932
|
+
meta.schema_fingerprint !== PRE_SPLIT_RECALL_SCHEMA_V9_FINGERPRINT
|
|
933
|
+
) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const recallView = schemaDefinitions.find(
|
|
937
|
+
(definition) =>
|
|
938
|
+
definition.type === "view" && definition.name === "recall_documents",
|
|
939
|
+
);
|
|
940
|
+
const recallTrigger = schemaDefinitions.find(
|
|
941
|
+
(definition) =>
|
|
942
|
+
definition.type === "trigger" && definition.name === "messages_recall_index",
|
|
943
|
+
);
|
|
944
|
+
const metadataTrigger = schemaDefinitions.find(
|
|
945
|
+
(definition) =>
|
|
946
|
+
definition.type === "trigger" &&
|
|
947
|
+
definition.name === "session_meta_monotonic_update",
|
|
948
|
+
);
|
|
949
|
+
if (
|
|
950
|
+
recallView === undefined ||
|
|
951
|
+
recallTrigger === undefined ||
|
|
952
|
+
metadataTrigger === undefined
|
|
953
|
+
) {
|
|
954
|
+
throw new Error("Recall index schema definitions are missing.");
|
|
955
|
+
}
|
|
956
|
+
database.exec("DROP TRIGGER messages_recall_index");
|
|
957
|
+
database.exec("DROP VIEW recall_documents");
|
|
958
|
+
database.exec(recallView.sql);
|
|
959
|
+
database.exec(recallTrigger.sql);
|
|
960
|
+
rebuildRecallIndex(database);
|
|
961
|
+
database.exec("DROP TRIGGER session_meta_monotonic_update");
|
|
962
|
+
database
|
|
963
|
+
.query(
|
|
964
|
+
`UPDATE session_meta SET schema_fingerprint = ?
|
|
965
|
+
WHERE singleton = 1 AND schema_fingerprint = ?`,
|
|
966
|
+
)
|
|
967
|
+
.run(
|
|
968
|
+
PRE_ACTIVE_TURN_RETIREMENT_SCHEMA_V9_FINGERPRINT,
|
|
969
|
+
PRE_SPLIT_RECALL_SCHEMA_V9_FINGERPRINT,
|
|
970
|
+
);
|
|
971
|
+
database.exec(metadataTrigger.sql);
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function upgradeActiveTurnRetirementContract(database: Database): boolean {
|
|
976
|
+
const applicationId = Number(singlePragmaValue(database, "PRAGMA application_id"));
|
|
977
|
+
const userVersion = Number(singlePragmaValue(database, "PRAGMA user_version"));
|
|
978
|
+
if (
|
|
979
|
+
applicationId !== SESSION_APPLICATION_ID ||
|
|
980
|
+
userVersion !== SESSION_SCHEMA_VERSION
|
|
981
|
+
) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
const meta = database
|
|
985
|
+
.query(
|
|
986
|
+
`SELECT schema_version, schema_fingerprint
|
|
987
|
+
FROM session_meta WHERE singleton = 1`,
|
|
988
|
+
)
|
|
989
|
+
.get() as { schema_version: unknown; schema_fingerprint: unknown } | null;
|
|
990
|
+
if (
|
|
991
|
+
meta === null ||
|
|
992
|
+
Number(meta.schema_version) !== SESSION_SCHEMA_VERSION ||
|
|
993
|
+
meta.schema_fingerprint !== PRE_ACTIVE_TURN_RETIREMENT_SCHEMA_V9_FINGERPRINT
|
|
994
|
+
) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
const revisionTrigger = schemaDefinitions.find(
|
|
998
|
+
(definition) =>
|
|
999
|
+
definition.type === "trigger" &&
|
|
1000
|
+
definition.name === "context_revisions_validate_insert",
|
|
1001
|
+
);
|
|
1002
|
+
const metadataTrigger = schemaDefinitions.find(
|
|
1003
|
+
(definition) =>
|
|
1004
|
+
definition.type === "trigger" &&
|
|
1005
|
+
definition.name === "session_meta_monotonic_update",
|
|
1006
|
+
);
|
|
1007
|
+
if (revisionTrigger === undefined || metadataTrigger === undefined) {
|
|
1008
|
+
throw new Error("Active-turn retirement schema definitions are missing.");
|
|
1009
|
+
}
|
|
1010
|
+
database.exec("DROP TRIGGER context_revisions_validate_insert");
|
|
1011
|
+
database.exec(revisionTrigger.sql);
|
|
1012
|
+
database.exec("DROP TRIGGER session_meta_monotonic_update");
|
|
1013
|
+
database
|
|
1014
|
+
.query(
|
|
1015
|
+
`UPDATE session_meta SET schema_fingerprint = ?
|
|
1016
|
+
WHERE singleton = 1 AND schema_fingerprint = ?`,
|
|
1017
|
+
)
|
|
1018
|
+
.run(
|
|
1019
|
+
SESSION_SCHEMA_V9_FINGERPRINT,
|
|
1020
|
+
PRE_ACTIVE_TURN_RETIREMENT_SCHEMA_V9_FINGERPRINT,
|
|
1021
|
+
);
|
|
1022
|
+
database.exec(metadataTrigger.sql);
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
853
1026
|
export function configureWritableDatabase(database: Database): void {
|
|
854
1027
|
database.exec("PRAGMA foreign_keys = ON");
|
|
855
1028
|
const journal = singlePragmaValue(database, "PRAGMA journal_mode = WAL");
|
|
@@ -893,7 +1066,61 @@ export function reinstallSessionCloneTriggers(database: Database): void {
|
|
|
893
1066
|
}
|
|
894
1067
|
}
|
|
895
1068
|
|
|
1069
|
+
export function verifyReadableSessionSchema(
|
|
1070
|
+
database: Database,
|
|
1071
|
+
sessionId?: SessionId,
|
|
1072
|
+
): "current" | "migratable" {
|
|
1073
|
+
verifySessionSchemaVersion(database, sessionId);
|
|
1074
|
+
const meta = database
|
|
1075
|
+
.query(
|
|
1076
|
+
`SELECT schema_version, schema_fingerprint
|
|
1077
|
+
FROM session_meta WHERE singleton = 1`,
|
|
1078
|
+
)
|
|
1079
|
+
.get() as { schema_version: unknown; schema_fingerprint: unknown } | null;
|
|
1080
|
+
if (
|
|
1081
|
+
meta === null ||
|
|
1082
|
+
Number(meta.schema_version) !== SESSION_SCHEMA_VERSION ||
|
|
1083
|
+
typeof meta.schema_fingerprint !== "string"
|
|
1084
|
+
) {
|
|
1085
|
+
throw new SessionError(
|
|
1086
|
+
"SESSION_SCHEMA_INVALID",
|
|
1087
|
+
"verify_readable_schema",
|
|
1088
|
+
"Session schema metadata is invalid.",
|
|
1089
|
+
{ sessionId },
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
let expectedDefinitions: readonly SchemaDefinition[];
|
|
1094
|
+
let compatibility: "current" | "migratable";
|
|
1095
|
+
if (meta.schema_fingerprint === SESSION_SCHEMA_V9_FINGERPRINT) {
|
|
1096
|
+
expectedDefinitions = schemaDefinitions;
|
|
1097
|
+
compatibility = "current";
|
|
1098
|
+
} else if (
|
|
1099
|
+
meta.schema_fingerprint === PRE_ACTIVE_TURN_RETIREMENT_SCHEMA_V9_FINGERPRINT
|
|
1100
|
+
) {
|
|
1101
|
+
expectedDefinitions = preActiveTurnRetirementSchemaDefinitions;
|
|
1102
|
+
compatibility = "migratable";
|
|
1103
|
+
} else if (meta.schema_fingerprint === PRE_SPLIT_RECALL_SCHEMA_V9_FINGERPRINT) {
|
|
1104
|
+
expectedDefinitions = preSplitRecallSchemaDefinitions;
|
|
1105
|
+
compatibility = "migratable";
|
|
1106
|
+
} else {
|
|
1107
|
+
throw new SessionError(
|
|
1108
|
+
"SESSION_SCHEMA_INVALID",
|
|
1109
|
+
"verify_readable_schema",
|
|
1110
|
+
"Session schema is neither current nor a recognized migration source.",
|
|
1111
|
+
{ sessionId },
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
verifySessionSchemaDefinitions(database, expectedDefinitions, sessionId);
|
|
1115
|
+
return compatibility;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
896
1118
|
export function verifySessionSchema(database: Database, sessionId?: SessionId): void {
|
|
1119
|
+
verifySessionSchemaVersion(database, sessionId);
|
|
1120
|
+
verifySessionSchemaDefinitions(database, schemaDefinitions, sessionId);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function verifySessionSchemaVersion(database: Database, sessionId?: SessionId): void {
|
|
897
1124
|
const applicationId = Number(singlePragmaValue(database, "PRAGMA application_id"));
|
|
898
1125
|
const userVersion = Number(singlePragmaValue(database, "PRAGMA user_version"));
|
|
899
1126
|
if (
|
|
@@ -907,7 +1134,13 @@ export function verifySessionSchema(database: Database, sessionId?: SessionId):
|
|
|
907
1134
|
{ sessionId },
|
|
908
1135
|
);
|
|
909
1136
|
}
|
|
1137
|
+
}
|
|
910
1138
|
|
|
1139
|
+
function verifySessionSchemaDefinitions(
|
|
1140
|
+
database: Database,
|
|
1141
|
+
expectedDefinitions: readonly SchemaDefinition[],
|
|
1142
|
+
sessionId?: SessionId,
|
|
1143
|
+
): void {
|
|
911
1144
|
const actual = database
|
|
912
1145
|
.query(
|
|
913
1146
|
`SELECT type, name, sql FROM sqlite_schema
|
|
@@ -918,7 +1151,7 @@ export function verifySessionSchema(database: Database, sessionId?: SessionId):
|
|
|
918
1151
|
const actualByName = new Map(
|
|
919
1152
|
actual.map((entry) => [`${entry.type}:${entry.name}`, normalizeSql(entry.sql)]),
|
|
920
1153
|
);
|
|
921
|
-
for (const expected of
|
|
1154
|
+
for (const expected of expectedDefinitions) {
|
|
922
1155
|
const actualSql = actualByName.get(`${expected.type}:${expected.name}`);
|
|
923
1156
|
if (actualSql !== normalizeSql(expected.sql)) {
|
|
924
1157
|
throw new SessionError(
|
|
@@ -1053,3 +1286,34 @@ function singlePragmaValue(database: Database, sql: string): unknown {
|
|
|
1053
1286
|
function normalizeSql(sql: string): string {
|
|
1054
1287
|
return sql.replace(/\s+/g, " ").trim().replace(/;$/, "").toLowerCase();
|
|
1055
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
function replaceSchemaDefinitionSql(
|
|
1291
|
+
definitions: readonly SchemaDefinition[],
|
|
1292
|
+
type: SchemaDefinition["type"],
|
|
1293
|
+
name: string,
|
|
1294
|
+
replace: (sql: string) => string,
|
|
1295
|
+
): readonly SchemaDefinition[] {
|
|
1296
|
+
const target = definitions.find(
|
|
1297
|
+
(definition) => definition.type === type && definition.name === name,
|
|
1298
|
+
);
|
|
1299
|
+
if (target === undefined) {
|
|
1300
|
+
throw new Error(`Schema definition is missing: ${type} ${name}.`);
|
|
1301
|
+
}
|
|
1302
|
+
return definitions.map((definition) =>
|
|
1303
|
+
definition === target
|
|
1304
|
+
? { ...definition, sql: replace(definition.sql) }
|
|
1305
|
+
: definition,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function replaceExactSqlFragment(
|
|
1310
|
+
sql: string,
|
|
1311
|
+
currentFragment: string,
|
|
1312
|
+
previousFragment: string,
|
|
1313
|
+
): string {
|
|
1314
|
+
const replaced = sql.replace(currentFragment, previousFragment);
|
|
1315
|
+
if (replaced === sql) {
|
|
1316
|
+
throw new Error("Schema definition does not contain its historical fragment.");
|
|
1317
|
+
}
|
|
1318
|
+
return replaced;
|
|
1319
|
+
}
|
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
createModelContextProfile,
|
|
29
29
|
type ModelContextProfile,
|
|
30
30
|
} from "../model/model-context-profile";
|
|
31
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
33
|
+
type ModelMessageProtocol,
|
|
34
|
+
} from "../model/model-client";
|
|
32
35
|
import type { InputTokenEstimatorCompatibility } from "../model/input-token-estimator";
|
|
33
36
|
import type { ToolDefinition, ToolRawResult } from "../tools/types";
|
|
34
37
|
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
@@ -45,7 +48,6 @@ import {
|
|
|
45
48
|
ContextRevisionCompiler,
|
|
46
49
|
createInitialContextRevision,
|
|
47
50
|
} from "../context/context-revision-compiler";
|
|
48
|
-
import { recallFirstRetirementPolicyV1 } from "../context/context-policy";
|
|
49
51
|
import {
|
|
50
52
|
ContextSwapRenderer,
|
|
51
53
|
SWAP_OBSERVATION_FORMAT,
|
|
@@ -90,7 +92,10 @@ import { ImageAssetStore } from "../image/image-asset-store";
|
|
|
90
92
|
import type { MeasuredContextAnchor } from "../agent/context-meter";
|
|
91
93
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
92
94
|
import { SUPPORTED_RECALL_RETIREMENT_CONTRACT_VERSIONS } from "../context/recall-retirement-contract";
|
|
93
|
-
import type {
|
|
95
|
+
import type {
|
|
96
|
+
ActiveTurnBoundary,
|
|
97
|
+
ClosedTurnBoundary,
|
|
98
|
+
} from "../context/prefix-retirement-planner";
|
|
94
99
|
import {
|
|
95
100
|
AdmissionStaleError,
|
|
96
101
|
InMemorySessionLedger,
|
|
@@ -106,11 +111,14 @@ import { SessionLease } from "./session-lock";
|
|
|
106
111
|
import {
|
|
107
112
|
SESSION_SCHEMA_V9_FINGERPRINT,
|
|
108
113
|
SESSION_SCHEMA_VERSION,
|
|
114
|
+
upgradeActiveTurnRetirementContract,
|
|
115
|
+
upgradeRecallIndexContract,
|
|
109
116
|
configureWritableDatabase,
|
|
110
117
|
createSessionSchema,
|
|
111
118
|
dropSessionCloneTriggers,
|
|
112
119
|
rebuildRecallIndex,
|
|
113
120
|
reinstallSessionCloneTriggers,
|
|
121
|
+
verifyReadableSessionSchema,
|
|
114
122
|
verifyRecallIndex,
|
|
115
123
|
verifySessionSchema,
|
|
116
124
|
verifySqliteIntegrity,
|
|
@@ -231,6 +239,7 @@ export type CommitSwapRevisionInput = {
|
|
|
231
239
|
nextActiveOverrideManifestSha256: string;
|
|
232
240
|
canonicalSequenceSha256: string;
|
|
233
241
|
renderedMessageSha256: string;
|
|
242
|
+
activeTurnId?: TurnId;
|
|
234
243
|
};
|
|
235
244
|
|
|
236
245
|
export type CommitSwapRevisionFaultStage =
|
|
@@ -288,6 +297,7 @@ export type CommitPrefixRetirementRevisionInput = {
|
|
|
288
297
|
nextActiveOverrideManifestSha256: string;
|
|
289
298
|
canonicalSequenceSha256: string;
|
|
290
299
|
renderedMessageSha256: string;
|
|
300
|
+
activeTurnId?: TurnId;
|
|
291
301
|
};
|
|
292
302
|
|
|
293
303
|
export type CommitPrefixRetirementRevisionFaultStage =
|
|
@@ -564,8 +574,13 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
564
574
|
let database: Database | undefined;
|
|
565
575
|
try {
|
|
566
576
|
database = openWritableDatabase(databasePath);
|
|
567
|
-
verifySessionSchema(database, input.sessionId);
|
|
568
577
|
verifySqliteIntegrity(database, input.sessionId);
|
|
578
|
+
verifyReadableSessionSchema(database, input.sessionId);
|
|
579
|
+
const recallIndexContractUpgraded = runTransaction(database, () =>
|
|
580
|
+
upgradeRecallIndexContract(database!),
|
|
581
|
+
);
|
|
582
|
+
runTransaction(database, () => upgradeActiveTurnRetirementContract(database!));
|
|
583
|
+
verifySessionSchema(database, input.sessionId);
|
|
569
584
|
const store = new SessionStore(database, lease, {
|
|
570
585
|
sessionId: input.sessionId,
|
|
571
586
|
workspaceRoot,
|
|
@@ -573,6 +588,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
573
588
|
databasePath,
|
|
574
589
|
clock,
|
|
575
590
|
});
|
|
591
|
+
store.recallIndexRebuilt = recallIndexContractUpgraded;
|
|
576
592
|
const meta = store.readMeta();
|
|
577
593
|
if (meta.initializationState !== "ready" && input.allowIncomplete !== true) {
|
|
578
594
|
throw new SessionError(
|
|
@@ -644,6 +660,9 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
644
660
|
case "begin_turn":
|
|
645
661
|
this.commitBeginTurn(mutation, now);
|
|
646
662
|
break;
|
|
663
|
+
case "append_steering_users":
|
|
664
|
+
this.commitSteeringUsers(mutation, now);
|
|
665
|
+
break;
|
|
647
666
|
case "append_assistant":
|
|
648
667
|
this.commitAssistant(mutation, now);
|
|
649
668
|
break;
|
|
@@ -926,25 +945,38 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
926
945
|
}
|
|
927
946
|
|
|
928
947
|
assertContextRevisionIdle(): void {
|
|
948
|
+
this.assertContextRevisionBoundary();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
assertContextRevisionBoundary(activeTurnId?: TurnId): void {
|
|
929
952
|
this.requireOpen();
|
|
930
953
|
const row = this.database
|
|
931
954
|
.query(
|
|
932
955
|
`SELECT
|
|
933
956
|
(SELECT COUNT(*) FROM turns WHERE status = 'open') AS open_turns,
|
|
957
|
+
(SELECT turn_id FROM turns WHERE status = 'open' LIMIT 1) AS open_turn_id,
|
|
934
958
|
(SELECT COUNT(*) FROM iterations WHERE outcome = 'open') AS open_iterations,
|
|
935
959
|
(SELECT COUNT(*) FROM protocol_frames WHERE state = 'open') AS open_frames`,
|
|
936
960
|
)
|
|
937
961
|
.get() as Record<string, unknown> | null;
|
|
962
|
+
const openTurnCount =
|
|
963
|
+
row === null ? -1 : numberFromSql(row.open_turns, "open_turns");
|
|
964
|
+
const openTurnId =
|
|
965
|
+
row === null
|
|
966
|
+
? null
|
|
967
|
+
: (nullableStringFromSql(row.open_turn_id, "open_turn_id") as TurnId | null);
|
|
938
968
|
if (
|
|
939
969
|
row === null ||
|
|
940
|
-
|
|
970
|
+
(activeTurnId === undefined
|
|
971
|
+
? openTurnCount !== 0
|
|
972
|
+
: openTurnCount !== 1 || openTurnId !== activeTurnId) ||
|
|
941
973
|
numberFromSql(row.open_iterations, "open_iterations") !== 0 ||
|
|
942
974
|
numberFromSql(row.open_frames, "open_frames") !== 0
|
|
943
975
|
) {
|
|
944
976
|
throw new SessionError(
|
|
945
977
|
"SESSION_INTEGRITY_FAILED",
|
|
946
|
-
"
|
|
947
|
-
"Context revision requires a
|
|
978
|
+
"assert_context_revision_boundary",
|
|
979
|
+
"Context revision requires an idle store or a closed active-turn iteration boundary.",
|
|
948
980
|
{ sessionId: this.sessionId },
|
|
949
981
|
);
|
|
950
982
|
}
|
|
@@ -955,16 +987,35 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
955
987
|
this.assertContextRevisionIdle();
|
|
956
988
|
const canonical = this.loadProtocolView();
|
|
957
989
|
this.validator.validate(canonical, { fullIntegrity: true });
|
|
958
|
-
return this.
|
|
990
|
+
return this.readRetirementBoundaries(canonical).closedTurns;
|
|
959
991
|
}
|
|
960
992
|
|
|
961
|
-
|
|
993
|
+
loadRetirementBoundaries(activeTurnId?: TurnId): {
|
|
994
|
+
readonly closedTurns: readonly ClosedTurnBoundary[];
|
|
995
|
+
readonly activeTurn?: ActiveTurnBoundary;
|
|
996
|
+
} {
|
|
997
|
+
this.requireOpen();
|
|
998
|
+
this.assertContextRevisionBoundary(activeTurnId);
|
|
999
|
+
const canonical = this.loadProtocolView();
|
|
1000
|
+
this.validator.validate(canonical, {
|
|
1001
|
+
allowOpenTail: activeTurnId !== undefined,
|
|
1002
|
+
fullIntegrity: true,
|
|
1003
|
+
});
|
|
1004
|
+
return this.readRetirementBoundaries(canonical, activeTurnId);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private readRetirementBoundaries(
|
|
962
1008
|
canonical: ProtocolContextView,
|
|
963
|
-
|
|
1009
|
+
activeTurnId?: TurnId,
|
|
1010
|
+
): {
|
|
1011
|
+
readonly closedTurns: readonly ClosedTurnBoundary[];
|
|
1012
|
+
readonly activeTurn?: ActiveTurnBoundary;
|
|
1013
|
+
} {
|
|
964
1014
|
const rows = this.database
|
|
965
1015
|
.query("SELECT * FROM turns ORDER BY turn_number")
|
|
966
1016
|
.all() as Array<Record<string, unknown>>;
|
|
967
1017
|
const boundaries: ClosedTurnBoundary[] = [];
|
|
1018
|
+
let activeTurn: ActiveTurnBoundary | undefined;
|
|
968
1019
|
let expectedOrdinal = 2;
|
|
969
1020
|
for (let index = 0; index < rows.length; index += 1) {
|
|
970
1021
|
const row = requireItem(rows, index, "turn row");
|
|
@@ -972,8 +1023,8 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
972
1023
|
const turnNumber = numberFromSql(row.turn_number, "turn_number");
|
|
973
1024
|
const status = enumFromSql(
|
|
974
1025
|
row.status,
|
|
975
|
-
["completed", "failed", "cancelled", "interrupted"] as const,
|
|
976
|
-
"
|
|
1026
|
+
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
1027
|
+
"turn status",
|
|
977
1028
|
);
|
|
978
1029
|
const frames = canonical.frames.filter((frame) => frame.turnId === turnId);
|
|
979
1030
|
const messages = canonical.messages.filter(
|
|
@@ -981,6 +1032,29 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
981
1032
|
);
|
|
982
1033
|
const firstMessage = messages[0];
|
|
983
1034
|
const lastMessage = messages.at(-1);
|
|
1035
|
+
if (status === "open") {
|
|
1036
|
+
if (
|
|
1037
|
+
activeTurnId === undefined ||
|
|
1038
|
+
turnId !== activeTurnId ||
|
|
1039
|
+
index !== rows.length - 1 ||
|
|
1040
|
+
turnNumber !== index + 1 ||
|
|
1041
|
+
messages.length < 1 ||
|
|
1042
|
+
frames.length < 1 ||
|
|
1043
|
+
firstMessage?.role !== "user" ||
|
|
1044
|
+
firstMessage.ordinal !== expectedOrdinal ||
|
|
1045
|
+
lastMessage?.ordinal !== canonical.messages.length ||
|
|
1046
|
+
frames.some((frame) => frame.state !== "closed")
|
|
1047
|
+
) {
|
|
1048
|
+
throw new Error(`Turn ${turnId} has an invalid active boundary.`);
|
|
1049
|
+
}
|
|
1050
|
+
activeTurn = Object.freeze({
|
|
1051
|
+
turnId,
|
|
1052
|
+
turnNumber,
|
|
1053
|
+
firstOrdinal: expectedOrdinal,
|
|
1054
|
+
});
|
|
1055
|
+
expectedOrdinal = canonical.messages.length + 1;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
984
1058
|
let nextFrameOrdinal = expectedOrdinal;
|
|
985
1059
|
for (const frame of frames) {
|
|
986
1060
|
if (
|
|
@@ -1019,7 +1093,13 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1019
1093
|
if (expectedOrdinal !== canonical.messages.length + 1) {
|
|
1020
1094
|
throw new Error("Closed turn boundaries do not cover canonical history.");
|
|
1021
1095
|
}
|
|
1022
|
-
|
|
1096
|
+
if ((activeTurnId === undefined) !== (activeTurn === undefined)) {
|
|
1097
|
+
throw new Error("Active retirement boundary does not match the open turn.");
|
|
1098
|
+
}
|
|
1099
|
+
return Object.freeze({
|
|
1100
|
+
closedTurns: Object.freeze(boundaries),
|
|
1101
|
+
...(activeTurn === undefined ? {} : { activeTurn }),
|
|
1102
|
+
});
|
|
1023
1103
|
}
|
|
1024
1104
|
|
|
1025
1105
|
commitSwapRevision(
|
|
@@ -1043,7 +1123,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1043
1123
|
) {
|
|
1044
1124
|
throw new Error("Context revision commit base is stale.");
|
|
1045
1125
|
}
|
|
1046
|
-
this.
|
|
1126
|
+
this.assertContextRevisionBoundary(input.activeTurnId);
|
|
1047
1127
|
|
|
1048
1128
|
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1049
1129
|
const candidateOverrides = [
|
|
@@ -1229,25 +1309,27 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1229
1309
|
) {
|
|
1230
1310
|
throw new Error("Prefix retirement commit base is stale.");
|
|
1231
1311
|
}
|
|
1232
|
-
this.
|
|
1233
|
-
const
|
|
1312
|
+
this.assertContextRevisionBoundary(input.activeTurnId);
|
|
1313
|
+
const retirementBoundaries = this.readRetirementBoundaries(
|
|
1314
|
+
snapshot.canonical,
|
|
1315
|
+
input.activeTurnId,
|
|
1316
|
+
);
|
|
1317
|
+
const closedTurns = retirementBoundaries.closedTurns;
|
|
1234
1318
|
const activeTurns = closedTurns.filter(
|
|
1235
1319
|
(turn) => turn.firstOrdinal >= baseRevision.keepFromOrdinal,
|
|
1236
1320
|
);
|
|
1237
|
-
const nextBoundary =
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
);
|
|
1321
|
+
const nextBoundary = [
|
|
1322
|
+
...activeTurns,
|
|
1323
|
+
...(retirementBoundaries.activeTurn === undefined
|
|
1324
|
+
? []
|
|
1325
|
+
: [retirementBoundaries.activeTurn]),
|
|
1326
|
+
].find((turn) => turn.firstOrdinal === input.nextKeepFromOrdinal);
|
|
1243
1327
|
const retiredTurns = activeTurns.filter(
|
|
1244
1328
|
(turn) => turn.lastOrdinal < input.nextKeepFromOrdinal,
|
|
1245
1329
|
);
|
|
1246
1330
|
if (
|
|
1247
1331
|
nextBoundary === undefined ||
|
|
1248
1332
|
input.nextKeepFromOrdinal <= baseRevision.keepFromOrdinal ||
|
|
1249
|
-
retainedTurns.length <
|
|
1250
|
-
recallFirstRetirementPolicyV1.protectedRecentTurnCount ||
|
|
1251
1333
|
retiredTurns.length !== input.retiredTurnCount ||
|
|
1252
1334
|
retiredTurns.reduce((total, turn) => total + turn.frameCount, 0) !==
|
|
1253
1335
|
input.retiredFrameCount ||
|
|
@@ -2769,6 +2851,32 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
2769
2851
|
requireSingleChange(this.database, updated.changes, "advance turn counter");
|
|
2770
2852
|
}
|
|
2771
2853
|
|
|
2854
|
+
private commitSteeringUsers(
|
|
2855
|
+
mutation: Extract<LedgerMutation, { kind: "append_steering_users" }>,
|
|
2856
|
+
now: string,
|
|
2857
|
+
): void {
|
|
2858
|
+
const turn = this.requireTurnRow(mutation.turn.turnId);
|
|
2859
|
+
if (turn.status !== "open") {
|
|
2860
|
+
throw new Error(`Turn ${mutation.turn.turnId} is not open.`);
|
|
2861
|
+
}
|
|
2862
|
+
if (
|
|
2863
|
+
mutation.frames.length === 0 ||
|
|
2864
|
+
mutation.frames.length !== mutation.messages.length
|
|
2865
|
+
) {
|
|
2866
|
+
throw new Error(
|
|
2867
|
+
"Steering user mutation must contain matching frames and messages.",
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
for (let index = 0; index < mutation.frames.length; index += 1) {
|
|
2871
|
+
insertFrame(this.database, requireItem(mutation.frames, index, "steering frame"));
|
|
2872
|
+
insertMessage(
|
|
2873
|
+
this.database,
|
|
2874
|
+
requireItem(mutation.messages, index, "steering message"),
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
this.touch(now);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2772
2880
|
private commitAssistant(
|
|
2773
2881
|
mutation: Extract<LedgerMutation, { kind: "append_assistant" }>,
|
|
2774
2882
|
now: string,
|
|
@@ -3608,7 +3716,7 @@ export function createSessionCompatibilityContract(input: {
|
|
|
3608
3716
|
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
3609
3717
|
}
|
|
3610
3718
|
if (
|
|
3611
|
-
!
|
|
3719
|
+
!MODEL_MESSAGE_PROTOCOL_ADAPTERS.includes(input.messageProtocol.adapter) ||
|
|
3612
3720
|
input.messageProtocol.serializationVersion.trim() === ""
|
|
3613
3721
|
) {
|
|
3614
3722
|
throw new Error("Session compatibility message protocol is invalid.");
|
|
@@ -5473,7 +5581,7 @@ function decodeSessionCompatibilityContract(
|
|
|
5473
5581
|
const protocol: ModelMessageProtocol = Object.freeze({
|
|
5474
5582
|
adapter: enumFromSql(
|
|
5475
5583
|
messageProtocol.adapter,
|
|
5476
|
-
|
|
5584
|
+
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
5477
5585
|
"compatibility message adapter",
|
|
5478
5586
|
),
|
|
5479
5587
|
serializationVersion: stringFromSql(
|
|
@@ -298,7 +298,7 @@ export function renderSkillActivationReceipt(input: {
|
|
|
298
298
|
`source=${source}`,
|
|
299
299
|
"activation=The full historical instructions were promoted out of this tool observation.",
|
|
300
300
|
"current=Consult the current active_agent_skills system section; absence means inactive.",
|
|
301
|
-
"historical=Use
|
|
301
|
+
"historical=Use RecallGet with source to recover the original activation observation.",
|
|
302
302
|
].join("\n")
|
|
303
303
|
: [
|
|
304
304
|
"[Tinker Agent Skill activation rejected]",
|
|
@@ -306,7 +306,7 @@ export function renderSkillActivationReceipt(input: {
|
|
|
306
306
|
`source=${source}`,
|
|
307
307
|
`status=${input.outcome === "unavailable" ? "unavailable" : "not_dispatched"}`,
|
|
308
308
|
"current=This skill was not added to the active system surface.",
|
|
309
|
-
"historical=Use
|
|
309
|
+
"historical=Use RecallGet with source to recover the original activation observation.",
|
|
310
310
|
].join("\n");
|
|
311
311
|
const originalBytes = Buffer.byteLength(input.message.content, "utf8");
|
|
312
312
|
const renderedBytes = Buffer.byteLength(renderedContent, "utf8");
|