@sentry/junior 0.108.0 → 0.110.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/dist/{agent-hooks-NU5HK3PS.js → agent-hooks-ELT4ODFF.js} +12 -13
- package/dist/api/conversations/access.d.ts +9 -0
- package/dist/api/conversations/aggregate.d.ts +12 -1
- package/dist/api/conversations/archive.d.ts +3 -5
- package/dist/api/conversations/detail.d.ts +156 -7
- package/dist/api/conversations/events.d.ts +2 -2
- package/dist/api/conversations/list.d.ts +57 -6
- package/dist/api/conversations/projection.d.ts +6 -7
- package/dist/api/conversations/reporting.d.ts +10 -4
- package/dist/api/conversations/routes.d.ts +2 -1
- package/dist/api/conversations/shared.d.ts +1 -1
- package/dist/api/conversations/stats.d.ts +37 -6
- package/dist/api/conversations/stats.query.d.ts +1 -1
- package/dist/api/conversations/usage.d.ts +9 -0
- package/dist/api/http.d.ts +7 -0
- package/dist/api/locations/detail.d.ts +102 -6
- package/dist/api/locations/list.d.ts +43 -5
- package/dist/api/locations/query.d.ts +4 -2
- package/dist/api/locations/routes.d.ts +2 -1
- package/dist/api/people/list.d.ts +28 -6
- package/dist/api/people/list.query.d.ts +1 -1
- package/dist/api/people/profile.d.ts +96 -7
- package/dist/api/people/profile.query.d.ts +4 -2
- package/dist/api/people/routes.d.ts +2 -1
- package/dist/api/people/shared.d.ts +1219 -2
- package/dist/api/route.d.ts +18 -4
- package/dist/api/schema/common.d.ts +6 -0
- package/dist/api/{conversations/schema.d.ts → schema/conversation.d.ts} +88 -9
- package/dist/api/{locations/schema.d.ts → schema/location.d.ts} +5 -0
- package/dist/api/{people/schema.d.ts → schema/person.d.ts} +37 -45
- package/dist/api/schema.d.ts +8 -22
- package/dist/api/schema.js +10 -2
- package/dist/api.d.ts +5 -1
- package/dist/api.js +838 -369
- package/dist/app.d.ts +1 -0
- package/dist/app.js +1564 -486
- package/dist/catalog-runtime-4GI22PXK.js +16 -0
- package/dist/chat/agent/resume.d.ts +11 -12
- package/dist/chat/conversations/history.d.ts +3 -0
- package/dist/chat/conversations/sql/privacy.d.ts +3 -15
- package/dist/chat/conversations/sql/store.d.ts +0 -7
- package/dist/chat/log-context.d.ts +40 -0
- package/dist/chat/logging.d.ts +3 -26
- package/dist/chat/pi/messages.d.ts +2 -2
- package/dist/chat/pi/sql-model-usage.d.ts +4 -1
- package/dist/chat/runtime/slack-resume.d.ts +2 -2
- package/dist/chat/services/auth-pause.d.ts +4 -0
- package/dist/chat/services/context-compaction.d.ts +1 -0
- package/dist/chat/services/turn-session-record.d.ts +0 -2
- package/dist/chat/slack/reply.d.ts +7 -19
- package/dist/chat/state/conversation.d.ts +1 -0
- package/dist/chat/task-execution/queue-signing.d.ts +10 -8
- package/dist/chat/task-execution/queue.d.ts +9 -7
- package/dist/chat/task-execution/slack-work.d.ts +6 -6
- package/dist/{chunk-UD6THJ2I.js → chunk-2T7TIEXI.js} +197 -12
- package/dist/{chunk-CEA3A3UA.js → chunk-2ZTTXTSZ.js} +24 -1
- package/dist/{chunk-XKB7LGIW.js → chunk-3S7YYHBI.js} +3 -139
- package/dist/chunk-3TV7OEHL.js +10 -0
- package/dist/{chunk-A5CO2EHL.js → chunk-3UBQI73J.js} +11 -15
- package/dist/{chunk-7FBGKXPE.js → chunk-42WRQLJ6.js} +10 -33
- package/dist/{chunk-TE4QHJH4.js → chunk-4QU44P7Q.js} +135 -595
- package/dist/{chunk-RT5BUHR5.js → chunk-6Y5L4BOM.js} +3 -5
- package/dist/{chunk-VIXFG2VC.js → chunk-C4MZ3GP5.js} +1 -2
- package/dist/{chunk-3RGQLX2F.js → chunk-C5XAFTHY.js} +1011 -29
- package/dist/{chunk-PUAMO4PI.js → chunk-FPK7XY3F.js} +139 -99
- package/dist/{chunk-H3QYZL7K.js → chunk-GK47NBDE.js} +28 -3
- package/dist/{chunk-O24R4DCK.js → chunk-IYKPU7H6.js} +94 -56
- package/dist/chunk-JQFJO5WH.js +477 -0
- package/dist/{chunk-VH6KWKG2.js → chunk-L5XQPJ7T.js} +3 -3
- package/dist/{chunk-2XXEKR44.js → chunk-MIAKQNYX.js} +43 -31
- package/dist/{chunk-KPMPQ6AA.js → chunk-RHHRKSPI.js} +249 -223
- package/dist/{chunk-YT72TOEK.js → chunk-SBOBTS27.js} +2 -2
- package/dist/{chunk-B5I5LMSP.js → chunk-W6RJZG7F.js} +5 -7
- package/dist/chunk-W75ZPAGG.js +44 -0
- package/dist/{chunk-SS67LUOK.js → chunk-WNOVRQ67.js} +6 -7
- package/dist/{chunk-YIPD3L6L.js → chunk-Y3H2DN6F.js} +8 -10
- package/dist/{chunk-DY7TRYB5.js → chunk-Y5FK3CNG.js} +1 -1
- package/dist/chunk-ZKIOWEFL.js +125 -0
- package/dist/cli/chat.js +23 -24
- package/dist/cli/check.js +9 -7
- package/dist/cli/plugins.js +16 -17
- package/dist/cli/snapshot-warmup.js +11 -10
- package/dist/cli/upgrade.js +314 -3505
- package/dist/db/db.d.ts +1 -1
- package/dist/db/schema/conversations.d.ts +17 -0
- package/dist/db/schema.d.ts +17 -0
- package/dist/{db-AMRBAT5D.js → db-Y6W6CWWW.js} +8 -7
- package/dist/instrumentation.js +4 -3
- package/dist/nitro.js +4 -4
- package/dist/reporting-schema.d.ts +46 -0
- package/dist/{runner-TQH5GAJ4.js → runner-BBFV5GLR.js} +17 -21
- package/dist/{skills-ITNHBARB.js → skills-AR4AF32L.js} +8 -6
- package/dist/{validation-AGQ46ZGV.js → validation-FBIEPTC2.js} +8 -6
- package/dist/version.d.ts +2 -0
- package/dist/version.js +7 -0
- package/migrations/0006_drop_legacy_conversation_messages.sql +1 -0
- package/migrations/0007_thick_sugar_man.sql +23 -0
- package/migrations/README.md +12 -9
- package/migrations/meta/0006_snapshot.json +948 -0
- package/migrations/meta/0007_snapshot.json +1013 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +9 -5
- package/dist/catalog-runtime-FOFNPR3P.js +0 -14
- package/dist/chunk-AHJR2IFF.js +0 -115
- package/dist/chunk-B7PUBQQM.js +0 -53
- package/dist/chunk-G3E7SCME.js +0 -28
- package/dist/chunk-J3B3FPP2.js +0 -1061
- package/dist/chunk-OB42YVAE.js +0 -16
- package/dist/chunk-TWINAEZQ.js +0 -1283
package/dist/cli/upgrade.js
CHANGED
|
@@ -1,3550 +1,353 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getConversation,
|
|
3
|
-
listConversationsByActivity,
|
|
4
|
-
recordConversationActivity,
|
|
5
|
-
recordConversationExecution,
|
|
6
|
-
requestConversationWork
|
|
7
|
-
} from "../chunk-TWINAEZQ.js";
|
|
8
1
|
import {
|
|
9
2
|
defineJuniorPlugins,
|
|
10
3
|
pluginCatalogConfigFromEnv,
|
|
11
|
-
pluginCatalogConfigFromPluginSet
|
|
12
|
-
pluginRuntimeRegistrationsFromPluginSet
|
|
4
|
+
pluginCatalogConfigFromPluginSet
|
|
13
5
|
} from "../chunk-SG5WAA7H.js";
|
|
14
|
-
import {
|
|
15
|
-
AGENT_TURN_SESSION_PREFIX,
|
|
16
|
-
JUNIOR_THREAD_STATE_TTL_MS,
|
|
17
|
-
agentTurnSessionConversationIndexKey,
|
|
18
|
-
agentTurnSessionKey,
|
|
19
|
-
coerceThreadConversationState,
|
|
20
|
-
listAgentTurnSessionSummariesForConversations,
|
|
21
|
-
piMessageSchema
|
|
22
|
-
} from "../chunk-J3B3FPP2.js";
|
|
23
|
-
import {
|
|
24
|
-
addAgentTurnUsage
|
|
25
|
-
} from "../chunk-MU6HHZEN.js";
|
|
26
|
-
import "../chunk-PDO5BLNM.js";
|
|
27
6
|
import {
|
|
28
7
|
loadAppPluginSet
|
|
29
8
|
} from "../chunk-Y2CM7HXH.js";
|
|
30
9
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "../chunk-AHJR2IFF.js";
|
|
34
|
-
import {
|
|
35
|
-
disconnectStateAdapter,
|
|
36
|
-
getConnectedStateContext,
|
|
37
|
-
getStateAdapter
|
|
38
|
-
} from "../chunk-H3QYZL7K.js";
|
|
10
|
+
createJuniorSqlExecutor
|
|
11
|
+
} from "../chunk-JQFJO5WH.js";
|
|
39
12
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
conversationMessageProvenanceSchema,
|
|
43
|
-
createJuniorSqlExecutor,
|
|
44
|
-
createSqlStore,
|
|
45
|
-
getDb,
|
|
46
|
-
instructionProvenanceFor,
|
|
47
|
-
juniorConversationEvents,
|
|
48
|
-
juniorConversations,
|
|
49
|
-
newConversationEventSchema,
|
|
50
|
-
sanitizePostgresJson,
|
|
51
|
-
withConversationEventLock
|
|
52
|
-
} from "../chunk-TE4QHJH4.js";
|
|
53
|
-
import {
|
|
54
|
-
COMPACTION_SUMMARY_PREFIX,
|
|
55
|
-
getChatConfig,
|
|
56
|
-
isCompactionSummaryText,
|
|
57
|
-
modelIdForProfile,
|
|
58
|
-
modelProfileSchema,
|
|
59
|
-
standardModelId,
|
|
60
|
-
unescapeXml,
|
|
61
|
-
unwrapCurrentInstruction
|
|
62
|
-
} from "../chunk-SS67LUOK.js";
|
|
13
|
+
getChatConfig
|
|
14
|
+
} from "../chunk-WNOVRQ67.js";
|
|
63
15
|
import "../chunk-VFUK3X5B.js";
|
|
64
|
-
import "../chunk-
|
|
65
|
-
import {
|
|
66
|
-
parseDestination,
|
|
67
|
-
sameDestination
|
|
68
|
-
} from "../chunk-B7PUBQQM.js";
|
|
69
|
-
import {
|
|
70
|
-
pluginCatalogRuntime
|
|
71
|
-
} from "../chunk-YIPD3L6L.js";
|
|
72
|
-
import {
|
|
73
|
-
storedSlackActorSchema
|
|
74
|
-
} from "../chunk-VIXFG2VC.js";
|
|
75
|
-
import "../chunk-O24R4DCK.js";
|
|
16
|
+
import "../chunk-6Y5L4BOM.js";
|
|
76
17
|
import {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
18
|
+
createPluginCatalogRuntime
|
|
19
|
+
} from "../chunk-Y3H2DN6F.js";
|
|
20
|
+
import "../chunk-C4MZ3GP5.js";
|
|
21
|
+
import "../chunk-IYKPU7H6.js";
|
|
22
|
+
import "../chunk-42WRQLJ6.js";
|
|
81
23
|
import "../chunk-6GWA276C.js";
|
|
82
24
|
import "../chunk-B3IT5END.js";
|
|
83
|
-
import "../chunk-7FBGKXPE.js";
|
|
84
25
|
import "../chunk-3V7OFJZC.js";
|
|
26
|
+
import "../chunk-W75ZPAGG.js";
|
|
27
|
+
import "../chunk-ZKIOWEFL.js";
|
|
85
28
|
import "../chunk-MLKGABMK.js";
|
|
86
29
|
|
|
87
|
-
// src/cli/upgrade.ts
|
|
88
|
-
import { createJiti } from "jiti";
|
|
89
|
-
|
|
90
|
-
// src/cli/upgrade/migrations/conversation-history/state-conversation-store.ts
|
|
91
|
-
function createStateConversationStore(state) {
|
|
92
|
-
return {
|
|
93
|
-
get: (args) => getConversation({ ...args, state }),
|
|
94
|
-
// Task-execution state has no destination records, so visibility is never
|
|
95
|
-
// source-confirmed here and cross-context reads fail closed to private.
|
|
96
|
-
getDestinationVisibility: async () => void 0,
|
|
97
|
-
recordActivity: (args) => recordConversationActivity({ ...args, state }),
|
|
98
|
-
recordExecution: (args) => recordConversationExecution({ ...args, state }),
|
|
99
|
-
listByActivity: (args) => listConversationsByActivity({ ...args, state })
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// src/cli/upgrade/migrations/conversations-sql.ts
|
|
104
|
-
var CONVERSATION_BACKFILL_LIMIT = 1e4;
|
|
105
|
-
async function migrateConversationsToSql(context, options = {}) {
|
|
106
|
-
const source = createStateConversationStore(context.stateAdapter);
|
|
107
|
-
let target = options.target;
|
|
108
|
-
let closeTarget;
|
|
109
|
-
if (!target) {
|
|
110
|
-
const { sql: sql2 } = getChatConfig();
|
|
111
|
-
const executor = createJuniorSqlExecutor({
|
|
112
|
-
connectionString: sql2.databaseUrl,
|
|
113
|
-
driver: sql2.driver
|
|
114
|
-
});
|
|
115
|
-
target = createSqlStore(executor);
|
|
116
|
-
closeTarget = () => executor.close();
|
|
117
|
-
}
|
|
118
|
-
const limit = Math.max(1, options.batchSize ?? CONVERSATION_BACKFILL_LIMIT);
|
|
119
|
-
try {
|
|
120
|
-
const [stateConversations, sqlConversations] = await Promise.all([
|
|
121
|
-
source.listByActivity({ limit }),
|
|
122
|
-
target.listByActivity({ limit })
|
|
123
|
-
]);
|
|
124
|
-
const byId = new Map(
|
|
125
|
-
sqlConversations.map((conversation) => [
|
|
126
|
-
conversation.conversationId,
|
|
127
|
-
conversation
|
|
128
|
-
])
|
|
129
|
-
);
|
|
130
|
-
for (const conversation of stateConversations) {
|
|
131
|
-
const existing = byId.get(conversation.conversationId);
|
|
132
|
-
const existingExecutionAt = existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0;
|
|
133
|
-
const stateExecutionAt = conversation.execution.updatedAtMs ?? conversation.updatedAtMs;
|
|
134
|
-
if (!existing || stateExecutionAt >= existingExecutionAt) {
|
|
135
|
-
byId.set(conversation.conversationId, conversation);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const conversations = [...byId.values()].sort(
|
|
139
|
-
(left, right) => right.lastActivityAtMs - left.lastActivityAtMs || left.conversationId.localeCompare(right.conversationId)
|
|
140
|
-
).slice(0, limit);
|
|
141
|
-
const summaries = await listAgentTurnSessionSummariesForConversations(
|
|
142
|
-
context.stateAdapter,
|
|
143
|
-
conversations.map((conversation) => conversation.conversationId)
|
|
144
|
-
);
|
|
145
|
-
for (const conversation of conversations) {
|
|
146
|
-
const conversationSummaries = summaries.get(conversation.conversationId) ?? [];
|
|
147
|
-
const executionSummary = conversation.execution.runId ? conversationSummaries.find(
|
|
148
|
-
(summary) => summary.sessionId === conversation.execution.runId
|
|
149
|
-
) : void 0;
|
|
150
|
-
await target.backfillConversation(
|
|
151
|
-
conversation,
|
|
152
|
-
conversationSummaries.length > 0 ? {
|
|
153
|
-
durationMs: conversationSummaries.reduce(
|
|
154
|
-
(total, summary) => total + summary.cumulativeDurationMs,
|
|
155
|
-
0
|
|
156
|
-
),
|
|
157
|
-
usage: addAgentTurnUsage(
|
|
158
|
-
...conversationSummaries.map(
|
|
159
|
-
(summary) => summary.cumulativeUsage
|
|
160
|
-
)
|
|
161
|
-
),
|
|
162
|
-
executionDurationMs: executionSummary?.cumulativeDurationMs ?? 0,
|
|
163
|
-
executionUsage: executionSummary?.cumulativeUsage
|
|
164
|
-
} : void 0
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
existing: 0,
|
|
169
|
-
migrated: conversations.length,
|
|
170
|
-
missing: 0,
|
|
171
|
-
scanned: conversations.length
|
|
172
|
-
};
|
|
173
|
-
} finally {
|
|
174
|
-
await closeTarget?.();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
var sqlConversationMigration = {
|
|
178
|
-
name: "backfill-conversations-sql",
|
|
179
|
-
run: migrateConversationsToSql
|
|
180
|
-
};
|
|
181
|
-
|
|
182
30
|
// src/chat/conversations/sql/migrations.ts
|
|
183
31
|
import { basename, dirname, join, resolve } from "path";
|
|
184
32
|
import { fileURLToPath } from "url";
|
|
185
33
|
import { readMigrationFiles } from "drizzle-orm/migrator";
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
var
|
|
194
|
-
var LEGACY_CORE_MIGRATION_CHECKSUMS = {
|
|
195
|
-
"0001_conversation_core": "78fe050d8bec8ba18e2e3192497b3d8ad6b45fbb66ad4859377fb2202ed57651",
|
|
196
|
-
"0002_slack_destination_visibility_backfill": "fb590a09fa51db471a748e3d7abb4137f521ee8df97f6e9ef5563121be98c394",
|
|
197
|
-
"0003_user_identities": "67d9c9c26cbd76213614eb6d7a7cc7e2501fc20e92321eb5176a08ce39cd2efb",
|
|
198
|
-
"0004_actor_cutover": "d41b8bfa66b8a88d69e84af38950025ba4c9be56341565cbe1411f0ca50c1dc2",
|
|
199
|
-
"0005_conversation_transcripts": "add299d1b254e023f89b5993c417dd2248dc009e874efdeaf31ec0732e0d4fb4",
|
|
200
|
-
"0006_conversation_metrics": "7c7ca5c9e11ed4b0e14737fd90d3348ea46e306c88fdf31199b7afb2a11c6a41"
|
|
201
|
-
};
|
|
34
|
+
|
|
35
|
+
// src/db/postgres-error.ts
|
|
36
|
+
function isPostgresErrorCode(error, code) {
|
|
37
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/chat/conversations/sql/migrations.ts
|
|
41
|
+
var CORE_MIGRATION_BRIDGE_VERSION = "0.107.1";
|
|
202
42
|
var MIGRATIONS_TABLE = "__drizzle_junior_core";
|
|
203
43
|
function migrationFolder() {
|
|
204
44
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
205
45
|
const packageRoot = basename(moduleDir) === "dist" ? dirname(moduleDir) : basename(dirname(moduleDir)) === "dist" ? resolve(moduleDir, "../..") : resolve(moduleDir, "../../../..");
|
|
206
|
-
return join(packageRoot, "migrations");
|
|
207
|
-
}
|
|
208
|
-
async function adoptLegacyMigrationState(executor, migrationsFolder) {
|
|
209
|
-
const [tables] = await executor.query(`
|
|
210
|
-
SELECT
|
|
211
|
-
to_regclass('drizzle.__drizzle_junior_core')::text AS "drizzleTable",
|
|
212
|
-
to_regclass('public.junior_schema_migrations')::text AS "legacyTable"
|
|
213
|
-
`);
|
|
214
|
-
if (!tables?.legacyTable || tables.drizzleTable) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const migrations = readMigrationFiles({ migrationsFolder });
|
|
218
|
-
const [metrics] = await executor.query(`
|
|
219
|
-
SELECT count(*)::integer AS "columnCount"
|
|
220
|
-
FROM information_schema.columns
|
|
221
|
-
WHERE table_schema = 'public'
|
|
222
|
-
AND table_name = 'junior_conversations'
|
|
223
|
-
AND column_name IN (
|
|
224
|
-
'duration_ms',
|
|
225
|
-
'usage_json',
|
|
226
|
-
'execution_duration_ms',
|
|
227
|
-
'execution_usage_json'
|
|
228
|
-
)
|
|
229
|
-
`);
|
|
230
|
-
const legacyRecords = await executor.query("SELECT id, checksum FROM junior_schema_migrations");
|
|
231
|
-
const legacyRecordsById = new Map(
|
|
232
|
-
legacyRecords.map((record) => [record.id, record.checksum])
|
|
233
|
-
);
|
|
234
|
-
const metricColumnCount = metrics?.columnCount ?? 0;
|
|
235
|
-
if (metricColumnCount !== 0 && metricColumnCount !== 4) {
|
|
236
|
-
throw new Error(
|
|
237
|
-
`Cannot adopt partial legacy metrics state: found ${metricColumnCount} of 4 required columns`
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
const metricsComplete = metricColumnCount === 4;
|
|
241
|
-
const hasMetricsRecord = legacyRecordsById.has(LEGACY_METRICS_MIGRATION_ID);
|
|
242
|
-
if (metricsComplete !== hasMetricsRecord) {
|
|
243
|
-
throw new Error(
|
|
244
|
-
"Cannot adopt legacy core migration state: legacy metrics migration record does not match physical metric columns"
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
const expectedIds = metricsComplete ? [...LEGACY_CORE_MIGRATION_IDS, LEGACY_METRICS_MIGRATION_ID] : [...LEGACY_CORE_MIGRATION_IDS];
|
|
248
|
-
const missingIds = expectedIds.filter((id) => !legacyRecordsById.has(id));
|
|
249
|
-
if (missingIds.length > 0) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Cannot adopt partial legacy core migration state; missing: ${missingIds.join(", ")}`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
const checksumMismatches = expectedIds.filter(
|
|
255
|
-
(id) => legacyRecordsById.get(id) !== LEGACY_CORE_MIGRATION_CHECKSUMS[id]
|
|
256
|
-
);
|
|
257
|
-
if (checksumMismatches.length > 0) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`Cannot adopt legacy core migration state: checksum mismatch: ${checksumMismatches.join(", ")}`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
const [baseline] = await executor.query(`
|
|
263
|
-
SELECT
|
|
264
|
-
to_regclass('public.junior_conversation_events')::text AS "conversationEventsTable",
|
|
265
|
-
to_regclass('public.junior_conversation_messages_search_idx')::text AS "searchIndex",
|
|
266
|
-
EXISTS (
|
|
267
|
-
SELECT 1
|
|
268
|
-
FROM information_schema.tables
|
|
269
|
-
WHERE table_schema = 'public'
|
|
270
|
-
AND table_name = 'junior_agent_steps'
|
|
271
|
-
AND table_type = 'BASE TABLE'
|
|
272
|
-
) AS "legacyAgentStepsTable",
|
|
273
|
-
EXISTS (
|
|
274
|
-
SELECT 1
|
|
275
|
-
FROM information_schema.columns
|
|
276
|
-
WHERE table_schema = 'public'
|
|
277
|
-
AND table_name = 'junior_conversations'
|
|
278
|
-
AND column_name = 'metric_run_id'
|
|
279
|
-
) AS "metricRunIdColumn"
|
|
280
|
-
`);
|
|
281
|
-
if (!baseline?.legacyAgentStepsTable || baseline.conversationEventsTable) {
|
|
282
|
-
throw new Error(
|
|
283
|
-
"Cannot adopt legacy core migration state: expected the pre-Drizzle junior_agent_steps table and no junior_conversation_events table"
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
const postBaselineMarkers = [
|
|
287
|
-
baseline.searchIndex ? "junior_conversation_messages_search_idx" : void 0,
|
|
288
|
-
baseline.metricRunIdColumn ? "junior_conversations.metric_run_id" : void 0
|
|
289
|
-
].filter((marker) => marker !== void 0);
|
|
290
|
-
if (postBaselineMarkers.length > 0) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
`Cannot adopt legacy core migration state: post-baseline schema markers are already present: ${postBaselineMarkers.join(", ")}`
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
const migration = metricsComplete ? migrations[1] : migrations[0];
|
|
296
|
-
if (!migration) {
|
|
297
|
-
throw new Error("No core Drizzle migrations were packaged");
|
|
298
|
-
}
|
|
299
|
-
await executor.transaction(async () => {
|
|
300
|
-
await executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
|
|
301
|
-
await executor.execute(`
|
|
302
|
-
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_junior_core (
|
|
303
|
-
id SERIAL PRIMARY KEY,
|
|
304
|
-
hash TEXT NOT NULL,
|
|
305
|
-
created_at BIGINT
|
|
306
|
-
)
|
|
307
|
-
`);
|
|
308
|
-
await executor.execute(
|
|
309
|
-
`INSERT INTO drizzle.__drizzle_junior_core (hash, created_at)
|
|
310
|
-
VALUES ($1, $2)`,
|
|
311
|
-
[migration.hash, migration.folderMillis]
|
|
312
|
-
);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
async function migrateSchema(executor) {
|
|
316
|
-
const migrationsFolder = migrationFolder();
|
|
317
|
-
await executor.withMigrationLock(MIGRATIONS_TABLE, async () => {
|
|
318
|
-
await adoptLegacyMigrationState(executor, migrationsFolder);
|
|
319
|
-
await executor.migrate({
|
|
320
|
-
migrationsFolder,
|
|
321
|
-
migrationsTable: MIGRATIONS_TABLE
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/cli/upgrade/migrations/core-sql.ts
|
|
327
|
-
async function migrateCoreSqlSchema(_context) {
|
|
328
|
-
const { sql: sql2 } = getChatConfig();
|
|
329
|
-
const executor = createJuniorSqlExecutor({
|
|
330
|
-
connectionString: sql2.databaseUrl,
|
|
331
|
-
driver: sql2.driver
|
|
332
|
-
});
|
|
333
|
-
try {
|
|
334
|
-
await migrateSchema(executor);
|
|
335
|
-
return { existing: 0, migrated: 0, missing: 0, scanned: 0 };
|
|
336
|
-
} finally {
|
|
337
|
-
await executor.close();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
var coreSqlSchemaMigration = {
|
|
341
|
-
name: "core-sql-schema",
|
|
342
|
-
run: migrateCoreSqlSchema
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// src/cli/upgrade/migrations/conversation-history/import.ts
|
|
346
|
-
import { z as z3 } from "zod";
|
|
347
|
-
import { eq as eq2 } from "drizzle-orm";
|
|
348
|
-
|
|
349
|
-
// src/cli/upgrade/migrations/conversation-history/session-log.ts
|
|
350
|
-
import { z } from "zod";
|
|
351
|
-
var AGENT_SESSION_LOG_PREFIX = "junior:agent-session-log";
|
|
352
|
-
var AGENT_SESSION_LOG_SCHEMA_VERSION = 2;
|
|
353
|
-
var INITIAL_SESSION_ID = "session_0";
|
|
354
|
-
var schemaVersionSchema = z.union([z.literal(1), z.literal(2)]);
|
|
355
|
-
function legacyActorProvenance(actor) {
|
|
356
|
-
if (actor.teamId && actor.slackUserId && actor.platform) {
|
|
357
|
-
return instructionProvenanceFor({
|
|
358
|
-
platform: "slack",
|
|
359
|
-
teamId: actor.teamId,
|
|
360
|
-
userId: actor.slackUserId,
|
|
361
|
-
...actor.slackUserName ? { userName: actor.slackUserName } : {},
|
|
362
|
-
...actor.fullName ? { fullName: actor.fullName } : {},
|
|
363
|
-
...actor.email ? { email: actor.email } : {}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
return contextProvenance;
|
|
367
|
-
}
|
|
368
|
-
var piMessageEntrySchema = z.object({
|
|
369
|
-
schemaVersion: schemaVersionSchema,
|
|
370
|
-
type: z.literal("pi_message"),
|
|
371
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
372
|
-
message: piMessageSchema,
|
|
373
|
-
provenance: conversationMessageProvenanceSchema.optional(),
|
|
374
|
-
// Legacy v1 latest-wins actor, decoded into provenance on read.
|
|
375
|
-
actor: storedSlackActorSchema.optional()
|
|
376
|
-
});
|
|
377
|
-
var projectionResetEntrySchema = z.object({
|
|
378
|
-
schemaVersion: schemaVersionSchema,
|
|
379
|
-
type: z.literal("projection_reset"),
|
|
380
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
381
|
-
messages: z.array(piMessageSchema),
|
|
382
|
-
provenance: z.array(conversationMessageProvenanceSchema).optional(),
|
|
383
|
-
// Legacy v1 latest-wins actor; v1 resets carry no per-message provenance.
|
|
384
|
-
actor: storedSlackActorSchema.optional()
|
|
385
|
-
});
|
|
386
|
-
var actorRecordedEntrySchema = z.object({
|
|
387
|
-
schemaVersion: schemaVersionSchema,
|
|
388
|
-
type: z.literal("actor_recorded"),
|
|
389
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
390
|
-
actor: storedSlackActorSchema
|
|
391
|
-
});
|
|
392
|
-
var mcpProviderConnectedEntrySchema = z.object({
|
|
393
|
-
schemaVersion: schemaVersionSchema,
|
|
394
|
-
type: z.literal("mcp_provider_connected"),
|
|
395
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
396
|
-
provider: z.string().min(1)
|
|
397
|
-
});
|
|
398
|
-
var authorizationKindSchema = z.union([
|
|
399
|
-
z.literal("plugin"),
|
|
400
|
-
z.literal("mcp")
|
|
401
|
-
]);
|
|
402
|
-
var authorizationRequestedEntrySchema = z.object({
|
|
403
|
-
schemaVersion: schemaVersionSchema,
|
|
404
|
-
type: z.literal("authorization_requested"),
|
|
405
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
406
|
-
createdAtMs: z.number().int().nonnegative(),
|
|
407
|
-
kind: authorizationKindSchema,
|
|
408
|
-
provider: z.string().min(1),
|
|
409
|
-
actorId: z.string().min(1),
|
|
410
|
-
authorizationId: z.string().min(1),
|
|
411
|
-
delivery: z.union([
|
|
412
|
-
z.literal("private_link_sent"),
|
|
413
|
-
z.literal("private_link_reused")
|
|
414
|
-
])
|
|
415
|
-
});
|
|
416
|
-
var authorizationCompletedEntrySchema = z.object({
|
|
417
|
-
schemaVersion: schemaVersionSchema,
|
|
418
|
-
type: z.literal("authorization_completed"),
|
|
419
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
420
|
-
createdAtMs: z.number().int().nonnegative(),
|
|
421
|
-
kind: authorizationKindSchema,
|
|
422
|
-
provider: z.string().min(1),
|
|
423
|
-
actorId: z.string().min(1),
|
|
424
|
-
authorizationId: z.string().min(1)
|
|
425
|
-
});
|
|
426
|
-
var transcriptRefSchema = z.object({
|
|
427
|
-
type: z.literal("advisor_session"),
|
|
428
|
-
parentConversationId: z.string().min(1),
|
|
429
|
-
key: z.string().min(1)
|
|
430
|
-
});
|
|
431
|
-
var toolExecutionStartedEntrySchema = z.object({
|
|
432
|
-
schemaVersion: schemaVersionSchema,
|
|
433
|
-
type: z.literal("tool_execution_started"),
|
|
434
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
435
|
-
createdAtMs: z.number().int().nonnegative(),
|
|
436
|
-
toolCallId: z.string().min(1),
|
|
437
|
-
toolName: z.string().min(1),
|
|
438
|
-
args: z.unknown().optional()
|
|
439
|
-
});
|
|
440
|
-
var subagentStartedEntrySchema = z.object({
|
|
441
|
-
schemaVersion: schemaVersionSchema,
|
|
442
|
-
type: z.literal("subagent_started"),
|
|
443
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
444
|
-
subagentInvocationId: z.string().min(1),
|
|
445
|
-
subagentKind: z.string().min(1),
|
|
446
|
-
parentToolCallId: z.string().min(1).optional(),
|
|
447
|
-
parentConversationId: z.string().min(1),
|
|
448
|
-
parentSessionId: z.string().min(1).optional(),
|
|
449
|
-
transcriptRef: transcriptRefSchema,
|
|
450
|
-
historyMode: z.literal("shared"),
|
|
451
|
-
modelId: z.string().min(1).optional(),
|
|
452
|
-
reasoningLevel: z.string().min(1).optional(),
|
|
453
|
-
createdAtMs: z.number().int().nonnegative()
|
|
454
|
-
});
|
|
455
|
-
var subagentEndedEntrySchema = z.object({
|
|
456
|
-
schemaVersion: schemaVersionSchema,
|
|
457
|
-
type: z.literal("subagent_ended"),
|
|
458
|
-
sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
|
|
459
|
-
subagentInvocationId: z.string().min(1),
|
|
460
|
-
outcome: z.union([
|
|
461
|
-
z.literal("success"),
|
|
462
|
-
z.literal("error"),
|
|
463
|
-
z.literal("aborted")
|
|
464
|
-
]),
|
|
465
|
-
errorCode: z.string().min(1).optional(),
|
|
466
|
-
transcriptEndMessageIndex: z.number().int().nonnegative().optional(),
|
|
467
|
-
transcriptStartMessageIndex: z.number().int().nonnegative().optional(),
|
|
468
|
-
createdAtMs: z.number().int().nonnegative()
|
|
469
|
-
});
|
|
470
|
-
var sessionLogEntrySchema = z.discriminatedUnion("type", [
|
|
471
|
-
piMessageEntrySchema,
|
|
472
|
-
projectionResetEntrySchema,
|
|
473
|
-
actorRecordedEntrySchema,
|
|
474
|
-
mcpProviderConnectedEntrySchema,
|
|
475
|
-
authorizationRequestedEntrySchema,
|
|
476
|
-
authorizationCompletedEntrySchema,
|
|
477
|
-
toolExecutionStartedEntrySchema,
|
|
478
|
-
subagentStartedEntrySchema,
|
|
479
|
-
subagentEndedEntrySchema
|
|
480
|
-
]);
|
|
481
|
-
function key(scope) {
|
|
482
|
-
const prefix = getChatConfig().state.keyPrefix;
|
|
483
|
-
return [
|
|
484
|
-
...prefix ? [prefix] : [],
|
|
485
|
-
AGENT_SESSION_LOG_PREFIX,
|
|
486
|
-
scope.conversationId
|
|
487
|
-
].join(":");
|
|
488
|
-
}
|
|
489
|
-
function rawKey(scope) {
|
|
490
|
-
return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
|
|
491
|
-
}
|
|
492
|
-
function storedRecord(value) {
|
|
493
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
494
|
-
}
|
|
495
|
-
function migrateStoredEntry(value) {
|
|
496
|
-
const record = storedRecord(value);
|
|
497
|
-
if (!record) {
|
|
498
|
-
return value;
|
|
499
|
-
}
|
|
500
|
-
const migrated = { ...record };
|
|
501
|
-
if ("requester" in migrated && !("actor" in migrated)) {
|
|
502
|
-
migrated.actor = migrated.requester;
|
|
503
|
-
}
|
|
504
|
-
delete migrated.requester;
|
|
505
|
-
if (migrated.type === "requester_recorded") {
|
|
506
|
-
migrated.type = "actor_recorded";
|
|
507
|
-
}
|
|
508
|
-
if ("requesterId" in migrated && !("actorId" in migrated)) {
|
|
509
|
-
migrated.actorId = migrated.requesterId;
|
|
510
|
-
}
|
|
511
|
-
delete migrated.requesterId;
|
|
512
|
-
return migrated;
|
|
513
|
-
}
|
|
514
|
-
function decode(value) {
|
|
515
|
-
if (typeof value === "string") {
|
|
516
|
-
return decode(JSON.parse(value));
|
|
517
|
-
}
|
|
518
|
-
const parsed = sessionLogEntrySchema.safeParse(migrateStoredEntry(value));
|
|
519
|
-
if (parsed.success) {
|
|
520
|
-
return parsed.data;
|
|
521
|
-
}
|
|
522
|
-
return {
|
|
523
|
-
schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
|
|
524
|
-
type: "pi_message",
|
|
525
|
-
sessionId: INITIAL_SESSION_ID,
|
|
526
|
-
message: piMessageSchema.parse(value)
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
function redisStore(redisStateAdapter) {
|
|
530
|
-
const client = redisStateAdapter.getClient();
|
|
531
|
-
return {
|
|
532
|
-
async read(scope) {
|
|
533
|
-
const values = await client.lRange(key(scope), 0, -1);
|
|
534
|
-
return values.map(decode);
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
function stateStore() {
|
|
539
|
-
const stateAdapter = getStateAdapter();
|
|
540
|
-
return {
|
|
541
|
-
async read(scope) {
|
|
542
|
-
const listKey = rawKey(scope);
|
|
543
|
-
const value = await stateAdapter.get(listKey);
|
|
544
|
-
if (Array.isArray(value)) {
|
|
545
|
-
return value.map(decode);
|
|
546
|
-
}
|
|
547
|
-
const values = await stateAdapter.getList(listKey);
|
|
548
|
-
return values.map(decode);
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
async function defaultStore() {
|
|
553
|
-
const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
|
|
554
|
-
if (redisStateAdapter) {
|
|
555
|
-
return redisStore(redisStateAdapter);
|
|
556
|
-
}
|
|
557
|
-
await stateAdapter.connect();
|
|
558
|
-
return stateStore();
|
|
559
|
-
}
|
|
560
|
-
async function loadEntries(args) {
|
|
561
|
-
const store = args.store ?? await defaultStore();
|
|
562
|
-
return (await store.read(args)).map(decode);
|
|
563
|
-
}
|
|
564
|
-
async function readSessionLogEntries(args) {
|
|
565
|
-
return loadEntries(args);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// src/cli/upgrade/migrations/conversation-history/advisor-session.ts
|
|
569
|
-
import { z as z2 } from "zod";
|
|
570
|
-
var legacyAdvisorMessagesSchema = z2.array(piMessageSchema);
|
|
571
|
-
function key2(conversationId) {
|
|
572
|
-
return `junior:${conversationId}:advisor_session`;
|
|
573
|
-
}
|
|
574
|
-
function createLegacyAdvisorSessionReader() {
|
|
575
|
-
return {
|
|
576
|
-
load: async (conversationId) => {
|
|
577
|
-
const stateAdapter = getStateAdapter();
|
|
578
|
-
await stateAdapter.connect();
|
|
579
|
-
return legacyAdvisorMessagesSchema.parse(
|
|
580
|
-
structuredClone(
|
|
581
|
-
await stateAdapter.get(key2(conversationId)) ?? []
|
|
582
|
-
)
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// src/cli/upgrade/migrations/conversation-history/legacy-history-import.ts
|
|
589
|
-
import { eq, sql } from "drizzle-orm";
|
|
590
|
-
|
|
591
|
-
// src/cli/upgrade/migrations/conversation-history/legacy-context-message.ts
|
|
592
|
-
var LEGACY_COMPACTION_SUMMARY_PREFIX = "Context handoff summary for future Junior turns:";
|
|
593
|
-
function isLegacyCheckpointText(text) {
|
|
594
|
-
const normalized = (unwrapCurrentInstruction(text) ?? text).trimStart();
|
|
595
|
-
return normalized.startsWith(LEGACY_COMPACTION_SUMMARY_PREFIX);
|
|
596
|
-
}
|
|
597
|
-
function isLegacyOrCurrentCheckpointText(text) {
|
|
598
|
-
return isCompactionSummaryText(text) || isLegacyCheckpointText(text);
|
|
599
|
-
}
|
|
600
|
-
function normalizeText(text) {
|
|
601
|
-
if (!isLegacyCheckpointText(text)) return text;
|
|
602
|
-
return text.replace(
|
|
603
|
-
LEGACY_COMPACTION_SUMMARY_PREFIX,
|
|
604
|
-
COMPACTION_SUMMARY_PREFIX
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
function normalizeLegacyContextMessage(message) {
|
|
608
|
-
const record = message;
|
|
609
|
-
if (typeof record.content === "string") {
|
|
610
|
-
const content2 = normalizeText(record.content);
|
|
611
|
-
return content2 === record.content ? message : { ...record, content: content2 };
|
|
612
|
-
}
|
|
613
|
-
if (!Array.isArray(record.content)) return message;
|
|
614
|
-
let changed = false;
|
|
615
|
-
const content = record.content.map((part) => {
|
|
616
|
-
if (!part || typeof part !== "object" || typeof part.text !== "string") {
|
|
617
|
-
return part;
|
|
618
|
-
}
|
|
619
|
-
const text = normalizeText(part.text);
|
|
620
|
-
if (text === part.text) return part;
|
|
621
|
-
changed = true;
|
|
622
|
-
return { ...part, text };
|
|
623
|
-
});
|
|
624
|
-
return changed ? { ...record, content } : message;
|
|
625
|
-
}
|
|
626
|
-
function isLegacyOrCurrentCheckpointMessage(message) {
|
|
627
|
-
const record = message;
|
|
628
|
-
if (typeof record.content === "string") {
|
|
629
|
-
return isLegacyOrCurrentCheckpointText(record.content);
|
|
630
|
-
}
|
|
631
|
-
if (!Array.isArray(record.content)) return false;
|
|
632
|
-
return record.content.some(
|
|
633
|
-
(part) => part !== null && typeof part === "object" && typeof part.text === "string" && isLegacyOrCurrentCheckpointText(part.text)
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/cli/upgrade/migrations/conversation-history/legacy-history-import.ts
|
|
638
|
-
var INITIAL_SESSION_ID2 = "session_0";
|
|
639
|
-
var ADVISOR_TASK_OPEN = "<advisor-task>\n";
|
|
640
|
-
var ADVISOR_TASK_CLOSE = "\n</advisor-task>";
|
|
641
|
-
var ADVISOR_CONTEXT_OPEN = "<executor-context>\n";
|
|
642
|
-
var ADVISOR_CONTEXT_CLOSE = "\n</executor-context>";
|
|
643
|
-
function importedAdvisorChildConversationId(parentConversationId) {
|
|
644
|
-
return `advisor:${parentConversationId}`;
|
|
645
|
-
}
|
|
646
|
-
function epochFromSessionId(sessionId) {
|
|
647
|
-
const match = /^session_(\d+)$/.exec(sessionId);
|
|
648
|
-
return match ? Number(match[1]) : 0;
|
|
649
|
-
}
|
|
650
|
-
function messageTimestampMs(message) {
|
|
651
|
-
const timestamp = message.timestamp;
|
|
652
|
-
return typeof timestamp === "number" ? timestamp : void 0;
|
|
653
|
-
}
|
|
654
|
-
function readAdvisorRequest(text) {
|
|
655
|
-
if (!text.startsWith(ADVISOR_TASK_OPEN) || !text.endsWith(ADVISOR_CONTEXT_CLOSE)) {
|
|
656
|
-
return void 0;
|
|
657
|
-
}
|
|
658
|
-
const taskEnd = text.indexOf(ADVISOR_TASK_CLOSE, ADVISOR_TASK_OPEN.length);
|
|
659
|
-
if (taskEnd < 0) {
|
|
660
|
-
return void 0;
|
|
661
|
-
}
|
|
662
|
-
const contextStart = taskEnd + ADVISOR_TASK_CLOSE.length + 2;
|
|
663
|
-
if (!text.startsWith(ADVISOR_CONTEXT_OPEN, contextStart)) {
|
|
664
|
-
return void 0;
|
|
665
|
-
}
|
|
666
|
-
const task = text.slice(ADVISOR_TASK_OPEN.length, taskEnd);
|
|
667
|
-
const context = text.slice(
|
|
668
|
-
contextStart + ADVISOR_CONTEXT_OPEN.length,
|
|
669
|
-
-ADVISOR_CONTEXT_CLOSE.length
|
|
670
|
-
);
|
|
671
|
-
return `${unescapeXml(task)}
|
|
672
|
-
|
|
673
|
-
Executor context:
|
|
674
|
-
${unescapeXml(context)}`;
|
|
675
|
-
}
|
|
676
|
-
function normalizeAdvisorMessage(message) {
|
|
677
|
-
const record = message;
|
|
678
|
-
if (record.role !== "user" || !Array.isArray(record.content)) {
|
|
679
|
-
return message;
|
|
680
|
-
}
|
|
681
|
-
let changed = false;
|
|
682
|
-
const content = record.content.map((part) => {
|
|
683
|
-
if (!part || typeof part !== "object" || part.type !== "text" || typeof part.text !== "string") {
|
|
684
|
-
return part;
|
|
685
|
-
}
|
|
686
|
-
const text = readAdvisorRequest(part.text);
|
|
687
|
-
if (text === void 0) {
|
|
688
|
-
return part;
|
|
689
|
-
}
|
|
690
|
-
changed = true;
|
|
691
|
-
return { ...part, text };
|
|
692
|
-
});
|
|
693
|
-
return changed ? { ...record, content } : message;
|
|
694
|
-
}
|
|
695
|
-
function piEntryProvenance(entry) {
|
|
696
|
-
if (entry.provenance) {
|
|
697
|
-
return entry.provenance;
|
|
698
|
-
}
|
|
699
|
-
if (entry.actor) {
|
|
700
|
-
return legacyActorProvenance(entry.actor);
|
|
701
|
-
}
|
|
702
|
-
return contextProvenance;
|
|
703
|
-
}
|
|
704
|
-
function convertLegacySessionLog(args) {
|
|
705
|
-
const events = [];
|
|
706
|
-
const fallback = args.fallbackCreatedAtMs;
|
|
707
|
-
let advisorChildConversationId;
|
|
708
|
-
let seq = 0;
|
|
709
|
-
const push = (historyVersion, data, createdAtMs) => {
|
|
710
|
-
events.push({ seq, historyVersion, data, createdAtMs });
|
|
711
|
-
seq += 1;
|
|
712
|
-
};
|
|
713
|
-
for (const entry of args.entries) {
|
|
714
|
-
const epoch = epochFromSessionId(entry.sessionId ?? INITIAL_SESSION_ID2);
|
|
715
|
-
switch (entry.type) {
|
|
716
|
-
case "pi_message": {
|
|
717
|
-
const message = entry.message;
|
|
718
|
-
push(
|
|
719
|
-
epoch,
|
|
720
|
-
{
|
|
721
|
-
type: "agent_step",
|
|
722
|
-
message,
|
|
723
|
-
provenance: piEntryProvenance(entry)
|
|
724
|
-
},
|
|
725
|
-
messageTimestampMs(message) ?? fallback
|
|
726
|
-
);
|
|
727
|
-
break;
|
|
728
|
-
}
|
|
729
|
-
case "projection_reset": {
|
|
730
|
-
const provenance = entry.provenance ?? entry.messages.map(() => contextProvenance);
|
|
731
|
-
if (provenance.length !== entry.messages.length) {
|
|
732
|
-
throw new Error(
|
|
733
|
-
"projection_reset provenance must align one-to-one with messages"
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
const resetCreatedAtMs = entry.messages.map(messageTimestampMs).find(
|
|
737
|
-
(timestamp) => timestamp !== void 0
|
|
738
|
-
) ?? fallback;
|
|
739
|
-
const checkpointIndex = entry.messages.findIndex(
|
|
740
|
-
isLegacyOrCurrentCheckpointMessage
|
|
741
|
-
);
|
|
742
|
-
push(
|
|
743
|
-
epoch,
|
|
744
|
-
{
|
|
745
|
-
type: "compaction",
|
|
746
|
-
modelProfile: "standard",
|
|
747
|
-
modelId: args.modelId,
|
|
748
|
-
replacementHistory: entry.messages.map((message, index) => ({
|
|
749
|
-
message: index === checkpointIndex ? normalizeLegacyContextMessage(message) : message,
|
|
750
|
-
provenance: provenance[index]
|
|
751
|
-
}))
|
|
752
|
-
},
|
|
753
|
-
resetCreatedAtMs
|
|
754
|
-
);
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
case "mcp_provider_connected": {
|
|
758
|
-
push(
|
|
759
|
-
epoch,
|
|
760
|
-
{ type: "mcp_provider_connected", provider: entry.provider },
|
|
761
|
-
fallback
|
|
762
|
-
);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
case "authorization_requested": {
|
|
766
|
-
push(
|
|
767
|
-
epoch,
|
|
768
|
-
{
|
|
769
|
-
type: "authorization_requested",
|
|
770
|
-
kind: entry.kind,
|
|
771
|
-
provider: entry.provider,
|
|
772
|
-
actorId: entry.actorId,
|
|
773
|
-
authorizationId: entry.authorizationId,
|
|
774
|
-
delivery: entry.delivery
|
|
775
|
-
},
|
|
776
|
-
entry.createdAtMs
|
|
777
|
-
);
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
case "authorization_completed": {
|
|
781
|
-
push(
|
|
782
|
-
epoch,
|
|
783
|
-
{
|
|
784
|
-
type: "authorization_completed",
|
|
785
|
-
kind: entry.kind,
|
|
786
|
-
provider: entry.provider,
|
|
787
|
-
actorId: entry.actorId,
|
|
788
|
-
authorizationId: entry.authorizationId
|
|
789
|
-
},
|
|
790
|
-
entry.createdAtMs
|
|
791
|
-
);
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
case "tool_execution_started": {
|
|
795
|
-
push(
|
|
796
|
-
epoch,
|
|
797
|
-
{
|
|
798
|
-
type: "tool_execution_started",
|
|
799
|
-
toolCallId: entry.toolCallId,
|
|
800
|
-
toolName: entry.toolName
|
|
801
|
-
},
|
|
802
|
-
entry.createdAtMs
|
|
803
|
-
);
|
|
804
|
-
break;
|
|
805
|
-
}
|
|
806
|
-
case "subagent_started": {
|
|
807
|
-
const childConversationId = importedAdvisorChildConversationId(
|
|
808
|
-
args.conversationId
|
|
809
|
-
);
|
|
810
|
-
advisorChildConversationId = childConversationId;
|
|
811
|
-
push(
|
|
812
|
-
epoch,
|
|
813
|
-
{
|
|
814
|
-
type: "subagent_started",
|
|
815
|
-
subagentInvocationId: entry.subagentInvocationId,
|
|
816
|
-
subagentKind: entry.subagentKind,
|
|
817
|
-
...entry.parentToolCallId ? { parentToolCallId: entry.parentToolCallId } : {},
|
|
818
|
-
childConversationId
|
|
819
|
-
},
|
|
820
|
-
entry.createdAtMs
|
|
821
|
-
);
|
|
822
|
-
break;
|
|
823
|
-
}
|
|
824
|
-
case "subagent_ended": {
|
|
825
|
-
push(
|
|
826
|
-
epoch,
|
|
827
|
-
{
|
|
828
|
-
type: "subagent_ended",
|
|
829
|
-
subagentInvocationId: entry.subagentInvocationId,
|
|
830
|
-
outcome: entry.outcome,
|
|
831
|
-
...entry.errorCode ? { errorCode: entry.errorCode } : {}
|
|
832
|
-
},
|
|
833
|
-
entry.createdAtMs
|
|
834
|
-
);
|
|
835
|
-
break;
|
|
836
|
-
}
|
|
837
|
-
case "actor_recorded": {
|
|
838
|
-
break;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return {
|
|
843
|
-
events,
|
|
844
|
-
...advisorChildConversationId ? { advisorChildConversationId } : {}
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
function convertAdvisorMessages(messages, fallbackCreatedAtMs) {
|
|
848
|
-
return messages.map((sourceMessage, seq) => {
|
|
849
|
-
const message = normalizeAdvisorMessage(sourceMessage);
|
|
850
|
-
return {
|
|
851
|
-
seq,
|
|
852
|
-
historyVersion: 0,
|
|
853
|
-
data: { type: "agent_step", message, provenance: contextProvenance },
|
|
854
|
-
createdAtMs: messageTimestampMs(message) ?? fallbackCreatedAtMs
|
|
855
|
-
};
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
function insertRow(conversationId, event) {
|
|
859
|
-
const { type, ...payload } = conversationEventDataSchema.parse(event.data);
|
|
860
|
-
return {
|
|
861
|
-
conversationId,
|
|
862
|
-
seq: event.seq,
|
|
863
|
-
historyVersion: event.historyVersion,
|
|
864
|
-
schemaVersion: 1,
|
|
865
|
-
idempotencyKey: event.idempotencyKey ?? null,
|
|
866
|
-
type,
|
|
867
|
-
payload: sanitizePostgresJson(payload),
|
|
868
|
-
createdAt: new Date(event.createdAtMs)
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
function mergeImportedChronology(executionEvents, messages) {
|
|
872
|
-
const visibleEvents = messages.flatMap((message) => {
|
|
873
|
-
const recorded = {
|
|
874
|
-
seq: 0,
|
|
875
|
-
historyVersion: 0,
|
|
876
|
-
idempotencyKey: `message:${message.messageId}`,
|
|
877
|
-
data: {
|
|
878
|
-
type: "message",
|
|
879
|
-
messageId: message.messageId,
|
|
880
|
-
role: message.role,
|
|
881
|
-
text: message.text,
|
|
882
|
-
...message.authorIdentityId ? { authorIdentityId: message.authorIdentityId } : {},
|
|
883
|
-
...message.meta ? { meta: message.meta } : {}
|
|
884
|
-
},
|
|
885
|
-
createdAtMs: message.createdAtMs
|
|
886
|
-
};
|
|
887
|
-
return message.repliedAtMs === void 0 ? [recorded] : [
|
|
888
|
-
recorded,
|
|
889
|
-
{
|
|
890
|
-
seq: 0,
|
|
891
|
-
historyVersion: 0,
|
|
892
|
-
idempotencyKey: `message:${message.messageId}:handled`,
|
|
893
|
-
data: {
|
|
894
|
-
type: "message_handled",
|
|
895
|
-
messageId: message.messageId
|
|
896
|
-
},
|
|
897
|
-
createdAtMs: message.repliedAtMs
|
|
898
|
-
}
|
|
899
|
-
];
|
|
900
|
-
}).map((event, sourceOrder) => ({ event, sourceOrder })).sort(
|
|
901
|
-
(left, right) => left.event.createdAtMs - right.event.createdAtMs || left.sourceOrder - right.sourceOrder
|
|
902
|
-
);
|
|
903
|
-
const merged = [];
|
|
904
|
-
let visibleIndex = 0;
|
|
905
|
-
let currentEpoch = 0;
|
|
906
|
-
const push = (event, historyVersion) => {
|
|
907
|
-
merged.push({ ...event, seq: merged.length, historyVersion });
|
|
908
|
-
};
|
|
909
|
-
for (const executionEvent of [...executionEvents].sort(
|
|
910
|
-
(left, right) => left.seq - right.seq
|
|
911
|
-
)) {
|
|
912
|
-
while (visibleIndex < visibleEvents.length && visibleEvents[visibleIndex].event.createdAtMs <= executionEvent.createdAtMs) {
|
|
913
|
-
push(visibleEvents[visibleIndex].event, currentEpoch);
|
|
914
|
-
visibleIndex += 1;
|
|
915
|
-
}
|
|
916
|
-
push(executionEvent, executionEvent.historyVersion);
|
|
917
|
-
currentEpoch = Math.max(currentEpoch, executionEvent.historyVersion);
|
|
918
|
-
}
|
|
919
|
-
while (visibleIndex < visibleEvents.length) {
|
|
920
|
-
push(visibleEvents[visibleIndex].event, currentEpoch);
|
|
921
|
-
visibleIndex += 1;
|
|
922
|
-
}
|
|
923
|
-
const firstVisibleSeq = merged.find(
|
|
924
|
-
(event) => event.data.type === "message"
|
|
925
|
-
)?.seq;
|
|
926
|
-
return merged.map(
|
|
927
|
-
(event) => event.data.type === "messages_summarized" ? {
|
|
928
|
-
...event,
|
|
929
|
-
data: {
|
|
930
|
-
...event.data,
|
|
931
|
-
historyFromSeq: firstVisibleSeq ?? event.seq + 1
|
|
932
|
-
}
|
|
933
|
-
} : event
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
async function writeLegacyImport(executor, args) {
|
|
937
|
-
return withConversationEventLock(executor, args.conversationId, async () => {
|
|
938
|
-
const db = executor.db();
|
|
939
|
-
const conversations = await db.select({
|
|
940
|
-
transcriptPurgedAt: juniorConversations.transcriptPurgedAt
|
|
941
|
-
}).from(juniorConversations).where(eq(juniorConversations.conversationId, args.conversationId)).for("update");
|
|
942
|
-
if (conversations[0]?.transcriptPurgedAt) {
|
|
943
|
-
return false;
|
|
944
|
-
}
|
|
945
|
-
const existing = await db.select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, args.conversationId)).limit(1);
|
|
946
|
-
if (existing.length > 0) {
|
|
947
|
-
return false;
|
|
948
|
-
}
|
|
949
|
-
const createdAt = new Date(args.fallbackCreatedAtMs);
|
|
950
|
-
await ensureConversationRow(
|
|
951
|
-
executor,
|
|
952
|
-
args.conversationId,
|
|
953
|
-
createdAt,
|
|
954
|
-
new Date(args.lastActivityAtMs)
|
|
955
|
-
);
|
|
956
|
-
const mergedEvents = mergeImportedChronology(
|
|
957
|
-
args.events,
|
|
958
|
-
args.messages ?? []
|
|
959
|
-
);
|
|
960
|
-
if (mergedEvents.length > 0) {
|
|
961
|
-
await db.insert(juniorConversationEvents).values(
|
|
962
|
-
mergedEvents.map((event) => insertRow(args.conversationId, event))
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
if (args.child) {
|
|
966
|
-
const childCreatedAtMs = args.child.events.length > 0 ? Math.min(...args.child.events.map((event) => event.createdAtMs)) : args.fallbackCreatedAtMs;
|
|
967
|
-
const childLastActivityAtMs = args.child.events.length > 0 ? Math.max(...args.child.events.map((event) => event.createdAtMs)) : childCreatedAtMs;
|
|
968
|
-
await insertLegacyAdvisorChildRow(
|
|
969
|
-
executor,
|
|
970
|
-
args.child.conversationId,
|
|
971
|
-
args.conversationId,
|
|
972
|
-
new Date(childCreatedAtMs),
|
|
973
|
-
new Date(childLastActivityAtMs)
|
|
974
|
-
);
|
|
975
|
-
if (args.child.events.length > 0) {
|
|
976
|
-
await db.insert(juniorConversationEvents).values(
|
|
977
|
-
args.child.events.map(
|
|
978
|
-
(event) => insertRow(args.child.conversationId, event)
|
|
979
|
-
)
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
return true;
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
async function ensureConversationRow(executor, conversationId, createdAt, lastActivityAt) {
|
|
987
|
-
await executor.db().insert(juniorConversations).values({
|
|
988
|
-
conversationId,
|
|
989
|
-
schemaVersion: 1,
|
|
990
|
-
createdAt,
|
|
991
|
-
lastActivityAt,
|
|
992
|
-
updatedAt: lastActivityAt,
|
|
993
|
-
executionStatus: "idle"
|
|
994
|
-
}).onConflictDoUpdate({
|
|
995
|
-
target: juniorConversations.conversationId,
|
|
996
|
-
set: {
|
|
997
|
-
createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
|
|
998
|
-
lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
|
|
999
|
-
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
async function insertLegacyAdvisorChildRow(executor, childConversationId, parentConversationId, createdAt, lastActivityAt) {
|
|
1004
|
-
await ensureConversationRow(
|
|
1005
|
-
executor,
|
|
1006
|
-
parentConversationId,
|
|
1007
|
-
createdAt,
|
|
1008
|
-
lastActivityAt
|
|
1009
|
-
);
|
|
1010
|
-
await executor.db().insert(juniorConversations).values({
|
|
1011
|
-
conversationId: childConversationId,
|
|
1012
|
-
schemaVersion: 1,
|
|
1013
|
-
parentConversationId,
|
|
1014
|
-
createdAt,
|
|
1015
|
-
lastActivityAt,
|
|
1016
|
-
updatedAt: lastActivityAt,
|
|
1017
|
-
executionStatus: "idle"
|
|
1018
|
-
}).onConflictDoUpdate({
|
|
1019
|
-
target: juniorConversations.conversationId,
|
|
1020
|
-
set: {
|
|
1021
|
-
createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
|
|
1022
|
-
lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
|
|
1023
|
-
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
const rows = await executor.db().select({
|
|
1027
|
-
parentConversationId: juniorConversations.parentConversationId
|
|
1028
|
-
}).from(juniorConversations).where(eq(juniorConversations.conversationId, childConversationId));
|
|
1029
|
-
const child = rows[0];
|
|
1030
|
-
if (!child || child.parentConversationId !== parentConversationId) {
|
|
1031
|
-
throw new Error(
|
|
1032
|
-
"Legacy advisor child conflicts with existing conversation lineage"
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// src/cli/upgrade/migrations/conversation-history/import.ts
|
|
1038
|
-
var legacyVisibleMessageSchema = z3.object({
|
|
1039
|
-
id: z3.string(),
|
|
1040
|
-
role: z3.enum(["user", "assistant", "system"]),
|
|
1041
|
-
text: z3.string(),
|
|
1042
|
-
createdAtMs: z3.number().finite(),
|
|
1043
|
-
author: z3.object({}).passthrough().optional(),
|
|
1044
|
-
meta: z3.object({}).passthrough().optional()
|
|
1045
|
-
});
|
|
1046
|
-
var legacyCompactionSchema = z3.object({
|
|
1047
|
-
coveredMessageIds: z3.array(z3.string()),
|
|
1048
|
-
createdAtMs: z3.number().finite(),
|
|
1049
|
-
id: z3.string(),
|
|
1050
|
-
summary: z3.string()
|
|
1051
|
-
});
|
|
1052
|
-
function toImportedMessage(message) {
|
|
1053
|
-
const meta = {};
|
|
1054
|
-
if (message.author) meta.author = message.author;
|
|
1055
|
-
const { replied: _replied, ...restMeta } = message.meta ?? {};
|
|
1056
|
-
Object.assign(meta, restMeta);
|
|
1057
|
-
return {
|
|
1058
|
-
messageId: message.id,
|
|
1059
|
-
role: message.role,
|
|
1060
|
-
text: message.text,
|
|
1061
|
-
...Object.keys(meta).length > 0 ? { meta } : {},
|
|
1062
|
-
createdAtMs: message.createdAtMs
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
var legacyThreadStateSnapshotSchema = z3.object({
|
|
1066
|
-
conversation: z3.object({
|
|
1067
|
-
compactions: z3.array(legacyCompactionSchema).optional(),
|
|
1068
|
-
messages: z3.array(legacyVisibleMessageSchema).optional(),
|
|
1069
|
-
stats: z3.object({
|
|
1070
|
-
compactedMessageCount: z3.number().finite().optional(),
|
|
1071
|
-
updatedAtMs: z3.number().finite().optional()
|
|
1072
|
-
}).passthrough().optional()
|
|
1073
|
-
}).passthrough().optional()
|
|
1074
|
-
});
|
|
1075
|
-
async function loadThreadStateSnapshot(conversationId) {
|
|
1076
|
-
const stateAdapter = getStateAdapter();
|
|
1077
|
-
await stateAdapter.connect();
|
|
1078
|
-
const raw = await stateAdapter.get(`thread-state:${conversationId}`);
|
|
1079
|
-
if (!raw) {
|
|
1080
|
-
return { compactions: [], messages: [] };
|
|
1081
|
-
}
|
|
1082
|
-
const conversation = legacyThreadStateSnapshotSchema.parse(raw).conversation;
|
|
1083
|
-
return {
|
|
1084
|
-
compactions: conversation?.compactions ?? [],
|
|
1085
|
-
messages: conversation?.messages ?? [],
|
|
1086
|
-
...conversation?.stats?.compactedMessageCount !== void 0 ? { compactedMessageCount: conversation.stats.compactedMessageCount } : {},
|
|
1087
|
-
...conversation?.stats?.updatedAtMs !== void 0 ? { lastActivityAtMs: conversation.stats.updatedAtMs } : {}
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
function intrinsicTimestamps(entries, visible, compactions) {
|
|
1091
|
-
const candidates = [];
|
|
1092
|
-
const pushMessageTs = (message) => {
|
|
1093
|
-
const timestamp = message.timestamp;
|
|
1094
|
-
if (typeof timestamp === "number") {
|
|
1095
|
-
candidates.push(timestamp);
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
for (const entry of entries) {
|
|
1099
|
-
if (entry.type === "pi_message") {
|
|
1100
|
-
pushMessageTs(entry.message);
|
|
1101
|
-
} else if (entry.type === "projection_reset") {
|
|
1102
|
-
entry.messages.forEach(pushMessageTs);
|
|
1103
|
-
} else if ("createdAtMs" in entry) {
|
|
1104
|
-
candidates.push(entry.createdAtMs);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
for (const message of visible) {
|
|
1108
|
-
candidates.push(message.createdAtMs);
|
|
1109
|
-
}
|
|
1110
|
-
for (const compaction of compactions) {
|
|
1111
|
-
candidates.push(compaction.createdAtMs);
|
|
1112
|
-
}
|
|
1113
|
-
return candidates;
|
|
1114
|
-
}
|
|
1115
|
-
async function importConversationFromLegacy(conversationId, deps) {
|
|
1116
|
-
const existing = await deps.executor.db().select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(eq2(juniorConversationEvents.conversationId, conversationId)).limit(1);
|
|
1117
|
-
if (existing.length > 0) {
|
|
1118
|
-
return { imported: false };
|
|
1119
|
-
}
|
|
1120
|
-
const entries = deps.sessionLogStore ? await deps.sessionLogStore.read({ conversationId }) : await readSessionLogEntries({ conversationId });
|
|
1121
|
-
const snapshot = deps.loadVisibleMessages ? {
|
|
1122
|
-
compactions: deps.legacyCompactions ?? [],
|
|
1123
|
-
messages: await deps.loadVisibleMessages(conversationId)
|
|
1124
|
-
} : await loadThreadStateSnapshot(conversationId);
|
|
1125
|
-
const { compactedMessageCount, compactions, messages: visible } = snapshot;
|
|
1126
|
-
if (entries.length === 0 && visible.length === 0 && compactions.length === 0) {
|
|
1127
|
-
return { imported: false };
|
|
1128
|
-
}
|
|
1129
|
-
const hasAdvisor = entries.some((entry) => entry.type === "subagent_started");
|
|
1130
|
-
const advisorMessages = hasAdvisor ? await (deps.advisorSessionStore ?? createLegacyAdvisorSessionReader()).load(conversationId) : [];
|
|
1131
|
-
const intrinsic = intrinsicTimestamps(entries, visible, compactions);
|
|
1132
|
-
for (const message of advisorMessages) {
|
|
1133
|
-
const timestamp = message.timestamp;
|
|
1134
|
-
if (typeof timestamp === "number") {
|
|
1135
|
-
intrinsic.push(timestamp);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
const fallbackCreatedAtMs = deps.conversationRecord?.createdAtMs ?? (intrinsic.length > 0 ? Math.min(...intrinsic) : void 0) ?? 0;
|
|
1139
|
-
const lastActivityAtMs = Math.max(
|
|
1140
|
-
fallbackCreatedAtMs,
|
|
1141
|
-
deps.conversationRecord?.lastActivityAtMs ?? 0,
|
|
1142
|
-
deps.legacyLastActivityAtMs ?? 0,
|
|
1143
|
-
intrinsic.length > 0 ? Math.max(...intrinsic) : 0
|
|
1144
|
-
);
|
|
1145
|
-
const converted = convertLegacySessionLog({
|
|
1146
|
-
conversationId,
|
|
1147
|
-
entries,
|
|
1148
|
-
fallbackCreatedAtMs,
|
|
1149
|
-
modelId: deps.modelId
|
|
1150
|
-
});
|
|
1151
|
-
if (compactions.length > 0) {
|
|
1152
|
-
const storedCount = compactions.reduce(
|
|
1153
|
-
(count, compaction) => count + compaction.coveredMessageIds.length,
|
|
1154
|
-
0
|
|
1155
|
-
);
|
|
1156
|
-
const statsCount = typeof compactedMessageCount === "number" && Number.isFinite(compactedMessageCount) && compactedMessageCount > 0 ? Math.floor(compactedMessageCount) : 0;
|
|
1157
|
-
const totalCount = Math.max(storedCount, statsCount);
|
|
1158
|
-
const compacted = compactions.map((compaction, index) => ({
|
|
1159
|
-
id: compaction.id,
|
|
1160
|
-
summary: compaction.summary,
|
|
1161
|
-
createdAtMs: compaction.createdAtMs,
|
|
1162
|
-
coveredMessageCount: compaction.coveredMessageIds.length + (index === 0 ? totalCount - storedCount : 0)
|
|
1163
|
-
}));
|
|
1164
|
-
converted.events.push({
|
|
1165
|
-
seq: converted.events.length,
|
|
1166
|
-
historyVersion: converted.events.at(-1)?.historyVersion ?? 0,
|
|
1167
|
-
data: {
|
|
1168
|
-
type: "messages_summarized",
|
|
1169
|
-
historyFromSeq: 0,
|
|
1170
|
-
compactions: compacted
|
|
1171
|
-
},
|
|
1172
|
-
createdAtMs: compactions.at(-1)?.createdAtMs ?? fallbackCreatedAtMs
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
let child;
|
|
1176
|
-
if (converted.advisorChildConversationId) {
|
|
1177
|
-
child = {
|
|
1178
|
-
conversationId: converted.advisorChildConversationId,
|
|
1179
|
-
events: convertAdvisorMessages(advisorMessages, fallbackCreatedAtMs)
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
const messages = visible.map((message) => ({
|
|
1183
|
-
...toImportedMessage(message),
|
|
1184
|
-
...message.meta?.replied ? { repliedAtMs: message.createdAtMs } : {}
|
|
1185
|
-
}));
|
|
1186
|
-
const imported = await writeLegacyImport(deps.executor, {
|
|
1187
|
-
conversationId,
|
|
1188
|
-
fallbackCreatedAtMs,
|
|
1189
|
-
lastActivityAtMs,
|
|
1190
|
-
...messages.length > 0 ? { messages } : {},
|
|
1191
|
-
events: converted.events,
|
|
1192
|
-
...child ? { child } : {}
|
|
1193
|
-
});
|
|
1194
|
-
return { imported };
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// src/cli/upgrade/migrations/conversations-history-sql.ts
|
|
1198
|
-
var HISTORY_BACKFILL_PAGE_SIZE = 500;
|
|
1199
|
-
async function migrateConversationHistoryToSql(context, options = {}) {
|
|
1200
|
-
const source = createStateConversationStore(context.stateAdapter);
|
|
1201
|
-
const chatConfig = getChatConfig();
|
|
1202
|
-
let executor = options.executor;
|
|
1203
|
-
let closeExecutor;
|
|
1204
|
-
if (!executor) {
|
|
1205
|
-
const { sql: sql2 } = chatConfig;
|
|
1206
|
-
executor = createJuniorSqlExecutor({
|
|
1207
|
-
connectionString: sql2.databaseUrl,
|
|
1208
|
-
driver: sql2.driver
|
|
1209
|
-
});
|
|
1210
|
-
closeExecutor = () => executor.close();
|
|
1211
|
-
}
|
|
1212
|
-
const pageSize = Math.max(
|
|
1213
|
-
1,
|
|
1214
|
-
Math.floor(options.batchSize ?? HISTORY_BACKFILL_PAGE_SIZE)
|
|
1215
|
-
);
|
|
1216
|
-
try {
|
|
1217
|
-
let migrated = 0;
|
|
1218
|
-
let existing = 0;
|
|
1219
|
-
let scanned = 0;
|
|
1220
|
-
let offset = 0;
|
|
1221
|
-
while (true) {
|
|
1222
|
-
const conversations = await source.listByActivity({
|
|
1223
|
-
limit: pageSize,
|
|
1224
|
-
offset
|
|
1225
|
-
});
|
|
1226
|
-
if (conversations.length === 0) {
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
for (const conversation of conversations) {
|
|
1230
|
-
const result = await importConversationFromLegacy(
|
|
1231
|
-
conversation.conversationId,
|
|
1232
|
-
{
|
|
1233
|
-
executor,
|
|
1234
|
-
modelId: standardModelId(chatConfig.bot),
|
|
1235
|
-
conversationRecord: conversation,
|
|
1236
|
-
...options.sessionLogStore ? { sessionLogStore: options.sessionLogStore } : {},
|
|
1237
|
-
...options.advisorSessionStore ? { advisorSessionStore: options.advisorSessionStore } : {},
|
|
1238
|
-
...options.loadVisibleMessages ? { loadVisibleMessages: options.loadVisibleMessages } : {}
|
|
1239
|
-
}
|
|
1240
|
-
);
|
|
1241
|
-
if (result.imported) {
|
|
1242
|
-
migrated += 1;
|
|
1243
|
-
} else {
|
|
1244
|
-
existing += 1;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
scanned += conversations.length;
|
|
1248
|
-
offset += conversations.length;
|
|
1249
|
-
}
|
|
1250
|
-
return {
|
|
1251
|
-
existing,
|
|
1252
|
-
migrated,
|
|
1253
|
-
missing: 0,
|
|
1254
|
-
scanned
|
|
1255
|
-
};
|
|
1256
|
-
} finally {
|
|
1257
|
-
await closeExecutor?.();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
var sqlConversationHistoryMigration = {
|
|
1261
|
-
name: "backfill-conversation-events-sql",
|
|
1262
|
-
run: migrateConversationHistoryToSql
|
|
1263
|
-
};
|
|
1264
|
-
|
|
1265
|
-
// src/cli/upgrade/migrations/upgrade-plugins.ts
|
|
1266
|
-
function unique(values) {
|
|
1267
|
-
return [...new Set(values)];
|
|
1268
|
-
}
|
|
1269
|
-
function baseCatalogConfig(context) {
|
|
1270
|
-
return context.pluginCatalogConfig ?? (context.pluginSet ? pluginCatalogConfigFromPluginSet(context.pluginSet) : pluginCatalogConfigFromEnv());
|
|
1271
|
-
}
|
|
1272
|
-
function inlinePluginName(definition) {
|
|
1273
|
-
return definition.manifest.name;
|
|
1274
|
-
}
|
|
1275
|
-
function mergeInlineManifests(left, right) {
|
|
1276
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1277
|
-
for (const definition of [...left ?? [], ...right ?? []]) {
|
|
1278
|
-
merged.set(inlinePluginName(definition), definition);
|
|
1279
|
-
}
|
|
1280
|
-
return merged.size > 0 ? [...merged.values()] : void 0;
|
|
1281
|
-
}
|
|
1282
|
-
function mergeCatalogConfig(base, added) {
|
|
1283
|
-
if (!base) {
|
|
1284
|
-
return added;
|
|
1285
|
-
}
|
|
1286
|
-
if (!added) {
|
|
1287
|
-
return base;
|
|
1288
|
-
}
|
|
1289
|
-
const inlineManifests = mergeInlineManifests(
|
|
1290
|
-
base.inlineManifests,
|
|
1291
|
-
added.inlineManifests
|
|
1292
|
-
);
|
|
1293
|
-
const packages = unique([
|
|
1294
|
-
...base.packages ?? [],
|
|
1295
|
-
...added.packages ?? []
|
|
1296
|
-
]);
|
|
1297
|
-
const manifests = base.manifests || added.manifests ? { ...base.manifests, ...added.manifests } : void 0;
|
|
1298
|
-
return {
|
|
1299
|
-
...inlineManifests ? { inlineManifests } : {},
|
|
1300
|
-
...packages.length > 0 ? { packages } : {},
|
|
1301
|
-
...manifests ? { manifests } : {}
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
function packageNamesFromContext(context, catalog) {
|
|
1305
|
-
return unique([
|
|
1306
|
-
...context.pluginSet?.packageNames ?? [],
|
|
1307
|
-
...catalog?.packages ?? []
|
|
1308
|
-
]);
|
|
1309
|
-
}
|
|
1310
|
-
async function resolveUpgradePlugins(context) {
|
|
1311
|
-
const catalog = baseCatalogConfig(context);
|
|
1312
|
-
const packageNames = packageNamesFromContext(context, catalog);
|
|
1313
|
-
const registrations = context.pluginSet?.registrations ?? [];
|
|
1314
|
-
const manifests = context.pluginSet?.manifests || catalog?.manifests ? {
|
|
1315
|
-
...catalog?.manifests,
|
|
1316
|
-
...context.pluginSet?.manifests
|
|
1317
|
-
} : void 0;
|
|
1318
|
-
const pluginSet = packageNames.length > 0 || registrations.length > 0 || context.pluginSet ? defineJuniorPlugins(
|
|
1319
|
-
[...packageNames, ...registrations],
|
|
1320
|
-
manifests ? { manifests } : {}
|
|
1321
|
-
) : void 0;
|
|
1322
|
-
return {
|
|
1323
|
-
pluginCatalogConfig: mergeCatalogConfig(
|
|
1324
|
-
catalog,
|
|
1325
|
-
pluginCatalogConfigFromPluginSet(pluginSet)
|
|
1326
|
-
),
|
|
1327
|
-
...pluginSet ? { pluginSet } : {}
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// src/cli/upgrade/migrations/plugin-storage.ts
|
|
1332
|
-
function emptyResult() {
|
|
1333
|
-
return {
|
|
1334
|
-
existing: 0,
|
|
1335
|
-
migrated: 0,
|
|
1336
|
-
missing: 0,
|
|
1337
|
-
scanned: 0
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
function addResult(left, right) {
|
|
1341
|
-
return {
|
|
1342
|
-
existing: left.existing + right.existing,
|
|
1343
|
-
migrated: left.migrated + right.migrated,
|
|
1344
|
-
missing: left.missing + right.missing,
|
|
1345
|
-
scanned: left.scanned + right.scanned,
|
|
1346
|
-
...left.skipped !== void 0 || right.skipped !== void 0 ? { skipped: (left.skipped ?? 0) + (right.skipped ?? 0) } : {}
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
function dbForPlugin(context, sqlUrlDb) {
|
|
1350
|
-
return context.db ?? sqlUrlDb ?? getDb();
|
|
1351
|
-
}
|
|
1352
|
-
async function runPluginStorageMigrations(context) {
|
|
1353
|
-
const { pluginCatalogConfig, pluginSet } = await resolveUpgradePlugins(context);
|
|
1354
|
-
if (!pluginSet) {
|
|
1355
|
-
return emptyResult();
|
|
1356
|
-
}
|
|
1357
|
-
const previousConfig = pluginCatalogRuntime.setConfig(pluginCatalogConfig);
|
|
1358
|
-
const sql2 = getChatConfig().sql;
|
|
1359
|
-
const ownedExecutor = context.db ? void 0 : createJuniorSqlExecutor({
|
|
1360
|
-
connectionString: sql2.databaseUrl,
|
|
1361
|
-
driver: sql2.driver
|
|
1362
|
-
});
|
|
1363
|
-
const sqlUrlDb = ownedExecutor ? ownedExecutor.db() : void 0;
|
|
1364
|
-
try {
|
|
1365
|
-
let result = emptyResult();
|
|
1366
|
-
const plugins = pluginRuntimeRegistrationsFromPluginSet(pluginSet).filter((plugin) => plugin.hooks?.migrateStorage).sort(
|
|
1367
|
-
(left, right) => left.manifest.name.localeCompare(right.manifest.name)
|
|
1368
|
-
);
|
|
1369
|
-
for (const plugin of plugins) {
|
|
1370
|
-
const pluginName = plugin.manifest.name;
|
|
1371
|
-
const hook = plugin.hooks?.migrateStorage;
|
|
1372
|
-
if (!hook) {
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
const db = dbForPlugin(context, sqlUrlDb);
|
|
1376
|
-
const pluginResult = await hook({
|
|
1377
|
-
db,
|
|
1378
|
-
log: createPluginLogger(pluginName),
|
|
1379
|
-
plugin: { name: pluginName },
|
|
1380
|
-
state: createPluginState(pluginName, context.stateAdapter)
|
|
1381
|
-
});
|
|
1382
|
-
if (pluginResult) {
|
|
1383
|
-
result = addResult(result, pluginResult);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
return result;
|
|
1387
|
-
} finally {
|
|
1388
|
-
pluginCatalogRuntime.setConfig(previousConfig);
|
|
1389
|
-
await ownedExecutor?.close();
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
var pluginStorageMigration = {
|
|
1393
|
-
name: "run-plugin-storage-migrations",
|
|
1394
|
-
run: runPluginStorageMigrations
|
|
1395
|
-
};
|
|
1396
|
-
|
|
1397
|
-
// src/chat/plugins/migrations.ts
|
|
1398
|
-
import { createHash } from "crypto";
|
|
1399
|
-
import { readMigrationFiles as readMigrationFiles2 } from "drizzle-orm/migrator";
|
|
1400
|
-
var LEGACY_SCHEDULER_BASELINE_HASH = "d1d2f712181dd3a0557808f0fc67fd0722691d25f4c8cfb816b77c71d19e1e42";
|
|
1401
|
-
function migrationTable(pluginName) {
|
|
1402
|
-
const label = pluginName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32);
|
|
1403
|
-
const hash = createHash("sha256").update(pluginName).digest("hex").slice(0, 8);
|
|
1404
|
-
return `__drizzle_${label}_${hash}`;
|
|
1405
|
-
}
|
|
1406
|
-
async function appliedMigrationTime(executor, table) {
|
|
1407
|
-
const [exists] = await executor.query(
|
|
1408
|
-
`SELECT to_regclass($1)::text AS "tableName"`,
|
|
1409
|
-
[`drizzle.${table}`]
|
|
1410
|
-
);
|
|
1411
|
-
if (!exists?.tableName) {
|
|
1412
|
-
return void 0;
|
|
1413
|
-
}
|
|
1414
|
-
const [row] = await executor.query(
|
|
1415
|
-
`SELECT created_at::text AS "createdAt"
|
|
1416
|
-
FROM drizzle.${table}
|
|
1417
|
-
ORDER BY created_at DESC
|
|
1418
|
-
LIMIT 1`
|
|
1419
|
-
);
|
|
1420
|
-
return row?.createdAt === null || row?.createdAt === void 0 ? void 0 : Number(row.createdAt);
|
|
1421
|
-
}
|
|
1422
|
-
async function legacyMigrationHashes(executor, pluginName) {
|
|
1423
|
-
const [exists] = await executor.query(
|
|
1424
|
-
`SELECT to_regclass('public.junior_schema_migrations')::text AS "tableName"`
|
|
1425
|
-
);
|
|
1426
|
-
if (!exists?.tableName) {
|
|
1427
|
-
return /* @__PURE__ */ new Set();
|
|
1428
|
-
}
|
|
1429
|
-
const rows = await executor.query(
|
|
1430
|
-
`SELECT checksum
|
|
1431
|
-
FROM junior_schema_migrations
|
|
1432
|
-
WHERE id LIKE $1
|
|
1433
|
-
ORDER BY id ASC`,
|
|
1434
|
-
[`plugin:${pluginName}/%`]
|
|
1435
|
-
);
|
|
1436
|
-
return new Set(rows.map((row) => row.checksum));
|
|
1437
|
-
}
|
|
1438
|
-
function adoptedMigration(migrations, legacyHashes, pluginName) {
|
|
1439
|
-
let adopted;
|
|
1440
|
-
for (const migration of migrations) {
|
|
1441
|
-
if (!legacyHashes.has(migration.hash)) {
|
|
1442
|
-
break;
|
|
1443
|
-
}
|
|
1444
|
-
adopted = migration;
|
|
1445
|
-
}
|
|
1446
|
-
if (!adopted && pluginName === "scheduler" && legacyHashes.has(LEGACY_SCHEDULER_BASELINE_HASH)) {
|
|
1447
|
-
return migrations[0];
|
|
1448
|
-
}
|
|
1449
|
-
return adopted;
|
|
1450
|
-
}
|
|
1451
|
-
async function adoptLegacyMigrationState2(args) {
|
|
1452
|
-
const legacyHashes = await legacyMigrationHashes(
|
|
1453
|
-
args.executor,
|
|
1454
|
-
args.pluginName
|
|
1455
|
-
);
|
|
1456
|
-
const migration = adoptedMigration(
|
|
1457
|
-
args.migrations,
|
|
1458
|
-
legacyHashes,
|
|
1459
|
-
args.pluginName
|
|
1460
|
-
);
|
|
1461
|
-
if (!migration) {
|
|
1462
|
-
return void 0;
|
|
1463
|
-
}
|
|
1464
|
-
await args.executor.transaction(async () => {
|
|
1465
|
-
await args.executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
|
|
1466
|
-
await args.executor.execute(`
|
|
1467
|
-
CREATE TABLE IF NOT EXISTS drizzle.${args.table} (
|
|
1468
|
-
id SERIAL PRIMARY KEY,
|
|
1469
|
-
hash TEXT NOT NULL,
|
|
1470
|
-
created_at BIGINT
|
|
1471
|
-
)
|
|
1472
|
-
`);
|
|
1473
|
-
await args.executor.execute(
|
|
1474
|
-
`INSERT INTO drizzle.${args.table} (hash, created_at) VALUES ($1, $2)`,
|
|
1475
|
-
[migration.hash, migration.folderMillis]
|
|
1476
|
-
);
|
|
1477
|
-
});
|
|
1478
|
-
return migration.folderMillis;
|
|
1479
|
-
}
|
|
1480
|
-
function appliedCount(migrations, createdAt) {
|
|
1481
|
-
return createdAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= createdAt).length;
|
|
1482
|
-
}
|
|
1483
|
-
async function migratePluginSchemas(executor, roots) {
|
|
1484
|
-
const result = {
|
|
1485
|
-
existing: 0,
|
|
1486
|
-
migrated: 0,
|
|
1487
|
-
scanned: 0
|
|
1488
|
-
};
|
|
1489
|
-
const orderedRoots = [...roots].sort(
|
|
1490
|
-
(left, right) => left.pluginName.localeCompare(right.pluginName)
|
|
1491
|
-
);
|
|
1492
|
-
for (const root of orderedRoots) {
|
|
1493
|
-
const migrations = readMigrationFiles2({ migrationsFolder: root.dir });
|
|
1494
|
-
const table = migrationTable(root.pluginName);
|
|
1495
|
-
await executor.withMigrationLock(table, async () => {
|
|
1496
|
-
const currentTime = await appliedMigrationTime(executor, table) ?? await adoptLegacyMigrationState2({
|
|
1497
|
-
executor,
|
|
1498
|
-
migrations,
|
|
1499
|
-
pluginName: root.pluginName,
|
|
1500
|
-
table
|
|
1501
|
-
});
|
|
1502
|
-
const existing = appliedCount(migrations, currentTime);
|
|
1503
|
-
await executor.migrate({
|
|
1504
|
-
migrationsFolder: root.dir,
|
|
1505
|
-
migrationsTable: table
|
|
1506
|
-
});
|
|
1507
|
-
result.scanned += migrations.length;
|
|
1508
|
-
result.existing += existing;
|
|
1509
|
-
result.migrated += migrations.length - existing;
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
return result;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// src/cli/upgrade/migrations/plugin-sql.ts
|
|
1516
|
-
async function migratePluginsToSql(context) {
|
|
1517
|
-
const { sql: sql2 } = getChatConfig();
|
|
1518
|
-
const { pluginCatalogConfig } = await resolveUpgradePlugins(context);
|
|
1519
|
-
const previousConfig = pluginCatalogRuntime.setConfig(pluginCatalogConfig);
|
|
1520
|
-
const executor = createJuniorSqlExecutor({
|
|
1521
|
-
connectionString: sql2.databaseUrl,
|
|
1522
|
-
driver: sql2.driver
|
|
1523
|
-
});
|
|
1524
|
-
try {
|
|
1525
|
-
const result = await migratePluginSchemas(
|
|
1526
|
-
executor,
|
|
1527
|
-
pluginCatalogRuntime.getMigrationRoots()
|
|
1528
|
-
);
|
|
1529
|
-
return {
|
|
1530
|
-
existing: result.existing,
|
|
1531
|
-
migrated: result.migrated,
|
|
1532
|
-
missing: 0,
|
|
1533
|
-
scanned: result.scanned
|
|
1534
|
-
};
|
|
1535
|
-
} finally {
|
|
1536
|
-
pluginCatalogRuntime.setConfig(previousConfig);
|
|
1537
|
-
await executor.close();
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
var sqlPluginMigration = {
|
|
1541
|
-
name: "migrate-plugin-sql",
|
|
1542
|
-
run: migratePluginsToSql
|
|
1543
|
-
};
|
|
1544
|
-
|
|
1545
|
-
// src/cli/upgrade/migrations/redis-conversation-state.ts
|
|
1546
|
-
var CONVERSATION_PREFIX = "junior:conversation";
|
|
1547
|
-
var CONVERSATION_SCHEMA_VERSION = 1;
|
|
1548
|
-
var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
|
|
1549
|
-
var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
|
|
1550
|
-
var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
|
|
1551
|
-
var LEGACY_CONVERSATION_WORK_PREFIX = "junior:conversation-work";
|
|
1552
|
-
var LEGACY_CONVERSATION_WORK_SCHEMA_VERSION = 1;
|
|
1553
|
-
var LEGACY_CONVERSATION_WORK_INDEX_KEY = `${LEGACY_CONVERSATION_WORK_PREFIX}:index`;
|
|
1554
|
-
var AGENT_TURN_SESSION_PREFIX2 = "junior:agent_turn_session";
|
|
1555
|
-
var AGENT_TURN_SESSION_INDEX_KEY = `${AGENT_TURN_SESSION_PREFIX2}:index`;
|
|
1556
|
-
var THREAD_STATE_PREFIX = "thread-state";
|
|
1557
|
-
function conversationKey(conversationId) {
|
|
1558
|
-
return `${CONVERSATION_PREFIX}:${conversationId}`;
|
|
1559
|
-
}
|
|
1560
|
-
function legacyConversationWorkKey(conversationId) {
|
|
1561
|
-
return `${LEGACY_CONVERSATION_WORK_PREFIX}:state:${conversationId}`;
|
|
1562
|
-
}
|
|
1563
|
-
function threadStateKey(conversationId) {
|
|
1564
|
-
return `${THREAD_STATE_PREFIX}:${conversationId}`;
|
|
1565
|
-
}
|
|
1566
|
-
function uniqueStrings(values) {
|
|
1567
|
-
return [...new Set(values)];
|
|
1568
|
-
}
|
|
1569
|
-
function uniqueStringValues(value) {
|
|
1570
|
-
if (!Array.isArray(value)) {
|
|
1571
|
-
return [];
|
|
1572
|
-
}
|
|
1573
|
-
return uniqueStrings(
|
|
1574
|
-
value.map((value2) => typeof value2 === "string" ? value2 : void 0).filter((value2) => Boolean(value2))
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
function normalizeSource(value) {
|
|
1578
|
-
if (value === "api" || value === "internal" || value === "plugin" || value === "scheduler" || value === "slack") {
|
|
1579
|
-
return value;
|
|
1580
|
-
}
|
|
1581
|
-
return void 0;
|
|
1582
|
-
}
|
|
1583
|
-
function normalizeMetadata(value) {
|
|
1584
|
-
if (!isRecord(value)) {
|
|
1585
|
-
return void 0;
|
|
1586
|
-
}
|
|
1587
|
-
return value;
|
|
1588
|
-
}
|
|
1589
|
-
function normalizeInput(value) {
|
|
1590
|
-
if (!isRecord(value)) {
|
|
1591
|
-
return void 0;
|
|
1592
|
-
}
|
|
1593
|
-
const text = toOptionalString(value.text);
|
|
1594
|
-
if (!text) {
|
|
1595
|
-
return void 0;
|
|
1596
|
-
}
|
|
1597
|
-
return {
|
|
1598
|
-
text,
|
|
1599
|
-
authorId: toOptionalString(value.authorId),
|
|
1600
|
-
attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
|
|
1601
|
-
metadata: normalizeMetadata(value.metadata)
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
function normalizeMessage(value) {
|
|
1605
|
-
if (!isRecord(value)) {
|
|
1606
|
-
return void 0;
|
|
1607
|
-
}
|
|
1608
|
-
const conversationId = toOptionalString(value.conversationId);
|
|
1609
|
-
const inboundMessageId = toOptionalString(value.inboundMessageId);
|
|
1610
|
-
const source = normalizeSource(value.source);
|
|
1611
|
-
const destination = parseDestination(value.destination);
|
|
1612
|
-
const createdAtMs = toOptionalNumber(value.createdAtMs);
|
|
1613
|
-
const receivedAtMs = toOptionalNumber(value.receivedAtMs);
|
|
1614
|
-
const input = normalizeInput(value.input);
|
|
1615
|
-
if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
|
|
1616
|
-
return void 0;
|
|
1617
|
-
}
|
|
1618
|
-
return {
|
|
1619
|
-
conversationId,
|
|
1620
|
-
destination,
|
|
1621
|
-
inboundMessageId,
|
|
1622
|
-
source,
|
|
1623
|
-
createdAtMs,
|
|
1624
|
-
receivedAtMs,
|
|
1625
|
-
input,
|
|
1626
|
-
injectedAtMs: toOptionalNumber(value.injectedAtMs)
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
function normalizeLegacyLease(value) {
|
|
1630
|
-
if (!isRecord(value)) {
|
|
1631
|
-
return void 0;
|
|
1632
|
-
}
|
|
1633
|
-
const token = toOptionalString(value.leaseToken);
|
|
1634
|
-
const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
|
|
1635
|
-
const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
|
|
1636
|
-
const expiresAtMs = toOptionalNumber(value.leaseExpiresAtMs);
|
|
1637
|
-
if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
|
|
1638
|
-
return void 0;
|
|
1639
|
-
}
|
|
1640
|
-
return {
|
|
1641
|
-
token,
|
|
1642
|
-
acquiredAtMs,
|
|
1643
|
-
lastCheckInAtMs,
|
|
1644
|
-
expiresAtMs
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
function compareMessages(left, right) {
|
|
1648
|
-
return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
|
|
1649
|
-
}
|
|
1650
|
-
function normalizeLegacyConversation(conversationId, value) {
|
|
1651
|
-
if (!isRecord(value) || value.schemaVersion !== LEGACY_CONVERSATION_WORK_SCHEMA_VERSION) {
|
|
1652
|
-
return void 0;
|
|
1653
|
-
}
|
|
1654
|
-
const storedConversationId = toOptionalString(value.conversationId);
|
|
1655
|
-
const destination = parseDestination(value.destination);
|
|
1656
|
-
const updatedAtMs = toOptionalNumber(value.updatedAtMs);
|
|
1657
|
-
if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
|
|
1658
|
-
return void 0;
|
|
1659
|
-
}
|
|
1660
|
-
const normalizedMessages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)) : [];
|
|
1661
|
-
if (normalizedMessages.some(
|
|
1662
|
-
(message) => message.conversationId === conversationId && !sameDestination(message.destination, destination)
|
|
1663
|
-
)) {
|
|
1664
|
-
return void 0;
|
|
1665
|
-
}
|
|
1666
|
-
const messages = normalizedMessages.filter((message) => message.conversationId === conversationId).sort(compareMessages);
|
|
1667
|
-
const pendingMessages = messages.filter(
|
|
1668
|
-
(message) => message.injectedAtMs === void 0
|
|
1669
|
-
);
|
|
1670
|
-
const lease = normalizeLegacyLease(value.lease);
|
|
1671
|
-
const needsRun = value.needsRun === true || pendingMessages.length > 0;
|
|
1672
|
-
const status = lease ? value.needsRun === true ? "awaiting_resume" : "running" : needsRun ? "pending" : "idle";
|
|
1673
|
-
const messageTimes = messages.flatMap((message) => [
|
|
1674
|
-
message.createdAtMs,
|
|
1675
|
-
message.receivedAtMs
|
|
1676
|
-
]);
|
|
1677
|
-
const createdAtMs = messageTimes.length > 0 ? Math.min(...messageTimes) : updatedAtMs;
|
|
1678
|
-
const lastActivityAtMs = messageTimes.length > 0 ? Math.max(...messageTimes) : updatedAtMs;
|
|
1679
|
-
return {
|
|
1680
|
-
schemaVersion: CONVERSATION_SCHEMA_VERSION,
|
|
1681
|
-
conversationId,
|
|
1682
|
-
createdAtMs,
|
|
1683
|
-
destination,
|
|
1684
|
-
lastActivityAtMs,
|
|
1685
|
-
source: messages[0]?.source,
|
|
1686
|
-
updatedAtMs,
|
|
1687
|
-
execution: {
|
|
1688
|
-
status,
|
|
1689
|
-
inboundMessageIds: uniqueStrings(
|
|
1690
|
-
messages.map((message) => message.inboundMessageId)
|
|
1691
|
-
),
|
|
1692
|
-
pendingCount: pendingMessages.length,
|
|
1693
|
-
pendingMessages,
|
|
1694
|
-
...lease ? { lease } : {},
|
|
1695
|
-
lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
|
|
1696
|
-
updatedAtMs
|
|
1697
|
-
}
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
function mergeLegacyConversation(existing, legacy) {
|
|
1701
|
-
if (existing.destination && legacy.destination && !sameDestination(existing.destination, legacy.destination)) {
|
|
1702
|
-
throw new Error(
|
|
1703
|
-
`Legacy conversation work destination does not match conversation ${existing.conversationId}`
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1706
|
-
const knownInboundIds = new Set(existing.execution.inboundMessageIds);
|
|
1707
|
-
const pendingMessages = [
|
|
1708
|
-
...existing.execution.pendingMessages,
|
|
1709
|
-
...legacy.execution.pendingMessages.filter(
|
|
1710
|
-
(message) => !knownInboundIds.has(message.inboundMessageId)
|
|
1711
|
-
)
|
|
1712
|
-
].sort(compareMessages);
|
|
1713
|
-
const legacyIsRunnable = legacy.execution.status !== "idle";
|
|
1714
|
-
const existingIsIdle = existing.execution.status === "idle";
|
|
1715
|
-
const legacyLease = existingIsIdle && legacy.execution.lease ? { lease: legacy.execution.lease } : {};
|
|
1716
|
-
const legacyRunId = existingIsIdle && legacy.execution.runId ? { runId: legacy.execution.runId } : {};
|
|
1717
|
-
const legacyCheckpoint = existing.execution.lastCheckpointAtMs === void 0 && legacy.execution.lastCheckpointAtMs !== void 0 ? { lastCheckpointAtMs: legacy.execution.lastCheckpointAtMs } : {};
|
|
1718
|
-
const legacyEnqueue = existing.execution.lastEnqueuedAtMs === void 0 && legacy.execution.lastEnqueuedAtMs !== void 0 ? { lastEnqueuedAtMs: legacy.execution.lastEnqueuedAtMs } : {};
|
|
1719
|
-
const executionUpdatedAtMs = Math.max(
|
|
1720
|
-
existing.execution.updatedAtMs ?? existing.updatedAtMs,
|
|
1721
|
-
legacy.execution.updatedAtMs ?? legacy.updatedAtMs
|
|
1722
|
-
);
|
|
1723
|
-
const status = existingIsIdle && legacyIsRunnable ? legacy.execution.lease ? legacy.execution.status : "pending" : pendingMessages.length > 0 && existingIsIdle ? "pending" : existing.execution.status;
|
|
1724
|
-
return {
|
|
1725
|
-
...existing,
|
|
1726
|
-
destination: existing.destination ?? legacy.destination,
|
|
1727
|
-
source: existing.source ?? legacy.source,
|
|
1728
|
-
createdAtMs: Math.min(existing.createdAtMs, legacy.createdAtMs),
|
|
1729
|
-
lastActivityAtMs: Math.max(
|
|
1730
|
-
existing.lastActivityAtMs,
|
|
1731
|
-
legacy.lastActivityAtMs
|
|
1732
|
-
),
|
|
1733
|
-
updatedAtMs: Math.max(existing.updatedAtMs, legacy.updatedAtMs),
|
|
1734
|
-
execution: {
|
|
1735
|
-
...existing.execution,
|
|
1736
|
-
...legacyLease,
|
|
1737
|
-
...legacyRunId,
|
|
1738
|
-
...legacyCheckpoint,
|
|
1739
|
-
...legacyEnqueue,
|
|
1740
|
-
status,
|
|
1741
|
-
inboundMessageIds: uniqueStrings([
|
|
1742
|
-
...existing.execution.inboundMessageIds,
|
|
1743
|
-
...legacy.execution.inboundMessageIds
|
|
1744
|
-
]),
|
|
1745
|
-
pendingCount: pendingMessages.length,
|
|
1746
|
-
pendingMessages,
|
|
1747
|
-
updatedAtMs: executionUpdatedAtMs
|
|
1748
|
-
}
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
function compareIndexDescending(left, right) {
|
|
1752
|
-
return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
|
|
1753
|
-
}
|
|
1754
|
-
function compareIndexAscending(left, right) {
|
|
1755
|
-
return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
|
|
1756
|
-
}
|
|
1757
|
-
function normalizeIndexEntry(value) {
|
|
1758
|
-
if (!isRecord(value)) {
|
|
1759
|
-
return void 0;
|
|
1760
|
-
}
|
|
1761
|
-
const conversationId = toOptionalString(value.conversationId);
|
|
1762
|
-
const score = toOptionalNumber(value.score);
|
|
1763
|
-
if (!conversationId || typeof score !== "number") {
|
|
1764
|
-
return void 0;
|
|
1765
|
-
}
|
|
1766
|
-
return { conversationId, score };
|
|
1767
|
-
}
|
|
1768
|
-
function uniqueIndexEntries(value) {
|
|
1769
|
-
if (!Array.isArray(value)) {
|
|
1770
|
-
return [];
|
|
1771
|
-
}
|
|
1772
|
-
const entries = /* @__PURE__ */ new Map();
|
|
1773
|
-
for (const item of value) {
|
|
1774
|
-
const entry = normalizeIndexEntry(item);
|
|
1775
|
-
if (!entry) {
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
const existing = entries.get(entry.conversationId);
|
|
1779
|
-
if (!existing || entry.score > existing.score) {
|
|
1780
|
-
entries.set(entry.conversationId, entry);
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
return [...entries.values()];
|
|
1784
|
-
}
|
|
1785
|
-
function normalizeAwaitingContinuationSummary(value) {
|
|
1786
|
-
if (!isRecord(value)) {
|
|
1787
|
-
return void 0;
|
|
1788
|
-
}
|
|
1789
|
-
const conversationId = toOptionalString(value.conversationId);
|
|
1790
|
-
const sessionId = toOptionalString(value.sessionId);
|
|
1791
|
-
const state = value.state;
|
|
1792
|
-
const resumeReason = value.resumeReason;
|
|
1793
|
-
const destination = parseDestination(value.destination);
|
|
1794
|
-
const updatedAtMs = toOptionalNumber(value.updatedAtMs);
|
|
1795
|
-
if (!conversationId || !sessionId || state !== "awaiting_resume" || resumeReason !== "timeout" && resumeReason !== "yield" || !destination || typeof updatedAtMs !== "number") {
|
|
1796
|
-
return void 0;
|
|
1797
|
-
}
|
|
1798
|
-
return {
|
|
1799
|
-
conversationId,
|
|
1800
|
-
destination,
|
|
1801
|
-
resumeReason,
|
|
1802
|
-
sessionId,
|
|
1803
|
-
state,
|
|
1804
|
-
updatedAtMs
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
function uniqueAwaitingContinuationSummaries(values) {
|
|
1808
|
-
const summaries = /* @__PURE__ */ new Map();
|
|
1809
|
-
for (const value of [...values].reverse()) {
|
|
1810
|
-
const summary = normalizeAwaitingContinuationSummary(value);
|
|
1811
|
-
if (!summary) {
|
|
1812
|
-
continue;
|
|
1813
|
-
}
|
|
1814
|
-
const key3 = `${summary.conversationId}:${summary.sessionId}`;
|
|
1815
|
-
if (!summaries.has(key3)) {
|
|
1816
|
-
summaries.set(key3, summary);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
return [...summaries.values()];
|
|
1820
|
-
}
|
|
1821
|
-
function redisIndexKey(indexKey) {
|
|
1822
|
-
const prefix = getChatConfig().state.keyPrefix;
|
|
1823
|
-
return [...prefix ? [prefix] : [], indexKey].join(":");
|
|
1824
|
-
}
|
|
1825
|
-
async function upsertEmulatedIndexEntry(args) {
|
|
1826
|
-
const existing = uniqueIndexEntries(
|
|
1827
|
-
await args.stateAdapter.get(args.indexKey)
|
|
1828
|
-
);
|
|
1829
|
-
const next = [
|
|
1830
|
-
...existing.filter((entry) => entry.conversationId !== args.conversationId),
|
|
1831
|
-
{ conversationId: args.conversationId, score: args.score }
|
|
1832
|
-
];
|
|
1833
|
-
const retained = args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY ? next.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH) : next.sort(compareIndexAscending);
|
|
1834
|
-
await args.stateAdapter.set(
|
|
1835
|
-
args.indexKey,
|
|
1836
|
-
retained,
|
|
1837
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
1838
|
-
);
|
|
1839
|
-
}
|
|
1840
|
-
async function removeEmulatedIndexEntry(args) {
|
|
1841
|
-
const existing = uniqueIndexEntries(
|
|
1842
|
-
await args.stateAdapter.get(args.indexKey)
|
|
1843
|
-
);
|
|
1844
|
-
const next = existing.filter(
|
|
1845
|
-
(entry) => entry.conversationId !== args.conversationId
|
|
1846
|
-
);
|
|
1847
|
-
if (next.length === existing.length) {
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
await args.stateAdapter.set(args.indexKey, next, JUNIOR_THREAD_STATE_TTL_MS);
|
|
1851
|
-
}
|
|
1852
|
-
async function upsertRedisIndexEntry(args) {
|
|
1853
|
-
const key3 = redisIndexKey(args.indexKey);
|
|
1854
|
-
if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
|
|
1855
|
-
const upsertBoundedActivityScript = `
|
|
1856
|
-
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
|
|
1857
|
-
redis.call("PEXPIRE", KEYS[1], ARGV[3])
|
|
1858
|
-
local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
|
|
1859
|
-
if extra > 0 then
|
|
1860
|
-
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
|
|
1861
|
-
end
|
|
1862
|
-
return 1
|
|
1863
|
-
`;
|
|
1864
|
-
await args.client.sendCommand([
|
|
1865
|
-
"EVAL",
|
|
1866
|
-
upsertBoundedActivityScript,
|
|
1867
|
-
"1",
|
|
1868
|
-
key3,
|
|
1869
|
-
String(args.score),
|
|
1870
|
-
args.conversationId,
|
|
1871
|
-
String(JUNIOR_THREAD_STATE_TTL_MS),
|
|
1872
|
-
String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
|
|
1873
|
-
]);
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
await args.client.sendCommand([
|
|
1877
|
-
"ZADD",
|
|
1878
|
-
key3,
|
|
1879
|
-
String(args.score),
|
|
1880
|
-
args.conversationId
|
|
1881
|
-
]);
|
|
1882
|
-
await args.client.sendCommand([
|
|
1883
|
-
"PEXPIRE",
|
|
1884
|
-
key3,
|
|
1885
|
-
String(JUNIOR_THREAD_STATE_TTL_MS)
|
|
1886
|
-
]);
|
|
1887
|
-
}
|
|
1888
|
-
async function removeRedisIndexEntry(args) {
|
|
1889
|
-
await args.client.sendCommand([
|
|
1890
|
-
"ZREM",
|
|
1891
|
-
redisIndexKey(args.indexKey),
|
|
1892
|
-
args.conversationId
|
|
1893
|
-
]);
|
|
1894
|
-
}
|
|
1895
|
-
async function upsertConversationIndexes(args) {
|
|
1896
|
-
const redisClient = args.redisStateAdapter?.getClient();
|
|
1897
|
-
const upsert = redisClient ? (indexKey, score) => upsertRedisIndexEntry({
|
|
1898
|
-
client: redisClient,
|
|
1899
|
-
conversationId: args.conversation.conversationId,
|
|
1900
|
-
indexKey,
|
|
1901
|
-
score
|
|
1902
|
-
}) : (indexKey, score) => upsertEmulatedIndexEntry({
|
|
1903
|
-
stateAdapter: args.stateAdapter,
|
|
1904
|
-
conversationId: args.conversation.conversationId,
|
|
1905
|
-
indexKey,
|
|
1906
|
-
score
|
|
1907
|
-
});
|
|
1908
|
-
const remove = redisClient ? (indexKey) => removeRedisIndexEntry({
|
|
1909
|
-
client: redisClient,
|
|
1910
|
-
conversationId: args.conversation.conversationId,
|
|
1911
|
-
indexKey
|
|
1912
|
-
}) : (indexKey) => removeEmulatedIndexEntry({
|
|
1913
|
-
stateAdapter: args.stateAdapter,
|
|
1914
|
-
conversationId: args.conversation.conversationId,
|
|
1915
|
-
indexKey
|
|
1916
|
-
});
|
|
1917
|
-
await upsert(
|
|
1918
|
-
CONVERSATION_BY_ACTIVITY_INDEX_KEY,
|
|
1919
|
-
args.conversation.lastActivityAtMs
|
|
1920
|
-
);
|
|
1921
|
-
if (args.conversation.execution.status === "idle") {
|
|
1922
|
-
await remove(CONVERSATION_ACTIVE_INDEX_KEY);
|
|
1923
|
-
return;
|
|
1924
|
-
}
|
|
1925
|
-
await upsert(
|
|
1926
|
-
CONVERSATION_ACTIVE_INDEX_KEY,
|
|
1927
|
-
args.conversation.execution.updatedAtMs ?? args.conversation.updatedAtMs
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
1930
|
-
async function removeLegacyIndexEntry(args) {
|
|
1931
|
-
const existing = uniqueStringValues(
|
|
1932
|
-
await args.stateAdapter.get(LEGACY_CONVERSATION_WORK_INDEX_KEY)
|
|
1933
|
-
);
|
|
1934
|
-
const next = existing.filter((id) => id !== args.conversationId);
|
|
1935
|
-
if (next.length === existing.length) {
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
if (next.length === 0) {
|
|
1939
|
-
await args.stateAdapter.delete(LEGACY_CONVERSATION_WORK_INDEX_KEY);
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
await args.stateAdapter.set(
|
|
1943
|
-
LEGACY_CONVERSATION_WORK_INDEX_KEY,
|
|
1944
|
-
next,
|
|
1945
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
async function migrateLegacyConversationWorkRedisState(context) {
|
|
1949
|
-
const legacyIds = uniqueStringValues(
|
|
1950
|
-
await context.stateAdapter.get(LEGACY_CONVERSATION_WORK_INDEX_KEY)
|
|
1951
|
-
);
|
|
1952
|
-
const result = {
|
|
1953
|
-
existing: 0,
|
|
1954
|
-
migrated: 0,
|
|
1955
|
-
missing: 0,
|
|
1956
|
-
scanned: legacyIds.length
|
|
1957
|
-
};
|
|
1958
|
-
for (const conversationId of legacyIds) {
|
|
1959
|
-
const legacyKey = legacyConversationWorkKey(conversationId);
|
|
1960
|
-
const raw = await context.stateAdapter.get(legacyKey);
|
|
1961
|
-
if (raw == null) {
|
|
1962
|
-
result.missing += 1;
|
|
1963
|
-
await removeLegacyIndexEntry({
|
|
1964
|
-
conversationId,
|
|
1965
|
-
stateAdapter: context.stateAdapter
|
|
1966
|
-
});
|
|
1967
|
-
continue;
|
|
1968
|
-
}
|
|
1969
|
-
const conversation = normalizeLegacyConversation(conversationId, raw);
|
|
1970
|
-
if (!conversation) {
|
|
1971
|
-
throw new Error(
|
|
1972
|
-
`Legacy conversation work state is invalid for ${conversationId}`
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
|
-
const existingConversation = await getConversation({
|
|
1976
|
-
conversationId,
|
|
1977
|
-
state: context.stateAdapter
|
|
1978
|
-
});
|
|
1979
|
-
if (existingConversation) {
|
|
1980
|
-
const mergedConversation = mergeLegacyConversation(
|
|
1981
|
-
existingConversation,
|
|
1982
|
-
conversation
|
|
1983
|
-
);
|
|
1984
|
-
await context.stateAdapter.set(
|
|
1985
|
-
conversationKey(conversationId),
|
|
1986
|
-
mergedConversation,
|
|
1987
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
1988
|
-
);
|
|
1989
|
-
await upsertConversationIndexes({
|
|
1990
|
-
conversation: mergedConversation,
|
|
1991
|
-
redisStateAdapter: context.redisStateAdapter,
|
|
1992
|
-
stateAdapter: context.stateAdapter
|
|
1993
|
-
});
|
|
1994
|
-
result.existing += 1;
|
|
1995
|
-
await context.stateAdapter.delete(legacyKey);
|
|
1996
|
-
await removeLegacyIndexEntry({
|
|
1997
|
-
conversationId,
|
|
1998
|
-
stateAdapter: context.stateAdapter
|
|
1999
|
-
});
|
|
2000
|
-
continue;
|
|
2001
|
-
}
|
|
2002
|
-
await context.stateAdapter.set(
|
|
2003
|
-
conversationKey(conversationId),
|
|
2004
|
-
conversation,
|
|
2005
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
2006
|
-
);
|
|
2007
|
-
await upsertConversationIndexes({
|
|
2008
|
-
conversation,
|
|
2009
|
-
redisStateAdapter: context.redisStateAdapter,
|
|
2010
|
-
stateAdapter: context.stateAdapter
|
|
2011
|
-
});
|
|
2012
|
-
await context.stateAdapter.delete(legacyKey);
|
|
2013
|
-
await removeLegacyIndexEntry({
|
|
2014
|
-
conversationId,
|
|
2015
|
-
stateAdapter: context.stateAdapter
|
|
2016
|
-
});
|
|
2017
|
-
result.migrated += 1;
|
|
2018
|
-
}
|
|
2019
|
-
return result;
|
|
2020
|
-
}
|
|
2021
|
-
async function isActiveContinuationSummary(context, summary) {
|
|
2022
|
-
const rawState = await context.stateAdapter.get(
|
|
2023
|
-
threadStateKey(summary.conversationId)
|
|
2024
|
-
) ?? {};
|
|
2025
|
-
const conversation = coerceThreadConversationState(rawState);
|
|
2026
|
-
return conversation.processing.activeTurnId === summary.sessionId;
|
|
2027
|
-
}
|
|
2028
|
-
async function seedAwaitingContinuationConversationWork(context, result) {
|
|
2029
|
-
const summaries = uniqueAwaitingContinuationSummaries(
|
|
2030
|
-
await context.stateAdapter.getList(AGENT_TURN_SESSION_INDEX_KEY)
|
|
2031
|
-
);
|
|
2032
|
-
result.scanned += summaries.length;
|
|
2033
|
-
for (const summary of summaries) {
|
|
2034
|
-
if (!await isActiveContinuationSummary(context, summary)) {
|
|
2035
|
-
continue;
|
|
2036
|
-
}
|
|
2037
|
-
const existingConversation = await getConversation({
|
|
2038
|
-
conversationId: summary.conversationId,
|
|
2039
|
-
state: context.stateAdapter
|
|
2040
|
-
});
|
|
2041
|
-
if (existingConversation?.destination && !sameDestination(existingConversation.destination, summary.destination)) {
|
|
2042
|
-
throw new Error(
|
|
2043
|
-
`Awaiting continuation destination does not match conversation ${summary.conversationId}`
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
if (existingConversation && existingConversation.execution.status !== "idle") {
|
|
2047
|
-
continue;
|
|
2048
|
-
}
|
|
2049
|
-
await requestConversationWork({
|
|
2050
|
-
conversationId: summary.conversationId,
|
|
2051
|
-
destination: summary.destination,
|
|
2052
|
-
nowMs: Math.max(
|
|
2053
|
-
summary.updatedAtMs,
|
|
2054
|
-
existingConversation?.updatedAtMs ?? 0
|
|
2055
|
-
),
|
|
2056
|
-
state: context.stateAdapter
|
|
2057
|
-
});
|
|
2058
|
-
if (existingConversation) {
|
|
2059
|
-
result.existing += 1;
|
|
2060
|
-
} else {
|
|
2061
|
-
result.migrated += 1;
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
async function migrateRedisConversationState(context) {
|
|
2066
|
-
const result = await migrateLegacyConversationWorkRedisState(context);
|
|
2067
|
-
await seedAwaitingContinuationConversationWork(context, result);
|
|
2068
|
-
return result;
|
|
2069
|
-
}
|
|
2070
|
-
var redisConversationStateMigration = {
|
|
2071
|
-
// TODO(after 2026-07-01): remove after deployed installs have had a release
|
|
2072
|
-
// window to move legacy conversation-work Redis state forward.
|
|
2073
|
-
name: "migrate-redis-conversation-state",
|
|
2074
|
-
run: migrateRedisConversationState
|
|
2075
|
-
};
|
|
2076
|
-
|
|
2077
|
-
// src/cli/upgrade/migrations/agent-turn-session-actor.ts
|
|
2078
|
-
import { THREAD_STATE_TTL_MS } from "chat";
|
|
2079
|
-
var AGENT_TURN_SESSION_PREFIX3 = "junior:agent_turn_session";
|
|
2080
|
-
var AGENT_TURN_SESSION_INDEX_KEY2 = `${AGENT_TURN_SESSION_PREFIX3}:index`;
|
|
2081
|
-
var AGENT_TURN_SESSION_INDEX_MAX_LENGTH = 5e3;
|
|
2082
|
-
var REDIS_SCAN_COUNT = 500;
|
|
2083
|
-
function conversationIndexKey(conversationId) {
|
|
2084
|
-
return `${AGENT_TURN_SESSION_PREFIX3}:conversation:${conversationId}:index`;
|
|
2085
|
-
}
|
|
2086
|
-
function sessionRecordKey(conversationId, sessionId) {
|
|
2087
|
-
return `${AGENT_TURN_SESSION_PREFIX3}:${conversationId}:${sessionId}`;
|
|
2088
|
-
}
|
|
2089
|
-
function logicalConversationIndexPrefix() {
|
|
2090
|
-
const prefix = getChatConfig().state.keyPrefix;
|
|
2091
|
-
return [
|
|
2092
|
-
...prefix ? [prefix] : [],
|
|
2093
|
-
`${AGENT_TURN_SESSION_PREFIX3}:conversation:`
|
|
2094
|
-
].join(":");
|
|
2095
|
-
}
|
|
2096
|
-
function conversationIdFromRedisListKey(key3) {
|
|
2097
|
-
const marker = `:list:${logicalConversationIndexPrefix()}`;
|
|
2098
|
-
const markerIndex = key3.indexOf(marker);
|
|
2099
|
-
if (markerIndex < 0 || !key3.endsWith(":index")) {
|
|
2100
|
-
return void 0;
|
|
2101
|
-
}
|
|
2102
|
-
return toOptionalString(
|
|
2103
|
-
key3.slice(markerIndex + marker.length, -":index".length)
|
|
2104
|
-
);
|
|
2105
|
-
}
|
|
2106
|
-
async function discoverRedisConversationIds(redisStateAdapter) {
|
|
2107
|
-
const client = redisStateAdapter?.getClient();
|
|
2108
|
-
if (!client) {
|
|
2109
|
-
return [];
|
|
2110
|
-
}
|
|
2111
|
-
const conversationIds = /* @__PURE__ */ new Set();
|
|
2112
|
-
const match = `*:list:${logicalConversationIndexPrefix()}*:index`;
|
|
2113
|
-
let cursor = "0";
|
|
2114
|
-
do {
|
|
2115
|
-
const reply = await client.sendCommand([
|
|
2116
|
-
"SCAN",
|
|
2117
|
-
cursor,
|
|
2118
|
-
"MATCH",
|
|
2119
|
-
match,
|
|
2120
|
-
"COUNT",
|
|
2121
|
-
String(REDIS_SCAN_COUNT)
|
|
2122
|
-
]);
|
|
2123
|
-
if (!Array.isArray(reply) || reply.length !== 2 || typeof reply[0] !== "string" && typeof reply[0] !== "number" || !Array.isArray(reply[1])) {
|
|
2124
|
-
throw new Error(
|
|
2125
|
-
"Unexpected Redis SCAN response while migrating turn sessions"
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
cursor = String(reply[0]);
|
|
2129
|
-
for (const key3 of reply[1]) {
|
|
2130
|
-
if (typeof key3 !== "string") {
|
|
2131
|
-
continue;
|
|
2132
|
-
}
|
|
2133
|
-
const conversationId = conversationIdFromRedisListKey(key3);
|
|
2134
|
-
if (conversationId) {
|
|
2135
|
-
conversationIds.add(conversationId);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
} while (cursor !== "0");
|
|
2139
|
-
return [...conversationIds];
|
|
2140
|
-
}
|
|
2141
|
-
function migrateRequesterToActor(value) {
|
|
2142
|
-
if (!isRecord(value) || value.requester === void 0) {
|
|
2143
|
-
return { changed: false, value };
|
|
2144
|
-
}
|
|
2145
|
-
const { requester, ...record } = value;
|
|
2146
|
-
return {
|
|
2147
|
-
changed: true,
|
|
2148
|
-
value: {
|
|
2149
|
-
...record,
|
|
2150
|
-
...record.actor === void 0 ? { actor: requester } : {}
|
|
2151
|
-
}
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
async function rewriteList(args) {
|
|
2155
|
-
const values = await args.stateAdapter.getList(args.key);
|
|
2156
|
-
const migrated = values.map(migrateRequesterToActor);
|
|
2157
|
-
const changed = migrated.filter((entry) => entry.changed).length;
|
|
2158
|
-
if (changed === 0) {
|
|
2159
|
-
return { migrated: 0, values };
|
|
2160
|
-
}
|
|
2161
|
-
await args.stateAdapter.delete(args.key);
|
|
2162
|
-
for (const entry of migrated) {
|
|
2163
|
-
await args.stateAdapter.appendToList(args.key, entry.value, {
|
|
2164
|
-
...args.maxLength !== void 0 ? { maxLength: args.maxLength } : {},
|
|
2165
|
-
ttlMs: THREAD_STATE_TTL_MS
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
|
-
return { migrated: changed, values: migrated.map((entry) => entry.value) };
|
|
2169
|
-
}
|
|
2170
|
-
async function migrateSessionRecord(args) {
|
|
2171
|
-
const key3 = sessionRecordKey(args.conversationId, args.sessionId);
|
|
2172
|
-
const existing = await args.stateAdapter.get(key3);
|
|
2173
|
-
if (existing === void 0) {
|
|
2174
|
-
return "missing";
|
|
2175
|
-
}
|
|
2176
|
-
const migrated = migrateRequesterToActor(existing);
|
|
2177
|
-
if (!migrated.changed) {
|
|
2178
|
-
return "existing";
|
|
2179
|
-
}
|
|
2180
|
-
await args.stateAdapter.set(key3, migrated.value, THREAD_STATE_TTL_MS);
|
|
2181
|
-
return "migrated";
|
|
2182
|
-
}
|
|
2183
|
-
async function migrateAgentTurnSessionActor(context) {
|
|
2184
|
-
const result = {
|
|
2185
|
-
existing: 0,
|
|
2186
|
-
migrated: 0,
|
|
2187
|
-
missing: 0,
|
|
2188
|
-
scanned: 0
|
|
2189
|
-
};
|
|
2190
|
-
const global = await rewriteList({
|
|
2191
|
-
key: AGENT_TURN_SESSION_INDEX_KEY2,
|
|
2192
|
-
maxLength: AGENT_TURN_SESSION_INDEX_MAX_LENGTH,
|
|
2193
|
-
stateAdapter: context.stateAdapter
|
|
2194
|
-
});
|
|
2195
|
-
result.scanned += global.values.length;
|
|
2196
|
-
result.migrated += global.migrated;
|
|
2197
|
-
const conversations = /* @__PURE__ */ new Set();
|
|
2198
|
-
const sessions = /* @__PURE__ */ new Set();
|
|
2199
|
-
for (const conversationId of await discoverRedisConversationIds(
|
|
2200
|
-
context.redisStateAdapter
|
|
2201
|
-
)) {
|
|
2202
|
-
conversations.add(conversationId);
|
|
2203
|
-
}
|
|
2204
|
-
for (const value of global.values) {
|
|
2205
|
-
if (!isRecord(value)) {
|
|
2206
|
-
continue;
|
|
2207
|
-
}
|
|
2208
|
-
const conversationId = toOptionalString(value.conversationId);
|
|
2209
|
-
const sessionId = toOptionalString(value.sessionId);
|
|
2210
|
-
if (!conversationId) {
|
|
2211
|
-
continue;
|
|
2212
|
-
}
|
|
2213
|
-
conversations.add(conversationId);
|
|
2214
|
-
if (sessionId) {
|
|
2215
|
-
sessions.add(`${conversationId}\0${sessionId}`);
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
for (const conversationId of conversations) {
|
|
2219
|
-
const conversation = await rewriteList({
|
|
2220
|
-
key: conversationIndexKey(conversationId),
|
|
2221
|
-
stateAdapter: context.stateAdapter
|
|
2222
|
-
});
|
|
2223
|
-
result.scanned += conversation.values.length;
|
|
2224
|
-
result.migrated += conversation.migrated;
|
|
2225
|
-
for (const value of conversation.values) {
|
|
2226
|
-
if (!isRecord(value)) {
|
|
2227
|
-
continue;
|
|
2228
|
-
}
|
|
2229
|
-
const sessionId = toOptionalString(value.sessionId);
|
|
2230
|
-
if (sessionId) {
|
|
2231
|
-
sessions.add(`${conversationId}\0${sessionId}`);
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
for (const session of sessions) {
|
|
2236
|
-
const separator = session.indexOf("\0");
|
|
2237
|
-
const conversationId = session.slice(0, separator);
|
|
2238
|
-
const sessionId = session.slice(separator + 1);
|
|
2239
|
-
result.scanned += 1;
|
|
2240
|
-
const status = await migrateSessionRecord({
|
|
2241
|
-
conversationId,
|
|
2242
|
-
sessionId,
|
|
2243
|
-
stateAdapter: context.stateAdapter
|
|
2244
|
-
});
|
|
2245
|
-
if (status === "migrated") {
|
|
2246
|
-
result.migrated += 1;
|
|
2247
|
-
} else if (status === "existing") {
|
|
2248
|
-
result.existing += 1;
|
|
2249
|
-
} else {
|
|
2250
|
-
result.missing += 1;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
return result;
|
|
2254
|
-
}
|
|
2255
|
-
var agentTurnSessionActorMigration = {
|
|
2256
|
-
name: "migrate-agent-turn-session-requester-to-actor",
|
|
2257
|
-
run: migrateAgentTurnSessionActor
|
|
2258
|
-
};
|
|
2259
|
-
|
|
2260
|
-
// src/cli/upgrade/migrations/conversation-usage.ts
|
|
2261
|
-
var CONVERSATION_USAGE_REPAIR_BATCH_SIZE = 500;
|
|
2262
|
-
var MAX_SAFE_INTEGER = 9007199254740991;
|
|
2263
|
-
var MAX_FINITE_DOUBLE = "1.7976931348623157e308";
|
|
2264
|
-
var REPAIR_LOCK = "junior:upgrade:repair-conversation-usage";
|
|
2265
|
-
var USAGE_REPAIR_BATCH_SQL = `
|
|
2266
|
-
WITH candidates AS MATERIALIZED (
|
|
2267
|
-
SELECT
|
|
2268
|
-
conversation_id,
|
|
2269
|
-
updated_at,
|
|
2270
|
-
execution_updated_at
|
|
2271
|
-
FROM junior_conversations
|
|
2272
|
-
WHERE parent_conversation_id IS NULL
|
|
2273
|
-
AND execution_status = 'idle'
|
|
2274
|
-
AND conversation_id > $1
|
|
2275
|
-
ORDER BY conversation_id
|
|
2276
|
-
LIMIT $2
|
|
2277
|
-
),
|
|
2278
|
-
-- Occurrence numbers preserve intentional duplicates within one history version
|
|
2279
|
-
-- while collapsing context copies of the same message across replacements.
|
|
2280
|
-
message_occurrences AS (
|
|
2281
|
-
SELECT
|
|
2282
|
-
event.conversation_id,
|
|
2283
|
-
event.payload -> 'message' AS message,
|
|
2284
|
-
row_number() OVER (
|
|
2285
|
-
PARTITION BY
|
|
2286
|
-
event.conversation_id,
|
|
2287
|
-
event.history_version,
|
|
2288
|
-
event.payload -> 'message'
|
|
2289
|
-
ORDER BY event.seq
|
|
2290
|
-
) AS occurrence
|
|
2291
|
-
FROM junior_conversation_events AS event
|
|
2292
|
-
INNER JOIN candidates AS candidate
|
|
2293
|
-
ON candidate.conversation_id = event.conversation_id
|
|
2294
|
-
WHERE event.type = 'agent_step'
|
|
2295
|
-
AND event.payload -> 'message' ->> 'role' = 'assistant'
|
|
2296
|
-
AND jsonb_typeof(event.payload -> 'message' -> 'usage') = 'object'
|
|
2297
|
-
),
|
|
2298
|
-
canonical_messages AS (
|
|
2299
|
-
SELECT DISTINCT conversation_id, message, occurrence
|
|
2300
|
-
FROM message_occurrences
|
|
2301
|
-
),
|
|
2302
|
-
usage_values AS (
|
|
2303
|
-
SELECT
|
|
2304
|
-
conversation_id,
|
|
2305
|
-
CASE
|
|
2306
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'input') = 'number'
|
|
2307
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'input')::numeric))
|
|
2308
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'inputTokens') = 'number'
|
|
2309
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'inputTokens')::numeric))
|
|
2310
|
-
END AS input_tokens,
|
|
2311
|
-
CASE
|
|
2312
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'output') = 'number'
|
|
2313
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'output')::numeric))
|
|
2314
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'outputTokens') = 'number'
|
|
2315
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'outputTokens')::numeric))
|
|
2316
|
-
END AS output_tokens,
|
|
2317
|
-
CASE
|
|
2318
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'cacheRead') = 'number'
|
|
2319
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'cacheRead')::numeric))
|
|
2320
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'cachedInputTokens') = 'number'
|
|
2321
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'cachedInputTokens')::numeric))
|
|
2322
|
-
END AS cached_input_tokens,
|
|
2323
|
-
CASE
|
|
2324
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'cacheWrite') = 'number'
|
|
2325
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'cacheWrite')::numeric))
|
|
2326
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'cacheCreationTokens') = 'number'
|
|
2327
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'cacheCreationTokens')::numeric))
|
|
2328
|
-
END AS cache_creation_tokens,
|
|
2329
|
-
CASE
|
|
2330
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'reasoning') = 'number'
|
|
2331
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'reasoning')::numeric))
|
|
2332
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'reasoningTokens') = 'number'
|
|
2333
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'reasoningTokens')::numeric))
|
|
2334
|
-
END AS reasoning_tokens,
|
|
2335
|
-
CASE
|
|
2336
|
-
WHEN jsonb_typeof(message -> 'usage' -> 'totalTokens') = 'number'
|
|
2337
|
-
THEN greatest(0, floor((message -> 'usage' ->> 'totalTokens')::numeric))
|
|
2338
|
-
END AS total_tokens,
|
|
2339
|
-
CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'input') = 'number'
|
|
2340
|
-
THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'input')::numeric)
|
|
2341
|
-
END AS cost_input,
|
|
2342
|
-
CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'output') = 'number'
|
|
2343
|
-
THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'output')::numeric)
|
|
2344
|
-
END AS cost_output,
|
|
2345
|
-
CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'cacheRead') = 'number'
|
|
2346
|
-
THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'cacheRead')::numeric)
|
|
2347
|
-
END AS cost_cache_read,
|
|
2348
|
-
CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'cacheWrite') = 'number'
|
|
2349
|
-
THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'cacheWrite')::numeric)
|
|
2350
|
-
END AS cost_cache_write,
|
|
2351
|
-
CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'total') = 'number'
|
|
2352
|
-
THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'total')::numeric)
|
|
2353
|
-
END AS cost_total
|
|
2354
|
-
FROM canonical_messages
|
|
2355
|
-
),
|
|
2356
|
-
rollups AS (
|
|
2357
|
-
SELECT
|
|
2358
|
-
conversation_id,
|
|
2359
|
-
sum(input_tokens) AS input_tokens,
|
|
2360
|
-
sum(output_tokens) AS output_tokens,
|
|
2361
|
-
sum(cached_input_tokens) AS cached_input_tokens,
|
|
2362
|
-
sum(cache_creation_tokens) AS cache_creation_tokens,
|
|
2363
|
-
sum(reasoning_tokens) AS reasoning_tokens,
|
|
2364
|
-
sum(
|
|
2365
|
-
CASE
|
|
2366
|
-
WHEN input_tokens IS NULL
|
|
2367
|
-
AND output_tokens IS NULL
|
|
2368
|
-
AND cached_input_tokens IS NULL
|
|
2369
|
-
AND cache_creation_tokens IS NULL
|
|
2370
|
-
THEN total_tokens
|
|
2371
|
-
END
|
|
2372
|
-
) AS total_only_tokens,
|
|
2373
|
-
round(sum(cost_input), 12) AS cost_input,
|
|
2374
|
-
round(sum(cost_output), 12) AS cost_output,
|
|
2375
|
-
round(sum(cost_cache_read), 12) AS cost_cache_read,
|
|
2376
|
-
round(sum(cost_cache_write), 12) AS cost_cache_write,
|
|
2377
|
-
round(sum(cost_total), 12) AS cost_total
|
|
2378
|
-
FROM usage_values
|
|
2379
|
-
GROUP BY conversation_id
|
|
2380
|
-
HAVING count(input_tokens)
|
|
2381
|
-
+ count(output_tokens)
|
|
2382
|
-
+ count(cached_input_tokens)
|
|
2383
|
-
+ count(cache_creation_tokens)
|
|
2384
|
-
+ count(reasoning_tokens)
|
|
2385
|
-
+ count(total_tokens)
|
|
2386
|
-
+ count(cost_input)
|
|
2387
|
-
+ count(cost_output)
|
|
2388
|
-
+ count(cost_cache_read)
|
|
2389
|
-
+ count(cost_cache_write)
|
|
2390
|
-
+ count(cost_total) > 0
|
|
2391
|
-
),
|
|
2392
|
-
valid_rollups AS (
|
|
2393
|
-
SELECT *
|
|
2394
|
-
FROM rollups
|
|
2395
|
-
WHERE (input_tokens IS NULL OR input_tokens <= ${MAX_SAFE_INTEGER})
|
|
2396
|
-
AND (output_tokens IS NULL OR output_tokens <= ${MAX_SAFE_INTEGER})
|
|
2397
|
-
AND (cached_input_tokens IS NULL OR cached_input_tokens <= ${MAX_SAFE_INTEGER})
|
|
2398
|
-
AND (cache_creation_tokens IS NULL OR cache_creation_tokens <= ${MAX_SAFE_INTEGER})
|
|
2399
|
-
AND (reasoning_tokens IS NULL OR reasoning_tokens <= ${MAX_SAFE_INTEGER})
|
|
2400
|
-
AND (total_only_tokens IS NULL OR total_only_tokens <= ${MAX_SAFE_INTEGER})
|
|
2401
|
-
AND (
|
|
2402
|
-
total_only_tokens IS NULL
|
|
2403
|
-
OR total_only_tokens
|
|
2404
|
-
+ coalesce(input_tokens, 0)
|
|
2405
|
-
+ coalesce(output_tokens, 0)
|
|
2406
|
-
+ coalesce(cached_input_tokens, 0)
|
|
2407
|
-
+ coalesce(cache_creation_tokens, 0) <= ${MAX_SAFE_INTEGER}
|
|
2408
|
-
)
|
|
2409
|
-
AND (cost_input IS NULL OR cost_input <= ${MAX_FINITE_DOUBLE}::numeric)
|
|
2410
|
-
AND (cost_output IS NULL OR cost_output <= ${MAX_FINITE_DOUBLE}::numeric)
|
|
2411
|
-
AND (cost_cache_read IS NULL OR cost_cache_read <= ${MAX_FINITE_DOUBLE}::numeric)
|
|
2412
|
-
AND (cost_cache_write IS NULL OR cost_cache_write <= ${MAX_FINITE_DOUBLE}::numeric)
|
|
2413
|
-
AND (cost_total IS NULL OR cost_total <= ${MAX_FINITE_DOUBLE}::numeric)
|
|
2414
|
-
),
|
|
2415
|
-
normalized AS (
|
|
2416
|
-
SELECT
|
|
2417
|
-
conversation_id,
|
|
2418
|
-
jsonb_strip_nulls(jsonb_build_object(
|
|
2419
|
-
'inputTokens', CASE WHEN total_only_tokens IS NULL THEN input_tokens END,
|
|
2420
|
-
'outputTokens', CASE WHEN total_only_tokens IS NULL THEN output_tokens END,
|
|
2421
|
-
'cachedInputTokens', CASE
|
|
2422
|
-
WHEN total_only_tokens IS NULL THEN cached_input_tokens
|
|
2423
|
-
END,
|
|
2424
|
-
'cacheCreationTokens', CASE
|
|
2425
|
-
WHEN total_only_tokens IS NULL THEN cache_creation_tokens
|
|
2426
|
-
END,
|
|
2427
|
-
'reasoningTokens', reasoning_tokens,
|
|
2428
|
-
'totalTokens', CASE
|
|
2429
|
-
WHEN total_only_tokens IS NOT NULL
|
|
2430
|
-
THEN total_only_tokens
|
|
2431
|
-
+ coalesce(input_tokens, 0)
|
|
2432
|
-
+ coalesce(output_tokens, 0)
|
|
2433
|
-
+ coalesce(cached_input_tokens, 0)
|
|
2434
|
-
+ coalesce(cache_creation_tokens, 0)
|
|
2435
|
-
END,
|
|
2436
|
-
'cost', CASE
|
|
2437
|
-
WHEN cost_input IS NULL
|
|
2438
|
-
AND cost_output IS NULL
|
|
2439
|
-
AND cost_cache_read IS NULL
|
|
2440
|
-
AND cost_cache_write IS NULL
|
|
2441
|
-
AND cost_total IS NULL
|
|
2442
|
-
THEN NULL
|
|
2443
|
-
ELSE jsonb_strip_nulls(jsonb_build_object(
|
|
2444
|
-
'input', cost_input,
|
|
2445
|
-
'output', cost_output,
|
|
2446
|
-
'cacheRead', cost_cache_read,
|
|
2447
|
-
'cacheWrite', cost_cache_write,
|
|
2448
|
-
'total', cost_total
|
|
2449
|
-
))
|
|
2450
|
-
END
|
|
2451
|
-
)) AS usage
|
|
2452
|
-
FROM valid_rollups
|
|
2453
|
-
),
|
|
2454
|
-
matched AS MATERIALIZED (
|
|
2455
|
-
SELECT conversation.conversation_id
|
|
2456
|
-
FROM junior_conversations AS conversation
|
|
2457
|
-
INNER JOIN candidates AS candidate USING (conversation_id)
|
|
2458
|
-
INNER JOIN normalized USING (conversation_id)
|
|
2459
|
-
WHERE conversation.execution_status = 'idle'
|
|
2460
|
-
AND conversation.updated_at = candidate.updated_at
|
|
2461
|
-
AND conversation.execution_updated_at IS NOT DISTINCT FROM candidate.execution_updated_at
|
|
2462
|
-
FOR UPDATE OF conversation
|
|
2463
|
-
),
|
|
2464
|
-
updated AS (
|
|
2465
|
-
UPDATE junior_conversations AS conversation
|
|
2466
|
-
SET usage_json = normalized.usage
|
|
2467
|
-
FROM normalized
|
|
2468
|
-
INNER JOIN matched USING (conversation_id)
|
|
2469
|
-
WHERE conversation.conversation_id = normalized.conversation_id
|
|
2470
|
-
AND conversation.usage_json IS DISTINCT FROM normalized.usage
|
|
2471
|
-
RETURNING conversation.conversation_id
|
|
2472
|
-
)
|
|
2473
|
-
SELECT
|
|
2474
|
-
candidate.conversation_id AS "conversationId",
|
|
2475
|
-
updated.conversation_id IS NOT NULL AS changed,
|
|
2476
|
-
matched.conversation_id IS NOT NULL AS matched,
|
|
2477
|
-
normalized.conversation_id IS NOT NULL AS repairable
|
|
2478
|
-
FROM candidates AS candidate
|
|
2479
|
-
LEFT JOIN normalized USING (conversation_id)
|
|
2480
|
-
LEFT JOIN matched USING (conversation_id)
|
|
2481
|
-
LEFT JOIN updated USING (conversation_id)
|
|
2482
|
-
ORDER BY candidate.conversation_id
|
|
2483
|
-
`;
|
|
2484
|
-
async function repairConversationUsage(_context, options = {}) {
|
|
2485
|
-
let executor = options.executor;
|
|
2486
|
-
let closeExecutor;
|
|
2487
|
-
if (!executor) {
|
|
2488
|
-
const { sql: sql2 } = getChatConfig();
|
|
2489
|
-
executor = createJuniorSqlExecutor({
|
|
2490
|
-
connectionString: sql2.databaseUrl,
|
|
2491
|
-
driver: sql2.driver
|
|
2492
|
-
});
|
|
2493
|
-
closeExecutor = () => executor.close();
|
|
2494
|
-
}
|
|
2495
|
-
const batchSize = Math.max(
|
|
2496
|
-
1,
|
|
2497
|
-
Math.floor(options.batchSize ?? CONVERSATION_USAGE_REPAIR_BATCH_SIZE)
|
|
2498
|
-
);
|
|
2499
|
-
try {
|
|
2500
|
-
let cursor = "";
|
|
2501
|
-
let existing = 0;
|
|
2502
|
-
let migrated = 0;
|
|
2503
|
-
let missing = 0;
|
|
2504
|
-
let scanned = 0;
|
|
2505
|
-
let skipped = 0;
|
|
2506
|
-
while (true) {
|
|
2507
|
-
const sources = await executor.withLock(
|
|
2508
|
-
REPAIR_LOCK,
|
|
2509
|
-
() => executor.query(USAGE_REPAIR_BATCH_SQL, [
|
|
2510
|
-
cursor,
|
|
2511
|
-
batchSize
|
|
2512
|
-
])
|
|
2513
|
-
);
|
|
2514
|
-
if (sources.length === 0) break;
|
|
2515
|
-
scanned += sources.length;
|
|
2516
|
-
for (const source of sources) {
|
|
2517
|
-
if (!source.repairable) {
|
|
2518
|
-
missing += 1;
|
|
2519
|
-
} else if (source.changed) {
|
|
2520
|
-
migrated += 1;
|
|
2521
|
-
} else if (source.matched) {
|
|
2522
|
-
existing += 1;
|
|
2523
|
-
} else {
|
|
2524
|
-
skipped += 1;
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
cursor = sources.at(-1).conversationId;
|
|
2528
|
-
}
|
|
2529
|
-
return {
|
|
2530
|
-
existing,
|
|
2531
|
-
migrated,
|
|
2532
|
-
missing,
|
|
2533
|
-
scanned,
|
|
2534
|
-
...skipped > 0 ? { skipped } : {}
|
|
2535
|
-
};
|
|
2536
|
-
} finally {
|
|
2537
|
-
await closeExecutor?.();
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
var conversationUsageRepairMigration = {
|
|
2541
|
-
name: "repair-conversation-usage",
|
|
2542
|
-
run: repairConversationUsage
|
|
2543
|
-
};
|
|
2544
|
-
|
|
2545
|
-
// src/cli/upgrade/migrations/conversation-event-cursors.ts
|
|
2546
|
-
var REDIS_SCAN_COUNT2 = 500;
|
|
2547
|
-
function rawTurnSessionKeyPattern() {
|
|
2548
|
-
const statePrefix = getChatConfig().state.keyPrefix;
|
|
2549
|
-
return `*:cache:${statePrefix ? `${statePrefix}:` : ""}${AGENT_TURN_SESSION_PREFIX}:*`;
|
|
2550
|
-
}
|
|
2551
|
-
function logicalStateKey(rawKey2) {
|
|
2552
|
-
const statePrefix = getChatConfig().state.keyPrefix;
|
|
2553
|
-
const cachePrefix = `:cache:${statePrefix ? `${statePrefix}:` : ""}`;
|
|
2554
|
-
const cacheAt = rawKey2.indexOf(cachePrefix);
|
|
2555
|
-
return cacheAt < 0 ? void 0 : rawKey2.slice(cacheAt + cachePrefix.length);
|
|
2556
|
-
}
|
|
2557
|
-
async function discoverRawTurnSessionKeys(redisStateAdapter, conversationIds) {
|
|
2558
|
-
const client = redisStateAdapter?.getClient();
|
|
2559
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
2560
|
-
if (!client) return discovered;
|
|
2561
|
-
const targetKeyPrefixes = [...conversationIds].map(
|
|
2562
|
-
(conversationId) => `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:`
|
|
2563
|
-
);
|
|
2564
|
-
let cursor = "0";
|
|
2565
|
-
do {
|
|
2566
|
-
const reply = await client.sendCommand([
|
|
2567
|
-
"SCAN",
|
|
2568
|
-
cursor,
|
|
2569
|
-
"MATCH",
|
|
2570
|
-
rawTurnSessionKeyPattern(),
|
|
2571
|
-
"COUNT",
|
|
2572
|
-
String(REDIS_SCAN_COUNT2)
|
|
2573
|
-
]);
|
|
2574
|
-
if (!Array.isArray(reply) || reply.length !== 2 || typeof reply[0] !== "string" && typeof reply[0] !== "number" || !Array.isArray(reply[1])) {
|
|
2575
|
-
throw new Error(
|
|
2576
|
-
"Unexpected Redis SCAN response while checking turn-session cursors"
|
|
2577
|
-
);
|
|
2578
|
-
}
|
|
2579
|
-
cursor = String(reply[0]);
|
|
2580
|
-
for (const rawKey2 of reply[1]) {
|
|
2581
|
-
if (typeof rawKey2 !== "string") continue;
|
|
2582
|
-
const logicalKey = logicalStateKey(rawKey2);
|
|
2583
|
-
if (!logicalKey || !targetKeyPrefixes.some((prefix) => logicalKey.startsWith(prefix))) {
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
const encoded = await client.sendCommand(["GET", rawKey2]);
|
|
2587
|
-
if (encoded === null) continue;
|
|
2588
|
-
if (typeof encoded !== "string") {
|
|
2589
|
-
throw new Error(`Turn-session cursor ${rawKey2} is not JSON text`);
|
|
2590
|
-
}
|
|
2591
|
-
let value;
|
|
2592
|
-
try {
|
|
2593
|
-
value = JSON.parse(encoded);
|
|
2594
|
-
} catch (error) {
|
|
2595
|
-
throw new Error(`Turn-session cursor ${rawKey2} is invalid JSON`, {
|
|
2596
|
-
cause: error
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
if (!isRecord(value)) {
|
|
2600
|
-
throw new Error(`Turn-session cursor ${rawKey2} is not an object`);
|
|
2601
|
-
}
|
|
2602
|
-
const conversationId = toOptionalString(value.conversationId);
|
|
2603
|
-
const sessionId = toOptionalString(value.sessionId);
|
|
2604
|
-
if (!conversationId || !sessionId) {
|
|
2605
|
-
throw new Error(`Turn-session cursor ${rawKey2} has no identity`);
|
|
2606
|
-
}
|
|
2607
|
-
const recordKey = agentTurnSessionKey(conversationId, sessionId);
|
|
2608
|
-
if (logicalKey !== recordKey) {
|
|
2609
|
-
throw new Error(
|
|
2610
|
-
`Turn-session cursor ${rawKey2} identity does not match its key`
|
|
2611
|
-
);
|
|
2612
|
-
}
|
|
2613
|
-
if (!conversationIds.has(conversationId)) continue;
|
|
2614
|
-
discovered.set(recordKey, conversationId);
|
|
2615
|
-
}
|
|
2616
|
-
} while (cursor !== "0");
|
|
2617
|
-
return discovered;
|
|
2618
|
-
}
|
|
2619
|
-
async function indexedTurnSessionIds(context, conversationId) {
|
|
2620
|
-
const summaries = await context.stateAdapter.getList(
|
|
2621
|
-
agentTurnSessionConversationIndexKey(conversationId)
|
|
2622
|
-
);
|
|
2623
|
-
return [
|
|
2624
|
-
...new Set(
|
|
2625
|
-
summaries.flatMap((summary) => {
|
|
2626
|
-
if (!isRecord(summary)) return [];
|
|
2627
|
-
const sessionId = toOptionalString(summary.sessionId);
|
|
2628
|
-
return sessionId ? [sessionId] : [];
|
|
2629
|
-
})
|
|
2630
|
-
)
|
|
2631
|
-
];
|
|
2632
|
-
}
|
|
2633
|
-
function hasSeqCursor(record) {
|
|
2634
|
-
return Number.isInteger(record.committedSeq) || Number.isInteger(record.turnStartSeq);
|
|
2635
|
-
}
|
|
2636
|
-
async function prepareConversationEventResequence(context, conversationIds) {
|
|
2637
|
-
if (conversationIds.size === 0) return;
|
|
2638
|
-
await context.stateAdapter.connect();
|
|
2639
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2640
|
-
for (const conversationId of conversationIds) {
|
|
2641
|
-
for (const sessionId of await indexedTurnSessionIds(
|
|
2642
|
-
context,
|
|
2643
|
-
conversationId
|
|
2644
|
-
)) {
|
|
2645
|
-
keys.add(agentTurnSessionKey(conversationId, sessionId));
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
for (const [key3] of await discoverRawTurnSessionKeys(
|
|
2649
|
-
context.redisStateAdapter,
|
|
2650
|
-
conversationIds
|
|
2651
|
-
)) {
|
|
2652
|
-
keys.add(key3);
|
|
2653
|
-
}
|
|
2654
|
-
const terminalKeys = [];
|
|
2655
|
-
for (const key3 of keys) {
|
|
2656
|
-
const value = await context.stateAdapter.get(key3);
|
|
2657
|
-
if (!isRecord(value) || !hasSeqCursor(value)) continue;
|
|
2658
|
-
if (value.state === "running" || value.state === "awaiting_resume") {
|
|
2659
|
-
throw new Error(
|
|
2660
|
-
`Cannot resequence conversation events while unfinished turn session ${key3} retains a seq cursor`
|
|
2661
|
-
);
|
|
2662
|
-
}
|
|
2663
|
-
if (value.state !== "completed" && value.state !== "failed" && value.state !== "abandoned") {
|
|
2664
|
-
throw new Error(
|
|
2665
|
-
`Cannot resequence conversation events with invalid cursor-bearing turn session ${key3}`
|
|
2666
|
-
);
|
|
2667
|
-
}
|
|
2668
|
-
terminalKeys.push(key3);
|
|
2669
|
-
}
|
|
2670
|
-
for (const key3 of terminalKeys) {
|
|
2671
|
-
await context.stateAdapter.delete(key3);
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
// src/cli/upgrade/migrations/conversation-visible-message-events.ts
|
|
2676
|
-
var VISIBLE_EVENT_BATCH_SIZE = 500;
|
|
2677
|
-
var VISIBLE_EVENT_BACKFILL_LOCK = "junior:upgrade:conversation-visible-message-events";
|
|
2678
|
-
var LOAD_VISIBLE_CONVERSATION_IDS_SQL = `
|
|
2679
|
-
SELECT DISTINCT conversation_id
|
|
2680
|
-
FROM junior_conversation_messages
|
|
2681
|
-
WHERE $2::text IS NULL OR conversation_id > $2::text
|
|
2682
|
-
ORDER BY conversation_id
|
|
2683
|
-
LIMIT $1
|
|
2684
|
-
`;
|
|
2685
|
-
var LOAD_COMPACTED_CONVERSATION_IDS_SQL = `
|
|
2686
|
-
SELECT DISTINCT conversation_id
|
|
2687
|
-
FROM junior_conversation_events
|
|
2688
|
-
WHERE type = 'messages_summarized'
|
|
2689
|
-
AND payload @? '$.compactions[*].coveredMessageIds'
|
|
2690
|
-
AND ($2::text IS NULL OR conversation_id > $2::text)
|
|
2691
|
-
ORDER BY conversation_id
|
|
2692
|
-
LIMIT $1
|
|
2693
|
-
`;
|
|
2694
|
-
var LOAD_MISSING_VISIBLE_EVENTS_SQL = `
|
|
2695
|
-
SELECT
|
|
2696
|
-
message.conversation_id,
|
|
2697
|
-
message.message_id,
|
|
2698
|
-
message.role,
|
|
2699
|
-
message.text,
|
|
2700
|
-
message.author_identity_id,
|
|
2701
|
-
message.meta,
|
|
2702
|
-
message.replied_at,
|
|
2703
|
-
message.created_at
|
|
2704
|
-
FROM junior_conversation_messages message
|
|
2705
|
-
WHERE (
|
|
2706
|
-
NOT EXISTS (
|
|
2707
|
-
SELECT 1
|
|
2708
|
-
FROM junior_conversation_events event
|
|
2709
|
-
WHERE event.conversation_id = message.conversation_id
|
|
2710
|
-
AND event.idempotency_key =
|
|
2711
|
-
'message:' || message.message_id
|
|
2712
|
-
)
|
|
2713
|
-
OR (
|
|
2714
|
-
message.replied_at IS NOT NULL
|
|
2715
|
-
AND NOT EXISTS (
|
|
2716
|
-
SELECT 1
|
|
2717
|
-
FROM junior_conversation_events event
|
|
2718
|
-
WHERE event.conversation_id = message.conversation_id
|
|
2719
|
-
AND event.idempotency_key =
|
|
2720
|
-
'message:' || message.message_id || ':handled'
|
|
2721
|
-
)
|
|
2722
|
-
)
|
|
2723
|
-
)
|
|
2724
|
-
AND (
|
|
2725
|
-
$2::text IS NULL
|
|
2726
|
-
OR (message.conversation_id, message.created_at, message.message_id) >
|
|
2727
|
-
($2::text, $3::timestamptz, $4::text)
|
|
2728
|
-
)
|
|
2729
|
-
ORDER BY message.conversation_id, message.created_at, message.message_id
|
|
2730
|
-
LIMIT $1
|
|
2731
|
-
`;
|
|
2732
|
-
var COUNT_MISSING_VISIBLE_EVENTS_SQL = `
|
|
2733
|
-
SELECT count(*)::integer AS count
|
|
2734
|
-
FROM junior_conversation_messages message
|
|
2735
|
-
WHERE NOT EXISTS (
|
|
2736
|
-
SELECT 1
|
|
2737
|
-
FROM junior_conversation_events event
|
|
2738
|
-
WHERE event.conversation_id = message.conversation_id
|
|
2739
|
-
AND event.idempotency_key =
|
|
2740
|
-
'message:' || message.message_id
|
|
2741
|
-
)
|
|
2742
|
-
OR (
|
|
2743
|
-
message.replied_at IS NOT NULL
|
|
2744
|
-
AND NOT EXISTS (
|
|
2745
|
-
SELECT 1
|
|
2746
|
-
FROM junior_conversation_events event
|
|
2747
|
-
WHERE event.conversation_id = message.conversation_id
|
|
2748
|
-
AND event.idempotency_key =
|
|
2749
|
-
'message:' || message.message_id || ':handled'
|
|
2750
|
-
)
|
|
2751
|
-
)
|
|
2752
|
-
`;
|
|
2753
|
-
function timestampMs(value) {
|
|
2754
|
-
return value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
46
|
+
return join(packageRoot, "migrations");
|
|
2755
47
|
}
|
|
2756
|
-
function
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
48
|
+
async function loadCoreMigrationState(executor) {
|
|
49
|
+
try {
|
|
50
|
+
const [migration] = await executor.query(`
|
|
51
|
+
SELECT created_at::text AS "appliedAt"
|
|
52
|
+
FROM drizzle.__drizzle_junior_core
|
|
53
|
+
ORDER BY created_at DESC
|
|
54
|
+
LIMIT 1
|
|
55
|
+
`);
|
|
56
|
+
if (migration?.appliedAt !== null && migration?.appliedAt !== void 0) {
|
|
57
|
+
return {
|
|
58
|
+
appliedAt: Number(migration.appliedAt),
|
|
59
|
+
hasJuniorTables: true
|
|
60
|
+
};
|
|
2769
61
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
idempotencyKey: `message:${row.message_id}:handled`,
|
|
2774
|
-
data: { type: "message_handled", messageId: row.message_id },
|
|
2775
|
-
createdAtMs: timestampMs(row.replied_at)
|
|
2776
|
-
});
|
|
2777
|
-
}
|
|
2778
|
-
return events;
|
|
2779
|
-
}
|
|
2780
|
-
async function visitConversationIds(executor, query, batchSize, visit) {
|
|
2781
|
-
let cursor;
|
|
2782
|
-
while (true) {
|
|
2783
|
-
const rows = await executor.query(query, [
|
|
2784
|
-
batchSize,
|
|
2785
|
-
cursor ?? null
|
|
2786
|
-
]);
|
|
2787
|
-
if (rows.length === 0) return;
|
|
2788
|
-
for (const row of rows) {
|
|
2789
|
-
await visit(row.conversation_id);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!isPostgresErrorCode(error, "42P01")) {
|
|
64
|
+
throw error;
|
|
2790
65
|
}
|
|
2791
|
-
cursor = rows.at(-1).conversation_id;
|
|
2792
66
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
}
|
|
2802
|
-
async function readLegacyCompactionState(context, conversationId) {
|
|
2803
|
-
const raw = await context.stateAdapter.get(
|
|
2804
|
-
`thread-state:${conversationId}`
|
|
2805
|
-
);
|
|
2806
|
-
const conversation = isRecord(raw) && isRecord(raw.conversation) ? raw.conversation : void 0;
|
|
2807
|
-
const stats = conversation && isRecord(conversation.stats) ? conversation.stats : void 0;
|
|
2808
|
-
const rawCount = toOptionalNumber(stats?.compactedMessageCount);
|
|
2809
|
-
const compactedMessageCount = rawCount === void 0 ? void 0 : Math.max(0, Math.floor(rawCount));
|
|
2810
|
-
const messages = conversation && Array.isArray(conversation.messages) ? conversation.messages : [];
|
|
2811
|
-
const liveMessageIds = new Set(
|
|
2812
|
-
messages.flatMap((message) => {
|
|
2813
|
-
if (!isRecord(message)) return [];
|
|
2814
|
-
const id = toOptionalString(message.id);
|
|
2815
|
-
return id ? [id] : [];
|
|
2816
|
-
})
|
|
2817
|
-
);
|
|
67
|
+
const [tables] = await executor.query(`
|
|
68
|
+
SELECT EXISTS (
|
|
69
|
+
SELECT 1
|
|
70
|
+
FROM information_schema.tables
|
|
71
|
+
WHERE table_schema = 'public'
|
|
72
|
+
AND table_name LIKE 'junior\\_%' ESCAPE '\\'
|
|
73
|
+
) AS "hasJuniorTables"
|
|
74
|
+
`);
|
|
2818
75
|
return {
|
|
2819
|
-
|
|
2820
|
-
liveMessageIds
|
|
76
|
+
hasJuniorTables: tables?.hasJuniorTables ?? false
|
|
2821
77
|
};
|
|
2822
78
|
}
|
|
2823
|
-
function
|
|
2824
|
-
return
|
|
2825
|
-
const parsed = newConversationEventSchema.parse(event);
|
|
2826
|
-
if (!parsed.idempotencyKey) {
|
|
2827
|
-
throw new Error(
|
|
2828
|
-
"Visible-message backfill events require idempotency keys"
|
|
2829
|
-
);
|
|
2830
|
-
}
|
|
2831
|
-
const { type, ...payload } = conversationEventDataSchema.parse(parsed.data);
|
|
2832
|
-
return {
|
|
2833
|
-
createdAtMs: parsed.createdAtMs,
|
|
2834
|
-
idempotencyKey: parsed.idempotencyKey,
|
|
2835
|
-
payload: sanitizePostgresJson(payload),
|
|
2836
|
-
type
|
|
2837
|
-
};
|
|
2838
|
-
});
|
|
79
|
+
function isCurrentMigrationState(state, latestMigrationAt) {
|
|
80
|
+
return state.appliedAt !== void 0 && state.appliedAt >= latestMigrationAt;
|
|
2839
81
|
}
|
|
2840
|
-
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
LIMIT 1
|
|
2849
|
-
)
|
|
2850
|
-
SELECT (
|
|
2851
|
-
SELECT event.payload->>'messageId'
|
|
2852
|
-
FROM junior_conversation_events event
|
|
2853
|
-
WHERE event.conversation_id = $1
|
|
2854
|
-
AND event.type = 'message'
|
|
2855
|
-
AND event.seq >= (snapshot.payload->>'historyFromSeq')::integer
|
|
2856
|
-
ORDER BY event.seq
|
|
2857
|
-
LIMIT 1
|
|
2858
|
-
) AS "messageId"
|
|
2859
|
-
FROM snapshot`,
|
|
2860
|
-
[conversationId]
|
|
2861
|
-
);
|
|
2862
|
-
return row;
|
|
82
|
+
function migrationResult(migrations, state) {
|
|
83
|
+
const appliedAt = state.appliedAt;
|
|
84
|
+
const existing = appliedAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= appliedAt).length;
|
|
85
|
+
return {
|
|
86
|
+
existing,
|
|
87
|
+
migrated: migrations.length - existing,
|
|
88
|
+
scanned: migrations.length
|
|
89
|
+
};
|
|
2863
90
|
}
|
|
2864
|
-
|
|
2865
|
-
if (!
|
|
2866
|
-
|
|
2867
|
-
if (anchor.messageId) {
|
|
2868
|
-
const [baseline] = await executor.query(
|
|
2869
|
-
`SELECT seq
|
|
2870
|
-
FROM junior_conversation_events
|
|
2871
|
-
WHERE conversation_id = $1
|
|
2872
|
-
AND type = 'message'
|
|
2873
|
-
AND payload->>'messageId' = $2
|
|
2874
|
-
ORDER BY seq
|
|
2875
|
-
LIMIT 1`,
|
|
2876
|
-
[conversationId, anchor.messageId]
|
|
2877
|
-
);
|
|
2878
|
-
if (!baseline) {
|
|
2879
|
-
throw new Error(
|
|
2880
|
-
`Visible compaction anchor ${anchor.messageId} disappeared during resequence`
|
|
2881
|
-
);
|
|
2882
|
-
}
|
|
2883
|
-
historyFromSeq = baseline.seq;
|
|
2884
|
-
} else {
|
|
2885
|
-
const [end] = await executor.query(
|
|
2886
|
-
`SELECT coalesce(max(seq), -1)::integer + 1 AS seq
|
|
2887
|
-
FROM junior_conversation_events
|
|
2888
|
-
WHERE conversation_id = $1`,
|
|
2889
|
-
[conversationId]
|
|
2890
|
-
);
|
|
2891
|
-
historyFromSeq = end?.seq ?? 0;
|
|
91
|
+
function assertSupportedMigrationState(state) {
|
|
92
|
+
if (!state.hasJuniorTables || state.appliedAt !== void 0) {
|
|
93
|
+
return;
|
|
2892
94
|
}
|
|
2893
|
-
|
|
2894
|
-
`
|
|
2895
|
-
SELECT ctid
|
|
2896
|
-
FROM junior_conversation_events
|
|
2897
|
-
WHERE conversation_id = $1
|
|
2898
|
-
AND type = 'messages_summarized'
|
|
2899
|
-
ORDER BY seq DESC
|
|
2900
|
-
LIMIT 1
|
|
2901
|
-
)
|
|
2902
|
-
UPDATE junior_conversation_events event
|
|
2903
|
-
SET payload = jsonb_set(
|
|
2904
|
-
event.payload,
|
|
2905
|
-
'{historyFromSeq}',
|
|
2906
|
-
to_jsonb($2::integer)
|
|
2907
|
-
)
|
|
2908
|
-
FROM snapshot
|
|
2909
|
-
WHERE event.ctid = snapshot.ctid`,
|
|
2910
|
-
[conversationId, historyFromSeq]
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Existing Junior SQL tables have no core Drizzle migration history. Stop old Junior workers, install @sentry/junior@${CORE_MIGRATION_BRIDGE_VERSION}, run \`junior upgrade\`, then restore this Junior version and rerun \`junior upgrade\` before restarting workers.`
|
|
2911
97
|
);
|
|
2912
98
|
}
|
|
2913
|
-
function
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
99
|
+
async function migrateSchema(executor) {
|
|
100
|
+
const migrationsFolder = migrationFolder();
|
|
101
|
+
const migrations = readMigrationFiles({ migrationsFolder });
|
|
102
|
+
const latestMigration = migrations.at(-1);
|
|
103
|
+
if (!latestMigration) {
|
|
104
|
+
throw new Error("No core Drizzle migrations were packaged");
|
|
105
|
+
}
|
|
106
|
+
const initialState = await loadCoreMigrationState(executor);
|
|
107
|
+
if (isCurrentMigrationState(initialState, latestMigration.folderMillis)) {
|
|
108
|
+
return migrationResult(migrations, initialState);
|
|
109
|
+
}
|
|
110
|
+
assertSupportedMigrationState(initialState);
|
|
111
|
+
return await executor.withMigrationLock(MIGRATIONS_TABLE, async () => {
|
|
112
|
+
const lockedState = await loadCoreMigrationState(executor);
|
|
113
|
+
if (isCurrentMigrationState(lockedState, latestMigration.folderMillis)) {
|
|
114
|
+
return migrationResult(migrations, lockedState);
|
|
2918
115
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
116
|
+
assertSupportedMigrationState(lockedState);
|
|
117
|
+
await executor.migrate({
|
|
118
|
+
migrationsFolder,
|
|
119
|
+
migrationsTable: MIGRATIONS_TABLE
|
|
2922
120
|
});
|
|
121
|
+
return migrationResult(migrations, lockedState);
|
|
2923
122
|
});
|
|
2924
123
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
124
|
+
|
|
125
|
+
// src/cli/upgrade.ts
|
|
126
|
+
import { createJiti } from "jiti";
|
|
127
|
+
|
|
128
|
+
// src/chat/plugins/migrations.ts
|
|
129
|
+
import { createHash } from "crypto";
|
|
130
|
+
import { readMigrationFiles as readMigrationFiles2 } from "drizzle-orm/migrator";
|
|
131
|
+
var LEGACY_SCHEDULER_BASELINE_HASH = "d1d2f712181dd3a0557808f0fc67fd0722691d25f4c8cfb816b77c71d19e1e42";
|
|
132
|
+
function migrationTable(pluginName) {
|
|
133
|
+
const label = pluginName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32);
|
|
134
|
+
const hash = createHash("sha256").update(pluginName).digest("hex").slice(0, 8);
|
|
135
|
+
return `__drizzle_${label}_${hash}`;
|
|
2933
136
|
}
|
|
2934
|
-
function
|
|
2935
|
-
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
if (args.legacyState.compactedMessageCount !== void 0) {
|
|
2942
|
-
return args.visibleEvents[args.legacyState.compactedMessageCount]?.seq ?? args.endSeq;
|
|
2943
|
-
}
|
|
2944
|
-
const coveredIds = new Set(args.legacyCoveredMessageIds);
|
|
2945
|
-
if (coveredIds.size > 0) {
|
|
2946
|
-
const lastCoveredSeq = args.visibleEvents.reduce(
|
|
2947
|
-
(last, event) => coveredIds.has(event.messageId) ? Math.max(last, event.seq) : last,
|
|
2948
|
-
-1
|
|
137
|
+
async function appliedMigrationTime(executor, table) {
|
|
138
|
+
try {
|
|
139
|
+
const [row] = await executor.query(
|
|
140
|
+
`SELECT created_at::text AS "createdAt"
|
|
141
|
+
FROM drizzle.${table}
|
|
142
|
+
ORDER BY created_at DESC
|
|
143
|
+
LIMIT 1`
|
|
2949
144
|
);
|
|
2950
|
-
return
|
|
145
|
+
return row?.createdAt === null || row?.createdAt === void 0 ? void 0 : Number(row.createdAt);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isPostgresErrorCode(error, "42P01")) {
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
2951
151
|
}
|
|
2952
|
-
return args.currentHistoryFromSeq > 0 ? args.currentHistoryFromSeq : args.visibleEvents[0]?.seq ?? args.endSeq;
|
|
2953
152
|
}
|
|
2954
|
-
async function
|
|
2955
|
-
const
|
|
2956
|
-
|
|
2957
|
-
`junior_conversation:event:${conversationId}`,
|
|
2958
|
-
async () => {
|
|
2959
|
-
const [latest] = await executor.query(
|
|
2960
|
-
`SELECT seq, payload
|
|
2961
|
-
FROM junior_conversation_events
|
|
2962
|
-
WHERE conversation_id = $1
|
|
2963
|
-
AND type = 'messages_summarized'
|
|
2964
|
-
ORDER BY seq DESC
|
|
2965
|
-
LIMIT 1`,
|
|
2966
|
-
[conversationId]
|
|
2967
|
-
);
|
|
2968
|
-
if (!latest) return;
|
|
2969
|
-
const visibleEvents = await executor.query(
|
|
2970
|
-
`SELECT seq, payload->>'messageId' AS "messageId"
|
|
2971
|
-
FROM junior_conversation_events
|
|
2972
|
-
WHERE conversation_id = $1
|
|
2973
|
-
AND type = 'message'
|
|
2974
|
-
ORDER BY seq`,
|
|
2975
|
-
[conversationId]
|
|
2976
|
-
);
|
|
2977
|
-
const [cursor] = await executor.query(
|
|
2978
|
-
`SELECT coalesce(max(seq), -1)::integer + 1 AS "endSeq"
|
|
2979
|
-
FROM junior_conversation_events
|
|
2980
|
-
WHERE conversation_id = $1`,
|
|
2981
|
-
[conversationId]
|
|
2982
|
-
);
|
|
2983
|
-
const currentHistoryFromSeq = toOptionalNumber(latest.payload.historyFromSeq) ?? 0;
|
|
2984
|
-
const historyFromSeq = resolvedHistoryFromSeq({
|
|
2985
|
-
currentHistoryFromSeq: Math.max(0, Math.floor(currentHistoryFromSeq)),
|
|
2986
|
-
endSeq: cursor?.endSeq ?? 0,
|
|
2987
|
-
legacyCoveredMessageIds: coveredMessageIds(latest.payload),
|
|
2988
|
-
legacyState,
|
|
2989
|
-
visibleEvents
|
|
2990
|
-
});
|
|
2991
|
-
const retainedCount = retainedCompactionCount(latest.payload);
|
|
2992
|
-
const countDelta = Math.max(
|
|
2993
|
-
0,
|
|
2994
|
-
(legacyState.compactedMessageCount ?? retainedCount) - retainedCount
|
|
2995
|
-
);
|
|
2996
|
-
await executor.execute(
|
|
2997
|
-
`WITH rewritten AS (
|
|
2998
|
-
SELECT
|
|
2999
|
-
event.ctid,
|
|
3000
|
-
jsonb_agg(
|
|
3001
|
-
(compaction.value - 'coveredMessageIds') ||
|
|
3002
|
-
jsonb_build_object(
|
|
3003
|
-
'coveredMessageCount',
|
|
3004
|
-
coalesce(
|
|
3005
|
-
(compaction.value->>'coveredMessageCount')::integer,
|
|
3006
|
-
jsonb_array_length(
|
|
3007
|
-
coalesce(compaction.value->'coveredMessageIds', '[]'::jsonb)
|
|
3008
|
-
)
|
|
3009
|
-
) + CASE
|
|
3010
|
-
WHEN event.seq = $2 AND compaction.position = 1 THEN $4
|
|
3011
|
-
ELSE 0
|
|
3012
|
-
END
|
|
3013
|
-
)
|
|
3014
|
-
ORDER BY compaction.position
|
|
3015
|
-
) AS compactions
|
|
3016
|
-
FROM junior_conversation_events event
|
|
3017
|
-
CROSS JOIN LATERAL jsonb_array_elements(
|
|
3018
|
-
event.payload->'compactions'
|
|
3019
|
-
) WITH ORDINALITY AS compaction(value, position)
|
|
3020
|
-
WHERE event.conversation_id = $1
|
|
3021
|
-
AND event.type = 'messages_summarized'
|
|
3022
|
-
GROUP BY event.ctid
|
|
3023
|
-
)
|
|
3024
|
-
UPDATE junior_conversation_events event
|
|
3025
|
-
SET payload =
|
|
3026
|
-
(event.payload - 'compactions') ||
|
|
3027
|
-
jsonb_build_object('compactions', rewritten.compactions) ||
|
|
3028
|
-
CASE WHEN event.seq = $2
|
|
3029
|
-
THEN jsonb_build_object('historyFromSeq', $3::integer)
|
|
3030
|
-
ELSE '{}'::jsonb
|
|
3031
|
-
END
|
|
3032
|
-
FROM rewritten
|
|
3033
|
-
WHERE event.ctid = rewritten.ctid`,
|
|
3034
|
-
[conversationId, latest.seq, historyFromSeq, countDelta]
|
|
3035
|
-
);
|
|
3036
|
-
}
|
|
153
|
+
async function legacyMigrationHashes(executor, pluginName) {
|
|
154
|
+
const [exists] = await executor.query(
|
|
155
|
+
`SELECT to_regclass('public.junior_schema_migrations')::text AS "tableName"`
|
|
3037
156
|
);
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
await executor.
|
|
3042
|
-
`
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
conversationId
|
|
3048
|
-
);
|
|
3049
|
-
await executor.execute(
|
|
3050
|
-
`WITH input AS (
|
|
3051
|
-
SELECT *
|
|
3052
|
-
FROM jsonb_to_recordset($2::jsonb) AS event(
|
|
3053
|
-
"createdAtMs" bigint,
|
|
3054
|
-
"idempotencyKey" text,
|
|
3055
|
-
payload jsonb,
|
|
3056
|
-
type text
|
|
3057
|
-
)
|
|
3058
|
-
), base AS (
|
|
3059
|
-
SELECT coalesce(max(seq), -1) AS max_seq
|
|
3060
|
-
FROM junior_conversation_events
|
|
3061
|
-
WHERE conversation_id = $1
|
|
3062
|
-
), pending AS (
|
|
3063
|
-
SELECT
|
|
3064
|
-
input.*,
|
|
3065
|
-
row_number() OVER (
|
|
3066
|
-
ORDER BY input."createdAtMs", input."idempotencyKey"
|
|
3067
|
-
) AS offset
|
|
3068
|
-
FROM input
|
|
3069
|
-
WHERE NOT EXISTS (
|
|
3070
|
-
SELECT 1
|
|
3071
|
-
FROM junior_conversation_events existing
|
|
3072
|
-
WHERE existing.conversation_id = $1
|
|
3073
|
-
AND existing.idempotency_key = input."idempotencyKey"
|
|
3074
|
-
)
|
|
3075
|
-
)
|
|
3076
|
-
INSERT INTO junior_conversation_events (
|
|
3077
|
-
conversation_id,
|
|
3078
|
-
seq,
|
|
3079
|
-
history_version,
|
|
3080
|
-
schema_version,
|
|
3081
|
-
idempotency_key,
|
|
3082
|
-
type,
|
|
3083
|
-
payload,
|
|
3084
|
-
created_at
|
|
3085
|
-
)
|
|
3086
|
-
SELECT
|
|
3087
|
-
$1,
|
|
3088
|
-
base.max_seq + pending.offset,
|
|
3089
|
-
coalesce((
|
|
3090
|
-
SELECT prior.history_version
|
|
3091
|
-
FROM junior_conversation_events prior
|
|
3092
|
-
WHERE prior.conversation_id = $1
|
|
3093
|
-
AND prior.created_at < to_timestamp(pending."createdAtMs" / 1000.0)
|
|
3094
|
-
ORDER BY prior.seq DESC
|
|
3095
|
-
LIMIT 1
|
|
3096
|
-
), 0),
|
|
3097
|
-
1,
|
|
3098
|
-
pending."idempotencyKey",
|
|
3099
|
-
pending.type,
|
|
3100
|
-
pending.payload,
|
|
3101
|
-
to_timestamp(pending."createdAtMs" / 1000.0)
|
|
3102
|
-
FROM pending
|
|
3103
|
-
CROSS JOIN base
|
|
3104
|
-
ON CONFLICT (conversation_id, idempotency_key) DO NOTHING`,
|
|
3105
|
-
[conversationId, JSON.stringify(encoded)]
|
|
3106
|
-
);
|
|
3107
|
-
await executor.execute(
|
|
3108
|
-
`UPDATE junior_conversation_events
|
|
3109
|
-
SET seq = -seq - 1
|
|
3110
|
-
WHERE conversation_id = $1`,
|
|
3111
|
-
[conversationId]
|
|
3112
|
-
);
|
|
3113
|
-
await executor.execute(
|
|
3114
|
-
`WITH ranked AS (
|
|
3115
|
-
SELECT
|
|
3116
|
-
event.ctid,
|
|
3117
|
-
row_number() OVER (
|
|
3118
|
-
ORDER BY
|
|
3119
|
-
CASE
|
|
3120
|
-
WHEN event.type IN (
|
|
3121
|
-
'message',
|
|
3122
|
-
'message_handled'
|
|
3123
|
-
) THEN coalesce((
|
|
3124
|
-
SELECT max(-execution.seq - 1)
|
|
3125
|
-
FROM junior_conversation_events execution
|
|
3126
|
-
WHERE execution.conversation_id = event.conversation_id
|
|
3127
|
-
AND execution.type NOT IN (
|
|
3128
|
-
'message',
|
|
3129
|
-
'message_handled'
|
|
3130
|
-
)
|
|
3131
|
-
AND execution.created_at < event.created_at
|
|
3132
|
-
), -1)
|
|
3133
|
-
ELSE -event.seq - 1
|
|
3134
|
-
END,
|
|
3135
|
-
CASE
|
|
3136
|
-
WHEN event.type IN (
|
|
3137
|
-
'message',
|
|
3138
|
-
'message_handled'
|
|
3139
|
-
) THEN 1
|
|
3140
|
-
ELSE 0
|
|
3141
|
-
END,
|
|
3142
|
-
event.created_at,
|
|
3143
|
-
CASE event.type
|
|
3144
|
-
WHEN 'message' THEN 0
|
|
3145
|
-
WHEN 'message_handled' THEN 1
|
|
3146
|
-
ELSE 0
|
|
3147
|
-
END,
|
|
3148
|
-
event.idempotency_key NULLS FIRST,
|
|
3149
|
-
event.type
|
|
3150
|
-
) - 1 AS next_seq
|
|
3151
|
-
FROM junior_conversation_events event
|
|
3152
|
-
WHERE event.conversation_id = $1
|
|
3153
|
-
)
|
|
3154
|
-
UPDATE junior_conversation_events event
|
|
3155
|
-
SET seq = ranked.next_seq
|
|
3156
|
-
FROM ranked
|
|
3157
|
-
WHERE event.ctid = ranked.ctid`,
|
|
3158
|
-
[conversationId]
|
|
3159
|
-
);
|
|
3160
|
-
await normalizeVisibleCompactionBoundary(
|
|
3161
|
-
executor,
|
|
3162
|
-
conversationId,
|
|
3163
|
-
compactionAnchor
|
|
3164
|
-
);
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
157
|
+
if (!exists?.tableName) {
|
|
158
|
+
return /* @__PURE__ */ new Set();
|
|
159
|
+
}
|
|
160
|
+
const rows = await executor.query(
|
|
161
|
+
`SELECT checksum
|
|
162
|
+
FROM junior_schema_migrations
|
|
163
|
+
WHERE id LIKE $1
|
|
164
|
+
ORDER BY id ASC`,
|
|
165
|
+
[`plugin:${pluginName}/%`]
|
|
3167
166
|
);
|
|
167
|
+
return new Set(rows.map((row) => row.checksum));
|
|
3168
168
|
}
|
|
3169
|
-
|
|
3170
|
-
let
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
169
|
+
function adoptedMigration(migrations, legacyHashes, pluginName) {
|
|
170
|
+
let adopted;
|
|
171
|
+
for (const migration of migrations) {
|
|
172
|
+
if (!legacyHashes.has(migration.hash)) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
adopted = migration;
|
|
176
|
+
}
|
|
177
|
+
if (!adopted && pluginName === "scheduler" && legacyHashes.has(LEGACY_SCHEDULER_BASELINE_HASH)) {
|
|
178
|
+
return migrations[0];
|
|
3179
179
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
180
|
+
return adopted;
|
|
181
|
+
}
|
|
182
|
+
async function adoptLegacyMigrationState(args) {
|
|
183
|
+
const legacyHashes = await legacyMigrationHashes(
|
|
184
|
+
args.executor,
|
|
185
|
+
args.pluginName
|
|
3183
186
|
);
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
187
|
+
const migration = adoptedMigration(
|
|
188
|
+
args.migrations,
|
|
189
|
+
legacyHashes,
|
|
190
|
+
args.pluginName
|
|
191
|
+
);
|
|
192
|
+
if (!migration) {
|
|
193
|
+
return void 0;
|
|
194
|
+
}
|
|
195
|
+
await args.executor.transaction(async () => {
|
|
196
|
+
await args.executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
|
|
197
|
+
await args.executor.execute(`
|
|
198
|
+
CREATE TABLE IF NOT EXISTS drizzle.${args.table} (
|
|
199
|
+
id SERIAL PRIMARY KEY,
|
|
200
|
+
hash TEXT NOT NULL,
|
|
201
|
+
created_at BIGINT
|
|
202
|
+
)
|
|
203
|
+
`);
|
|
204
|
+
await args.executor.execute(
|
|
205
|
+
`INSERT INTO drizzle.${args.table} (hash, created_at) VALUES ($1, $2)`,
|
|
206
|
+
[migration.hash, migration.folderMillis]
|
|
3189
207
|
);
|
|
3190
|
-
|
|
3191
|
-
|
|
208
|
+
});
|
|
209
|
+
return migration.folderMillis;
|
|
210
|
+
}
|
|
211
|
+
function appliedCount(migrations, createdAt) {
|
|
212
|
+
return createdAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= createdAt).length;
|
|
213
|
+
}
|
|
214
|
+
async function migratePluginSchemas(executor, roots, options = {}) {
|
|
215
|
+
const result = {
|
|
216
|
+
existing: 0,
|
|
217
|
+
migrated: 0,
|
|
218
|
+
scanned: 0
|
|
219
|
+
};
|
|
220
|
+
const orderedRoots = [...roots].sort(
|
|
221
|
+
(left, right) => left.pluginName.localeCompare(right.pluginName)
|
|
222
|
+
);
|
|
223
|
+
for (const root of orderedRoots) {
|
|
224
|
+
const migrations = readMigrationFiles2({ migrationsFolder: root.dir });
|
|
225
|
+
if (migrations.length === 0) {
|
|
226
|
+
continue;
|
|
3192
227
|
}
|
|
3193
|
-
const
|
|
3194
|
-
|
|
3195
|
-
);
|
|
3196
|
-
if (
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
228
|
+
const table = migrationTable(root.pluginName);
|
|
229
|
+
const initialTime = await appliedMigrationTime(executor, table);
|
|
230
|
+
const initialExisting = appliedCount(migrations, initialTime);
|
|
231
|
+
if (initialExisting === migrations.length) {
|
|
232
|
+
const summary2 = {
|
|
233
|
+
existing: initialExisting,
|
|
234
|
+
migrated: 0,
|
|
235
|
+
pluginName: root.pluginName,
|
|
236
|
+
scanned: migrations.length
|
|
237
|
+
};
|
|
238
|
+
result.scanned += summary2.scanned;
|
|
239
|
+
result.existing += summary2.existing;
|
|
240
|
+
options.onPluginMigration?.(summary2);
|
|
241
|
+
continue;
|
|
3200
242
|
}
|
|
3201
|
-
|
|
3202
|
-
const
|
|
3203
|
-
await visitVisibleConversations(
|
|
243
|
+
const summary = await executor.withMigrationLock(table, async () => {
|
|
244
|
+
const currentTime = await appliedMigrationTime(executor, table) ?? await adoptLegacyMigrationState({
|
|
3204
245
|
executor,
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
);
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
() => executor.query(LOAD_MISSING_VISIBLE_EVENTS_SQL, [
|
|
3216
|
-
batchSize,
|
|
3217
|
-
cursor?.conversationId ?? null,
|
|
3218
|
-
cursor?.createdAt ?? null,
|
|
3219
|
-
cursor?.messageId ?? null
|
|
3220
|
-
])
|
|
3221
|
-
);
|
|
3222
|
-
if (rows.length === 0) {
|
|
3223
|
-
break;
|
|
3224
|
-
}
|
|
3225
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
3226
|
-
for (const row of rows) {
|
|
3227
|
-
const events = grouped.get(row.conversation_id) ?? [];
|
|
3228
|
-
events.push(...eventsForRow(row));
|
|
3229
|
-
grouped.set(row.conversation_id, events);
|
|
3230
|
-
}
|
|
3231
|
-
for (const [conversationId, events] of grouped) {
|
|
3232
|
-
await mergeVisibleEvents(executor, conversationId, events);
|
|
246
|
+
migrations,
|
|
247
|
+
pluginName: root.pluginName,
|
|
248
|
+
table
|
|
249
|
+
});
|
|
250
|
+
const existing = appliedCount(migrations, currentTime);
|
|
251
|
+
if (existing < migrations.length) {
|
|
252
|
+
await executor.migrate({
|
|
253
|
+
migrationsFolder: root.dir,
|
|
254
|
+
migrationsTable: table
|
|
255
|
+
});
|
|
3233
256
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
messageId: last.message_id
|
|
257
|
+
return {
|
|
258
|
+
existing,
|
|
259
|
+
migrated: migrations.length - existing,
|
|
260
|
+
pluginName: root.pluginName,
|
|
261
|
+
scanned: migrations.length
|
|
3240
262
|
};
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
throw new Error(
|
|
3247
|
-
"Visible-message event migration could not verify its projection"
|
|
3248
|
-
);
|
|
3249
|
-
}
|
|
3250
|
-
if (remaining.count > 0) {
|
|
3251
|
-
throw new Error(
|
|
3252
|
-
`Visible-message event migration left ${remaining.count} message row(s) without canonical events`
|
|
3253
|
-
);
|
|
3254
|
-
}
|
|
3255
|
-
await visitConversationIds(
|
|
3256
|
-
executor,
|
|
3257
|
-
LOAD_COMPACTED_CONVERSATION_IDS_SQL,
|
|
3258
|
-
batchSize,
|
|
3259
|
-
async (conversationId) => await finalizeVisibleCompactions(context, executor, conversationId)
|
|
3260
|
-
);
|
|
3261
|
-
return {
|
|
3262
|
-
existing: 0,
|
|
3263
|
-
migrated,
|
|
3264
|
-
missing: 0,
|
|
3265
|
-
scanned: migrated
|
|
3266
|
-
};
|
|
3267
|
-
} finally {
|
|
3268
|
-
await closeExecutor?.();
|
|
263
|
+
});
|
|
264
|
+
result.scanned += summary.scanned;
|
|
265
|
+
result.existing += summary.existing;
|
|
266
|
+
result.migrated += summary.migrated;
|
|
267
|
+
options.onPluginMigration?.(summary);
|
|
3269
268
|
}
|
|
269
|
+
return result;
|
|
3270
270
|
}
|
|
3271
|
-
var conversationVisibleMessageEventsMigration = {
|
|
3272
|
-
name: "move-conversation-messages-to-events",
|
|
3273
|
-
run: migrateConversationVisibleMessageEvents
|
|
3274
|
-
};
|
|
3275
271
|
|
|
3276
|
-
// src/cli/upgrade/migrations/
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
function messageTextParts(message) {
|
|
3280
|
-
if (typeof message.content === "string") {
|
|
3281
|
-
return [message.content];
|
|
3282
|
-
}
|
|
3283
|
-
if (!Array.isArray(message.content)) {
|
|
3284
|
-
return [];
|
|
3285
|
-
}
|
|
3286
|
-
return message.content.flatMap(
|
|
3287
|
-
(part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" ? [part.text] : []
|
|
3288
|
-
);
|
|
272
|
+
// src/cli/upgrade/migrations/upgrade-plugins.ts
|
|
273
|
+
function unique(values) {
|
|
274
|
+
return [...new Set(values)];
|
|
3289
275
|
}
|
|
3290
|
-
function
|
|
3291
|
-
return
|
|
3292
|
-
const message = row.payload.message;
|
|
3293
|
-
return message !== null && typeof message === "object" && !Array.isArray(message) && messageTextParts(message).some(
|
|
3294
|
-
isLegacyOrCurrentCheckpointText
|
|
3295
|
-
);
|
|
3296
|
-
});
|
|
276
|
+
function baseCatalogConfig(context) {
|
|
277
|
+
return context.pluginCatalogConfig ?? (context.pluginSet ? pluginCatalogConfigFromPluginSet(context.pluginSet) : pluginCatalogConfigFromEnv());
|
|
3297
278
|
}
|
|
3298
|
-
function
|
|
3299
|
-
|
|
3300
|
-
let count = 0;
|
|
3301
|
-
while (count < limit && isDeepStrictEqual(
|
|
3302
|
-
previous[count].payload.message,
|
|
3303
|
-
current[count].payload.message
|
|
3304
|
-
)) {
|
|
3305
|
-
count += 1;
|
|
3306
|
-
}
|
|
3307
|
-
return count;
|
|
279
|
+
function inlinePluginName(definition) {
|
|
280
|
+
return definition.manifest.name;
|
|
3308
281
|
}
|
|
3309
|
-
function
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
`Conversation message event ${row.seq} has no message object`
|
|
3314
|
-
);
|
|
282
|
+
function mergeInlineManifests(left, right) {
|
|
283
|
+
const merged = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const definition of [...left ?? [], ...right ?? []]) {
|
|
285
|
+
merged.set(inlinePluginName(definition), definition);
|
|
3315
286
|
}
|
|
3316
|
-
return
|
|
3317
|
-
message: normalizeCheckpoint ? normalizeLegacyContextMessage(message) : message,
|
|
3318
|
-
...row.payload.provenance === void 0 ? {} : { provenance: row.payload.provenance }
|
|
3319
|
-
};
|
|
287
|
+
return merged.size > 0 ? [...merged.values()] : void 0;
|
|
3320
288
|
}
|
|
3321
|
-
function
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
289
|
+
function mergeCatalogConfig(base, added) {
|
|
290
|
+
if (!base) {
|
|
291
|
+
return added;
|
|
292
|
+
}
|
|
293
|
+
if (!added) {
|
|
294
|
+
return base;
|
|
3326
295
|
}
|
|
296
|
+
const inlineManifests = mergeInlineManifests(
|
|
297
|
+
base.inlineManifests,
|
|
298
|
+
added.inlineManifests
|
|
299
|
+
);
|
|
300
|
+
const packages = unique([
|
|
301
|
+
...base.packages ?? [],
|
|
302
|
+
...added.packages ?? []
|
|
303
|
+
]);
|
|
304
|
+
const manifests = base.manifests || added.manifests ? { ...base.manifests, ...added.manifests } : void 0;
|
|
3327
305
|
return {
|
|
3328
|
-
|
|
3329
|
-
|
|
306
|
+
...inlineManifests ? { inlineManifests } : {},
|
|
307
|
+
...packages.length > 0 ? { packages } : {},
|
|
308
|
+
...manifests ? { manifests } : {}
|
|
3330
309
|
};
|
|
3331
310
|
}
|
|
3332
|
-
function
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
(row) => row.seq < marker.seq && row.type === "tool_execution_started" && row.payload.toolName === "handoff" && typeof row.payload.toolCallId === "string" && row.payload.toolCallId.length > 0
|
|
3338
|
-
);
|
|
3339
|
-
if (typeof tool?.payload.toolCallId !== "string") {
|
|
3340
|
-
throw new Error(
|
|
3341
|
-
`Cannot correlate handoff checkpoint at event ${marker.seq} during upgrade`
|
|
3342
|
-
);
|
|
3343
|
-
}
|
|
3344
|
-
return tool.payload.toolCallId;
|
|
311
|
+
function packageNamesFromContext(context, catalog) {
|
|
312
|
+
return unique([
|
|
313
|
+
...context.pluginSet?.packageNames ?? [],
|
|
314
|
+
...catalog?.packages ?? []
|
|
315
|
+
]);
|
|
3345
316
|
}
|
|
3346
|
-
async function
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
);
|
|
3362
|
-
const messagesByEpoch = /* @__PURE__ */ new Map();
|
|
3363
|
-
for (const row of rows) {
|
|
3364
|
-
if (row.type !== "agent_step") continue;
|
|
3365
|
-
const messages = messagesByEpoch.get(row.historyVersion) ?? [];
|
|
3366
|
-
messages.push(row);
|
|
3367
|
-
messagesByEpoch.set(row.historyVersion, messages);
|
|
3368
|
-
}
|
|
3369
|
-
let migrated = 0;
|
|
3370
|
-
const removedSeqs = [];
|
|
3371
|
-
for (const marker of rows) {
|
|
3372
|
-
if (marker.type !== "context_epoch_started") {
|
|
3373
|
-
continue;
|
|
3374
|
-
}
|
|
3375
|
-
const reason = marker.payload.reason;
|
|
3376
|
-
if (reason === "initial") {
|
|
3377
|
-
removedSeqs.push(marker.seq);
|
|
3378
|
-
migrated += 1;
|
|
3379
|
-
continue;
|
|
3380
|
-
}
|
|
3381
|
-
if (reason !== "compaction" && reason !== "handoff" && reason !== "rollback") {
|
|
3382
|
-
throw new Error(
|
|
3383
|
-
`Unknown context checkpoint reason at event ${marker.seq}`
|
|
3384
|
-
);
|
|
3385
|
-
}
|
|
3386
|
-
let replacementHistory = marker.payload.replacementHistory;
|
|
3387
|
-
if (!Array.isArray(replacementHistory)) {
|
|
3388
|
-
const current = messagesByEpoch.get(marker.historyVersion) ?? [];
|
|
3389
|
-
let replacementCount;
|
|
3390
|
-
if (reason === "rollback") {
|
|
3391
|
-
const previous = messagesByEpoch.get(marker.historyVersion - 1) ?? [];
|
|
3392
|
-
replacementCount = matchingMessagePrefix(previous, current);
|
|
3393
|
-
} else {
|
|
3394
|
-
const end = checkpointEnd(current);
|
|
3395
|
-
replacementCount = end < 0 ? current.length : end + 1;
|
|
3396
|
-
}
|
|
3397
|
-
const replacementRows = current.slice(0, replacementCount);
|
|
3398
|
-
replacementHistory = replacementRows.map(
|
|
3399
|
-
(row, index) => replacementEntry(
|
|
3400
|
-
row,
|
|
3401
|
-
reason !== "rollback" && index === replacementRows.length - 1
|
|
3402
|
-
)
|
|
3403
|
-
);
|
|
3404
|
-
removedSeqs.push(...replacementRows.map((row) => row.seq));
|
|
3405
|
-
}
|
|
3406
|
-
const binding = resolvedBinding(marker.payload, args.bot);
|
|
3407
|
-
const parsed = conversationEventDataSchema.parse({
|
|
3408
|
-
type: reason,
|
|
3409
|
-
...binding,
|
|
3410
|
-
...reason === "handoff" ? { triggeringToolCallId: handoffToolCallId(rows, marker) } : {},
|
|
3411
|
-
replacementHistory
|
|
3412
|
-
});
|
|
3413
|
-
const { type, ...payload } = parsed;
|
|
3414
|
-
await args.executor.execute(
|
|
3415
|
-
`UPDATE junior_conversation_events
|
|
3416
|
-
SET type = $3, payload = $4::jsonb
|
|
3417
|
-
WHERE conversation_id = $1 AND seq = $2`,
|
|
3418
|
-
[
|
|
3419
|
-
args.conversationId,
|
|
3420
|
-
marker.seq,
|
|
3421
|
-
type,
|
|
3422
|
-
JSON.stringify(sanitizePostgresJson(payload))
|
|
3423
|
-
]
|
|
3424
|
-
);
|
|
3425
|
-
migrated += 1;
|
|
3426
|
-
}
|
|
3427
|
-
if (removedSeqs.length > 0) {
|
|
3428
|
-
await args.executor.execute(
|
|
3429
|
-
`UPDATE junior_conversation_events
|
|
3430
|
-
SET payload = jsonb_set(
|
|
3431
|
-
payload,
|
|
3432
|
-
'{historyFromSeq}',
|
|
3433
|
-
to_jsonb(
|
|
3434
|
-
(payload->>'historyFromSeq')::integer - (
|
|
3435
|
-
SELECT count(*)::integer
|
|
3436
|
-
FROM unnest($2::integer[]) removed(seq)
|
|
3437
|
-
WHERE removed.seq < (payload->>'historyFromSeq')::integer
|
|
3438
|
-
)
|
|
3439
|
-
)
|
|
3440
|
-
)
|
|
3441
|
-
WHERE conversation_id = $1
|
|
3442
|
-
AND type = 'messages_summarized'
|
|
3443
|
-
AND jsonb_typeof(payload->'historyFromSeq') = 'number'`,
|
|
3444
|
-
[args.conversationId, removedSeqs]
|
|
3445
|
-
);
|
|
3446
|
-
await args.executor.execute(
|
|
3447
|
-
`DELETE FROM junior_conversation_events
|
|
3448
|
-
WHERE conversation_id = $1 AND seq = ANY($2::integer[])`,
|
|
3449
|
-
[args.conversationId, removedSeqs]
|
|
3450
|
-
);
|
|
3451
|
-
await args.executor.execute(
|
|
3452
|
-
`UPDATE junior_conversation_events
|
|
3453
|
-
SET seq = -seq - 1
|
|
3454
|
-
WHERE conversation_id = $1`,
|
|
3455
|
-
[args.conversationId]
|
|
3456
|
-
);
|
|
3457
|
-
await args.executor.execute(
|
|
3458
|
-
`WITH ranked AS (
|
|
3459
|
-
SELECT
|
|
3460
|
-
ctid,
|
|
3461
|
-
row_number() OVER (ORDER BY -seq - 1) - 1 AS next_seq
|
|
3462
|
-
FROM junior_conversation_events
|
|
3463
|
-
WHERE conversation_id = $1
|
|
3464
|
-
)
|
|
3465
|
-
UPDATE junior_conversation_events event
|
|
3466
|
-
SET seq = ranked.next_seq
|
|
3467
|
-
FROM ranked
|
|
3468
|
-
WHERE event.ctid = ranked.ctid`,
|
|
3469
|
-
[args.conversationId]
|
|
3470
|
-
);
|
|
3471
|
-
}
|
|
3472
|
-
return migrated;
|
|
3473
|
-
})
|
|
317
|
+
async function resolveUpgradePluginCatalog(context) {
|
|
318
|
+
const catalog = baseCatalogConfig(context);
|
|
319
|
+
const packageNames = packageNamesFromContext(context, catalog);
|
|
320
|
+
const registrations = context.pluginSet?.registrations ?? [];
|
|
321
|
+
const manifests = context.pluginSet?.manifests || catalog?.manifests ? {
|
|
322
|
+
...catalog?.manifests,
|
|
323
|
+
...context.pluginSet?.manifests
|
|
324
|
+
} : void 0;
|
|
325
|
+
const pluginSet = packageNames.length > 0 || registrations.length > 0 || context.pluginSet ? defineJuniorPlugins(
|
|
326
|
+
[...packageNames, ...registrations],
|
|
327
|
+
manifests ? { manifests } : {}
|
|
328
|
+
) : void 0;
|
|
329
|
+
return mergeCatalogConfig(
|
|
330
|
+
catalog,
|
|
331
|
+
pluginCatalogConfigFromPluginSet(pluginSet)
|
|
3474
332
|
);
|
|
3475
333
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
driver: config.sql.driver
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
try {
|
|
3490
|
-
const result = {
|
|
3491
|
-
existing: 0,
|
|
3492
|
-
migrated: 0,
|
|
3493
|
-
missing: 0,
|
|
3494
|
-
scanned: 0
|
|
3495
|
-
};
|
|
3496
|
-
while (true) {
|
|
3497
|
-
const conversations = await executor.query(
|
|
3498
|
-
`SELECT DISTINCT conversation_id AS "conversationId"
|
|
3499
|
-
FROM junior_conversation_events
|
|
3500
|
-
WHERE type = 'context_epoch_started'
|
|
3501
|
-
ORDER BY conversation_id
|
|
3502
|
-
LIMIT $1`,
|
|
3503
|
-
[PAGE_SIZE]
|
|
3504
|
-
);
|
|
3505
|
-
if (conversations.length === 0) break;
|
|
3506
|
-
await prepareConversationEventResequence(
|
|
3507
|
-
context,
|
|
3508
|
-
new Set(conversations.map(({ conversationId }) => conversationId))
|
|
3509
|
-
);
|
|
3510
|
-
for (const { conversationId } of conversations) {
|
|
3511
|
-
result.scanned += 1;
|
|
3512
|
-
result.migrated += await normalizeConversation({
|
|
3513
|
-
bot,
|
|
3514
|
-
conversationId,
|
|
3515
|
-
executor
|
|
3516
|
-
});
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
return result;
|
|
3520
|
-
} finally {
|
|
3521
|
-
if (!options.executor) await executor.close();
|
|
3522
|
-
}
|
|
334
|
+
|
|
335
|
+
// src/cli/upgrade/migrations/plugin-sql.ts
|
|
336
|
+
async function migratePluginsToSql(context, options = {}) {
|
|
337
|
+
const pluginCatalog = createPluginCatalogRuntime();
|
|
338
|
+
pluginCatalog.setConfig(await resolveUpgradePluginCatalog(context));
|
|
339
|
+
return await migratePluginSchemas(
|
|
340
|
+
context.sqlExecutor,
|
|
341
|
+
pluginCatalog.getMigrationRoots(),
|
|
342
|
+
options
|
|
343
|
+
);
|
|
3523
344
|
}
|
|
3524
|
-
var conversationContextCheckpointMigration = {
|
|
3525
|
-
name: "normalize-conversation-context-checkpoints",
|
|
3526
|
-
run: normalizeConversationContextCheckpoints
|
|
3527
|
-
};
|
|
3528
345
|
|
|
3529
346
|
// src/cli/upgrade.ts
|
|
3530
347
|
var DEFAULT_IO = {
|
|
3531
348
|
info: console.log
|
|
3532
349
|
};
|
|
3533
350
|
var localPluginLoader = createJiti(import.meta.url, { moduleCache: false });
|
|
3534
|
-
var MIGRATIONS = [
|
|
3535
|
-
agentTurnSessionActorMigration,
|
|
3536
|
-
redisConversationStateMigration,
|
|
3537
|
-
coreSqlSchemaMigration,
|
|
3538
|
-
sqlConversationMigration,
|
|
3539
|
-
conversationContextCheckpointMigration,
|
|
3540
|
-
// The external history import must precede the SQL read-model backfill.
|
|
3541
|
-
// Visible events seal a conversation against legacy Redis/session import.
|
|
3542
|
-
sqlConversationHistoryMigration,
|
|
3543
|
-
conversationVisibleMessageEventsMigration,
|
|
3544
|
-
conversationUsageRepairMigration,
|
|
3545
|
-
sqlPluginMigration,
|
|
3546
|
-
pluginStorageMigration
|
|
3547
|
-
];
|
|
3548
351
|
function isMissingVirtualConfig(error) {
|
|
3549
352
|
if (!(error instanceof Error)) {
|
|
3550
353
|
return false;
|
|
@@ -3566,50 +369,56 @@ async function resolveUpgradePluginSet() {
|
|
|
3566
369
|
async (moduleRef) => localPluginLoader.import(moduleRef.importPath)
|
|
3567
370
|
);
|
|
3568
371
|
}
|
|
3569
|
-
function
|
|
3570
|
-
|
|
3571
|
-
`scanned=${result.scanned}`,
|
|
3572
|
-
`migrated=${result.migrated}`,
|
|
3573
|
-
`existing=${result.existing}`,
|
|
3574
|
-
`missing=${result.missing}`
|
|
3575
|
-
];
|
|
3576
|
-
if (result.skipped !== void 0) {
|
|
3577
|
-
fields.push(`skipped=${result.skipped}`);
|
|
3578
|
-
}
|
|
3579
|
-
return fields.join(" ");
|
|
372
|
+
function migrationCount(count) {
|
|
373
|
+
return `${count} ${count === 1 ? "migration" : "migrations"}`;
|
|
3580
374
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
const results = [];
|
|
3585
|
-
for (const migration of MIGRATIONS) {
|
|
3586
|
-
migrationContext.io.info(`Running migration ${migration.name}...`);
|
|
3587
|
-
const result = await migration.run(migrationContext);
|
|
3588
|
-
migrationContext.io.info(
|
|
3589
|
-
`Finished migration ${migration.name}: ${formatMigrationResult(result)}`
|
|
3590
|
-
);
|
|
3591
|
-
results.push(result);
|
|
375
|
+
function formatOwnerSummary(owner, summary) {
|
|
376
|
+
if (summary.migrated === 0) {
|
|
377
|
+
return ` ${owner}: up to date (${migrationCount(summary.scanned)})`;
|
|
3592
378
|
}
|
|
3593
|
-
return
|
|
379
|
+
return ` ${owner}: applied ${migrationCount(summary.migrated)} (${summary.scanned} total)`;
|
|
380
|
+
}
|
|
381
|
+
function pluginMigrationOwner(pluginName) {
|
|
382
|
+
const unscopedName = pluginName.split("/").at(-1) ?? pluginName;
|
|
383
|
+
return unscopedName.startsWith("junior-") ? unscopedName : `junior-${unscopedName}`;
|
|
384
|
+
}
|
|
385
|
+
async function runDatabaseMigrations(context, io) {
|
|
386
|
+
io.info("Checking database migrations...");
|
|
387
|
+
const core = await migrateSchema(context.sqlExecutor);
|
|
388
|
+
io.info(formatOwnerSummary("junior", core));
|
|
389
|
+
const plugins = await migratePluginsToSql(context, {
|
|
390
|
+
onPluginMigration: (summary) => {
|
|
391
|
+
io.info(
|
|
392
|
+
formatOwnerSummary(pluginMigrationOwner(summary.pluginName), summary)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const migrated = core.migrated + plugins.migrated;
|
|
397
|
+
const scanned = core.scanned + plugins.scanned;
|
|
398
|
+
io.info(
|
|
399
|
+
migrated === 0 ? `Database is up to date (${migrationCount(scanned)}).` : `Applied ${migrationCount(migrated)} (${scanned} total).`
|
|
400
|
+
);
|
|
3594
401
|
}
|
|
3595
402
|
async function runUpgrade(io = DEFAULT_IO, options = {}) {
|
|
403
|
+
const { sql } = getChatConfig();
|
|
404
|
+
const sqlExecutor = createJuniorSqlExecutor({
|
|
405
|
+
connectionString: sql.databaseUrl,
|
|
406
|
+
driver: sql.driver
|
|
407
|
+
});
|
|
3596
408
|
try {
|
|
3597
|
-
const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
|
|
3598
409
|
const pluginSet = options.pluginSet === void 0 ? await resolveUpgradePluginSet() : options.pluginSet ?? void 0;
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
io.info("Junior upgrade complete.");
|
|
410
|
+
await runDatabaseMigrations(
|
|
411
|
+
{
|
|
412
|
+
pluginSet,
|
|
413
|
+
sqlExecutor
|
|
414
|
+
},
|
|
415
|
+
io
|
|
416
|
+
);
|
|
3607
417
|
} finally {
|
|
3608
|
-
await
|
|
418
|
+
await sqlExecutor.close();
|
|
3609
419
|
}
|
|
3610
420
|
}
|
|
3611
421
|
export {
|
|
3612
422
|
resolveUpgradePluginSet,
|
|
3613
|
-
runUpgrade
|
|
3614
|
-
runUpgradeMigrations
|
|
423
|
+
runUpgrade
|
|
3615
424
|
};
|