maka-agent 0.2.0-dev.21.20260905 → 0.2.0-dev.22.20260905
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/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +3 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +6 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +12 -2
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +1 -2
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +32 -35
- package/node_modules/@maka/storage/dist/artifact-store.js +42 -3
- package/node_modules/@maka/storage/dist/artifact-stores.js +1 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +1 -0
- package/node_modules/@maka/storage/dist/session-store.js +5 -10
- package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +15 -0
- package/node_modules/@maka/storage/dist/sqlite-artifact-schema.js +28 -8
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +105 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -62,7 +62,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
|
|
|
62
62
|
export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
|
|
63
63
|
// Increment when the same protocol version no longer guarantees safe Client-Host
|
|
64
64
|
// interoperability. Mismatches are rejected before domain commands are admitted.
|
|
65
|
-
export const RUNTIME_HOST_COMPATIBILITY_EPOCH =
|
|
65
|
+
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 117;
|
|
66
|
+
// 117: WorkHub exposes only one correction linkage per bounded candidate and
|
|
67
|
+
// no longer returns the Host's complete active-link set.
|
|
66
68
|
// 116: User deletion rejects workflow-owned Artifacts with operation_conflict.
|
|
67
69
|
// 115: Artifact creation requires explicit source ownership.
|
|
68
70
|
// 114: Artifacts are physically deleted and no longer expose tombstone status.
|
|
@@ -277,14 +277,7 @@ export function decodeWorkHubCoordinationActResult(value) {
|
|
|
277
277
|
throw invalidProtocolFrame('Invalid WorkHub Coordination action disposition');
|
|
278
278
|
}
|
|
279
279
|
function decodeWorkHubCoordinationCandidate(value) {
|
|
280
|
-
const candidate =
|
|
281
|
-
'candidateRef',
|
|
282
|
-
'sessionId',
|
|
283
|
-
'sessionName',
|
|
284
|
-
'workspace',
|
|
285
|
-
'state',
|
|
286
|
-
'updatedAt',
|
|
287
|
-
]);
|
|
280
|
+
const candidate = requireShapedRecord(value, 'WorkHub Coordination candidate', ['candidateRef', 'sessionId', 'sessionName', 'workspace', 'state', 'updatedAt'], ['latestDelegationActionId']);
|
|
288
281
|
return {
|
|
289
282
|
candidateRef: requireEntityId(candidate.candidateRef, 'WorkHub candidate ref'),
|
|
290
283
|
sessionId: requireEntityId(candidate.sessionId, 'WorkHub candidate Session id'),
|
|
@@ -292,6 +285,11 @@ function decodeWorkHubCoordinationCandidate(value) {
|
|
|
292
285
|
workspace: decodeWorkspaceProjection(candidate.workspace),
|
|
293
286
|
state: candidateState(candidate.state),
|
|
294
287
|
updatedAt: requireCount(candidate.updatedAt, 'WorkHub candidate update time'),
|
|
288
|
+
...(candidate.latestDelegationActionId === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
latestDelegationActionId: requireEntityId(candidate.latestDelegationActionId, 'WorkHub latest delegation action id'),
|
|
292
|
+
}),
|
|
295
293
|
};
|
|
296
294
|
}
|
|
297
295
|
function decodeWorkHubCoordinationProposal(value) {
|
|
@@ -1038,8 +1038,10 @@ export async function createExecutionRuntimeHostComposition(context, options = {
|
|
|
1038
1038
|
continuity: continuityCoordinator,
|
|
1039
1039
|
executions: coordinator,
|
|
1040
1040
|
sessionActions: {
|
|
1041
|
-
readDelegationRetirement: async (assignment) => {
|
|
1042
|
-
const disposition =
|
|
1041
|
+
readDelegationRetirement: async (assignment, admission) => {
|
|
1042
|
+
const disposition = admission
|
|
1043
|
+
? await messages.readMessageExecutionDispositionAdmitted(assignment.targetSessionId, assignment.targetMessageId, admission)
|
|
1044
|
+
: await messages.readMessageExecutionDisposition(assignment.targetSessionId, assignment.targetMessageId);
|
|
1043
1045
|
if (disposition.kind === 'recovering')
|
|
1044
1046
|
return 'recovering';
|
|
1045
1047
|
if (disposition.kind === 'pending')
|
|
@@ -1394,6 +1396,14 @@ export async function createExecutionRuntimeHostComposition(context, options = {
|
|
|
1394
1396
|
recovery: {
|
|
1395
1397
|
state: async () => {
|
|
1396
1398
|
await skills.recover();
|
|
1399
|
+
try {
|
|
1400
|
+
await openedArtifactStore.reclaimUpgradeResidue();
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
// Leftover bytes are not worth refusing to start over; the next
|
|
1404
|
+
// start tries again.
|
|
1405
|
+
console.error(`[runtime-host] upgrade residue could not be reclaimed: ${generalizedErrorMessage(error)}`);
|
|
1406
|
+
}
|
|
1397
1407
|
},
|
|
1398
1408
|
},
|
|
1399
1409
|
drain: [
|
|
@@ -187,6 +187,9 @@ export class HostMessageCoordinator {
|
|
|
187
187
|
readMessageExecutionDisposition(sessionId, messageId) {
|
|
188
188
|
return this.#sessionAdmission.run(sessionId, () => this.#resolveMessageExecution(sessionId, messageId));
|
|
189
189
|
}
|
|
190
|
+
readMessageExecutionDispositionAdmitted(sessionId, messageId, admission) {
|
|
191
|
+
return this.#sessionAdmission.runAdmitted(sessionId, admission, () => this.#resolveMessageExecution(sessionId, messageId));
|
|
192
|
+
}
|
|
190
193
|
async #resolveMessageExecution(sessionId, messageId) {
|
|
191
194
|
const receipt = await this.#durableProof.readRootTurnSourceMessageReceipt(sessionId, messageId);
|
|
192
195
|
if (receipt?.admission.sessionId === sessionId &&
|
|
@@ -259,8 +259,7 @@ export class WorkHubCoordinationActionGate {
|
|
|
259
259
|
return claimed;
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
-
const
|
|
263
|
-
const onTarget = active.filter((assignment) => assignment.targetSessionId === targetSessionId);
|
|
262
|
+
const onTarget = await this.#effects.listActiveAssignments(targetSessionId);
|
|
264
263
|
if (onTarget.length === 0) {
|
|
265
264
|
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub has no active durable delegation to stop on that Session');
|
|
266
265
|
}
|
|
@@ -79,7 +79,9 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
79
79
|
readActionClaim: (actionId) => this.#stores.readWorkHubActionClaim(actionId),
|
|
80
80
|
probeTargetRemoval: async (sessionId) => (await this.#stores.probeSessionRemoval(sessionId)).kind,
|
|
81
81
|
readAssignment: (actionId) => this.#stores.readWorkHubAssignment(actionId),
|
|
82
|
-
|
|
82
|
+
// This lookup is advisory. Stop and replacement both repeat their exact
|
|
83
|
+
// proof under the Coordination and target admissions before writing.
|
|
84
|
+
listActiveAssignments: (targetSessionId) => this.#stores.readActiveWorkHubAssignmentsByTarget([targetSessionId]),
|
|
83
85
|
readReplacement: (delegationId) => this.#stores.readWorkHubReplacement(delegationId),
|
|
84
86
|
readReplacementAbort: (delegationId) => this.#stores.readWorkHubReplacementAbort(delegationId),
|
|
85
87
|
readSupersession: (delegationId) => this.#stores.readWorkHubSupersession(delegationId),
|
|
@@ -113,6 +115,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
113
115
|
#prepareReplacement(input) {
|
|
114
116
|
const suffix = workHubDestructiveClaimIdentitySuffix(input.replacesDelegationId);
|
|
115
117
|
return this.#commitCoordinationFact({
|
|
118
|
+
admissionSessionIds: [WORKHUB_COORDINATION_SESSION_ID, input.replacedTargetSessionId],
|
|
116
119
|
read: () => this.#stores.readWorkHubReplacement(input.replacesDelegationId),
|
|
117
120
|
build: (existing) => ({
|
|
118
121
|
type: 'workhub_coordination',
|
|
@@ -136,6 +139,11 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
136
139
|
}),
|
|
137
140
|
conflictMessage: 'WorkHub action identity belongs to a different replacement',
|
|
138
141
|
beforeAppend: async () => {
|
|
142
|
+
const latest = (await this.#stores.readActiveWorkHubAssignmentsByTarget([input.replacedTargetSessionId], 1))[0];
|
|
143
|
+
if (latest?.actionId !== input.replacesActionId ||
|
|
144
|
+
latest.delegationId !== input.replacesDelegationId) {
|
|
145
|
+
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub correction source is no longer the latest active delegation');
|
|
146
|
+
}
|
|
139
147
|
const stopRequest = await this.#stores.readWorkHubStopRequest(input.replacesDelegationId);
|
|
140
148
|
if (stopRequest) {
|
|
141
149
|
const resolution = await this.#stores.readWorkHubStopResolution(input.replacesDelegationId);
|
|
@@ -179,21 +187,19 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
179
187
|
userText: input.userText,
|
|
180
188
|
}),
|
|
181
189
|
conflictMessage: 'WorkHub delegation already has a different stop claim',
|
|
182
|
-
beforeAppend: async () => {
|
|
183
|
-
const [replacement, supersession,
|
|
190
|
+
beforeAppend: async (lease) => {
|
|
191
|
+
const [replacement, supersession, activeAssignments] = await Promise.all([
|
|
184
192
|
this.#stores.readWorkHubReplacement(input.stopsDelegationId),
|
|
185
193
|
this.#stores.readWorkHubSupersession(input.stopsDelegationId),
|
|
186
|
-
this.#stores.
|
|
194
|
+
this.#stores.readActiveWorkHubAssignmentsByTarget([input.targetSessionId]),
|
|
187
195
|
]);
|
|
188
196
|
if (replacement || supersession) {
|
|
189
197
|
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub delegation is already being replaced');
|
|
190
198
|
}
|
|
191
|
-
const activeAssignments = activeWorkHubAssignments(messages);
|
|
192
199
|
// Held lanes make this the last moment the one-target proof can change.
|
|
193
200
|
// It is proved from opaque delegation identity, so a concurrent rename
|
|
194
201
|
// is harmless while a concurrent delegation to the same Session is not.
|
|
195
|
-
const
|
|
196
|
-
const source = targetActive.find((assignment) => assignment.actionId === input.stopsActionId &&
|
|
202
|
+
const source = activeAssignments.find((assignment) => assignment.actionId === input.stopsActionId &&
|
|
197
203
|
assignment.delegationId === input.stopsDelegationId);
|
|
198
204
|
if (!source) {
|
|
199
205
|
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub stop target does not identify one active durable delegation');
|
|
@@ -201,10 +207,10 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
201
207
|
// A delegation whose work already finished stays linked but competes
|
|
202
208
|
// for nothing; only work that could still be stopped makes the target
|
|
203
209
|
// ambiguous.
|
|
204
|
-
for (const competitor of
|
|
210
|
+
for (const competitor of activeAssignments) {
|
|
205
211
|
if (competitor.delegationId === source.delegationId)
|
|
206
212
|
continue;
|
|
207
|
-
if ((await this.#readDelegationRetirement(competitor)) !== 'retired') {
|
|
213
|
+
if ((await this.#readDelegationRetirement(competitor, lease)) !== 'retired') {
|
|
208
214
|
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub stop target does not identify one active durable delegation');
|
|
209
215
|
}
|
|
210
216
|
}
|
|
@@ -212,9 +218,6 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
212
218
|
unknownOutcomeMessage: 'WorkHub stop request outcome is unknown',
|
|
213
219
|
});
|
|
214
220
|
}
|
|
215
|
-
async #listActiveAssignments() {
|
|
216
|
-
return activeWorkHubAssignments(await this.#stores.readMessagesSnapshot(WORKHUB_COORDINATION_SESSION_ID));
|
|
217
|
-
}
|
|
218
221
|
#resolveStop(input) {
|
|
219
222
|
const request = input.request;
|
|
220
223
|
const suffix = workHubDestructiveClaimIdentitySuffix(request.stopsDelegationId);
|
|
@@ -286,7 +289,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
286
289
|
}
|
|
287
290
|
return existing;
|
|
288
291
|
}
|
|
289
|
-
await options.beforeAppend();
|
|
292
|
+
await options.beforeAppend(lease);
|
|
290
293
|
try {
|
|
291
294
|
await this.#stores.appendMessages(WORKHUB_COORDINATION_SESSION_ID, [requested]);
|
|
292
295
|
await this.#continuity.refreshCanonical(WORKHUB_COORDINATION_SESSION_ID, lease);
|
|
@@ -303,7 +306,22 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
303
306
|
}
|
|
304
307
|
async #candidates() {
|
|
305
308
|
try {
|
|
306
|
-
|
|
309
|
+
const result = await this.#actionGate.candidates();
|
|
310
|
+
// One bounded read for the whole page. The candidate set is already
|
|
311
|
+
// capped, and a per-candidate lookup would rescan each target's history.
|
|
312
|
+
const latestByTarget = new Map((await this.#stores.readActiveWorkHubAssignmentsByTarget(result.candidates.map(({ sessionId }) => sessionId), 1)).map((assignment) => [assignment.targetSessionId, assignment.actionId]));
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
result: {
|
|
316
|
+
candidateSetId: result.candidateSetId,
|
|
317
|
+
candidates: result.candidates.map((candidate) => {
|
|
318
|
+
const latestDelegationActionId = latestByTarget.get(candidate.sessionId);
|
|
319
|
+
return latestDelegationActionId
|
|
320
|
+
? { ...candidate, latestDelegationActionId }
|
|
321
|
+
: candidate;
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
307
325
|
}
|
|
308
326
|
catch {
|
|
309
327
|
return {
|
|
@@ -562,27 +580,6 @@ function validCoordinationHeader(header) {
|
|
|
562
580
|
function digest(value) {
|
|
563
581
|
return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
|
|
564
582
|
}
|
|
565
|
-
function activeWorkHubAssignments(messages) {
|
|
566
|
-
const terminalDelegationIds = new Set();
|
|
567
|
-
const assignments = [];
|
|
568
|
-
for (const message of messages) {
|
|
569
|
-
if (message.type !== 'workhub_coordination')
|
|
570
|
-
continue;
|
|
571
|
-
if (message.kind === 'delegation_assigned') {
|
|
572
|
-
assignments.push(message);
|
|
573
|
-
}
|
|
574
|
-
else if (message.kind === 'delegation_superseded') {
|
|
575
|
-
terminalDelegationIds.add(message.supersededDelegationId);
|
|
576
|
-
}
|
|
577
|
-
else if (message.kind === 'delegation_replacement_aborted') {
|
|
578
|
-
terminalDelegationIds.add(message.abortedDelegationId);
|
|
579
|
-
}
|
|
580
|
-
else if (message.kind === 'delegation_stop_resolved' && message.outcome !== 'not_owned') {
|
|
581
|
-
terminalDelegationIds.add(message.stopsDelegationId);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return assignments.filter(({ delegationId }) => !terminalDelegationIds.has(delegationId));
|
|
585
|
-
}
|
|
586
583
|
function workHubDestructiveClaimIdentitySuffix(delegationId) {
|
|
587
584
|
return createHash('sha256').update(delegationId, 'utf8').digest('hex').slice(0, 48);
|
|
588
585
|
}
|
|
@@ -233,6 +233,48 @@ class SqliteArtifactStore {
|
|
|
233
233
|
await this.purgeRecordsUnlocked(this.records.filter((record) => record.sessionId === sessionId));
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Deletes the files the v1 upgrade recorded as no longer named by any record.
|
|
238
|
+
*
|
|
239
|
+
* A path some record has since claimed keeps its bytes. A note is discharged
|
|
240
|
+
* once its file is gone, and a file that will not go keeps only its own note
|
|
241
|
+
* rather than holding up the ones behind it.
|
|
242
|
+
*/
|
|
243
|
+
async reclaimUpgradeResidue() {
|
|
244
|
+
await this.enqueueMutation(async () => {
|
|
245
|
+
await this.prepareMutationUnlocked();
|
|
246
|
+
const recorded = this.metadataRepository.readUpgradeOrphanPaths();
|
|
247
|
+
if (recorded.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
const claimed = new Set(this.records.map((record) => record.relativePath));
|
|
250
|
+
const directories = new Set();
|
|
251
|
+
const discharged = [];
|
|
252
|
+
try {
|
|
253
|
+
for (const relativePath of recorded) {
|
|
254
|
+
if (claimed.has(relativePath) || !isSafeRelativeArtifactPath(relativePath)) {
|
|
255
|
+
discharged.push(relativePath);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const target = join(this.artifactRoot, relativePath);
|
|
259
|
+
try {
|
|
260
|
+
await unlink(target);
|
|
261
|
+
directories.add(dirname(target));
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (!isNotFound(error))
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
discharged.push(relativePath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
for (const directory of directories)
|
|
272
|
+
await syncDirectory(directory);
|
|
273
|
+
}
|
|
274
|
+
if (discharged.length > 0)
|
|
275
|
+
this.metadataRepository.forgetUpgradeOrphanPaths(discharged);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
236
278
|
async replayExistingArtifactUnlocked(existing, input, canonical) {
|
|
237
279
|
const expectedBytes = Buffer.from(input.content);
|
|
238
280
|
if (existing.id !== canonical.id ||
|
|
@@ -726,9 +768,6 @@ function assertArtifactTurnKey(value) {
|
|
|
726
768
|
}
|
|
727
769
|
const ARTIFACT_KIND_SET = new Set(ARTIFACT_KINDS);
|
|
728
770
|
const ARTIFACT_SOURCE_SET = new Set(ARTIFACT_SOURCES);
|
|
729
|
-
function isRecord(value) {
|
|
730
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
731
|
-
}
|
|
732
771
|
async function assertArtifactDirectory(artifactRoot, directory) {
|
|
733
772
|
const root = await ensureRealDirectory(artifactRoot);
|
|
734
773
|
const resolvedDirectory = await realpath(directory);
|
|
@@ -103,6 +103,7 @@ function createWriterFacade(lease, authority) {
|
|
|
103
103
|
return run(() => store.copyConversationArtifacts(acceptedInput));
|
|
104
104
|
},
|
|
105
105
|
purgeSessionArtifacts: (sessionId) => run(() => store.purgeSessionArtifacts(sessionId)),
|
|
106
|
+
reclaimUpgradeResidue: () => run(() => store.reclaimUpgradeResidue()),
|
|
106
107
|
deleteUserArtifactInSession: (sessionId, artifactId) => run(() => store.deleteUserArtifactInSession(sessionId, artifactId)),
|
|
107
108
|
close: () => {
|
|
108
109
|
if (writerByLease.get(lease) === facade)
|
|
@@ -124,6 +124,7 @@ async function createExecutionStoresForWrite(lease, kind, extension) {
|
|
|
124
124
|
createStableSession: (request, initialBoundary) => run(() => sessionStore.createStableSession(request, initialBoundary)),
|
|
125
125
|
assignWorkHubMessage: (request) => run(() => sessionStore.assignWorkHubMessage(request)),
|
|
126
126
|
readWorkHubAssignment: (actionId) => run(() => sessionStore.readWorkHubAssignment(actionId)),
|
|
127
|
+
readActiveWorkHubAssignmentsByTarget: (targetSessionIds, maxAssignmentsPerTarget) => run(() => sessionStore.readActiveWorkHubAssignmentsByTarget(targetSessionIds, maxAssignmentsPerTarget)),
|
|
127
128
|
readWorkHubReplacement: (delegationId) => run(() => sessionStore.readWorkHubReplacement(delegationId)),
|
|
128
129
|
readWorkHubReplacementAbort: (delegationId) => run(() => sessionStore.readWorkHubReplacementAbort(delegationId)),
|
|
129
130
|
readWorkHubSupersession: (delegationId) => run(() => sessionStore.readWorkHubSupersession(delegationId)),
|
|
@@ -201,6 +201,10 @@ class SqliteSessionStore {
|
|
|
201
201
|
? message
|
|
202
202
|
: undefined;
|
|
203
203
|
}
|
|
204
|
+
async readActiveWorkHubAssignmentsByTarget(targetSessionIds, maxAssignmentsPerTarget) {
|
|
205
|
+
await this.ensureReady();
|
|
206
|
+
return this.metadata.readActiveWorkHubAssignmentsByTarget(targetSessionIds, maxAssignmentsPerTarget);
|
|
207
|
+
}
|
|
204
208
|
async readWorkHubReplacement(delegationId) {
|
|
205
209
|
const message = await this.readWorkHubCoordinationMessage(`whp_${workHubIdentitySuffix(delegationId)}`);
|
|
206
210
|
return message?.type === 'workhub_coordination' &&
|
|
@@ -243,16 +247,7 @@ class SqliteSessionStore {
|
|
|
243
247
|
}
|
|
244
248
|
async readWorkHubCoordinationMessage(messageId) {
|
|
245
249
|
await this.ensureReady();
|
|
246
|
-
|
|
247
|
-
if (throughSequence === null)
|
|
248
|
-
return undefined;
|
|
249
|
-
const messages = await this.metadata.readTranscriptMessages(WORKHUB_COORDINATION_SESSION_ID, {
|
|
250
|
-
messageIds: [messageId],
|
|
251
|
-
throughSequence,
|
|
252
|
-
maxMessages: 1,
|
|
253
|
-
maxBytes: 768 * 1024,
|
|
254
|
-
});
|
|
255
|
-
return messages[0];
|
|
250
|
+
return this.metadata.readMessageById(WORKHUB_COORDINATION_SESSION_ID, messageId);
|
|
256
251
|
}
|
|
257
252
|
async discardStableConversationCopy(sessionId, requestFingerprint) {
|
|
258
253
|
await this.ensureReady();
|
|
@@ -68,6 +68,21 @@ class SqliteArtifactMetadataRepository {
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
readUpgradeOrphanPaths() {
|
|
72
|
+
this.assertOpen();
|
|
73
|
+
const rows = this.#lease.database
|
|
74
|
+
.prepare('SELECT relative_path FROM artifact_upgrade_orphan_paths ORDER BY relative_path')
|
|
75
|
+
.all();
|
|
76
|
+
return rows.map((row) => row.relative_path);
|
|
77
|
+
}
|
|
78
|
+
forgetUpgradeOrphanPaths(relativePaths) {
|
|
79
|
+
this.assertOpen();
|
|
80
|
+
this.#lease.transaction('write', () => {
|
|
81
|
+
const forget = this.#lease.database.prepare('DELETE FROM artifact_upgrade_orphan_paths WHERE relative_path = ?');
|
|
82
|
+
for (const relativePath of relativePaths)
|
|
83
|
+
forget.run(relativePath);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
71
86
|
close() {
|
|
72
87
|
if (this.#closed)
|
|
73
88
|
return;
|
|
@@ -16,28 +16,35 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import { decodeArtifactRecordJsons } from './artifact-metadata-codec.js';
|
|
19
|
+
import { decodeArtifactRecordJsons, isSafeRelativeArtifactPath, } from './artifact-metadata-codec.js';
|
|
20
20
|
export const SQLITE_ARTIFACT_SCHEMA_VERSION = 3;
|
|
21
21
|
export function migrateSqliteArtifactDatabase(db) {
|
|
22
22
|
const columns = db.prepare('PRAGMA table_info(artifact_records)').all();
|
|
23
23
|
const retained = [];
|
|
24
|
-
|
|
24
|
+
// Every path the old table named. Whatever is not carried over is a file no
|
|
25
|
+
// catalog will name again, and this is the last moment anything knows it is
|
|
26
|
+
// there. Unlinking here is not an option: a rollback after one would be
|
|
27
|
+
// unrecoverable, so the paths are recorded for the store to reclaim later.
|
|
28
|
+
const scanned = [];
|
|
29
|
+
const hasStatusColumn = columns.some(({ name }) => name === 'status');
|
|
30
|
+
if (hasStatusColumn || columns.some(({ name }) => name === 'storage_key')) {
|
|
25
31
|
const rows = db.prepare('SELECT * FROM artifact_records').all();
|
|
26
32
|
for (const row of rows) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
33
|
+
if (typeof row.relative_path === 'string' && isSafeRelativeArtifactPath(row.relative_path)) {
|
|
34
|
+
scanned.push(row.relative_path);
|
|
35
|
+
}
|
|
29
36
|
try {
|
|
30
37
|
const parsed = JSON.parse(String(row.record_json));
|
|
31
38
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
32
39
|
continue;
|
|
33
|
-
if (parsed.status !== undefined && parsed.status !== 'live')
|
|
34
|
-
continue;
|
|
35
|
-
delete parsed.status;
|
|
36
40
|
if (parsed.id !== row.artifact_id ||
|
|
37
41
|
parsed.sessionId !== row.session_id ||
|
|
38
42
|
parsed.createdAt !== row.created_at ||
|
|
39
43
|
parsed.relativePath !== row.relative_path)
|
|
40
44
|
continue;
|
|
45
|
+
if ([hasStatusColumn ? row.status : undefined, parsed.status].some((value) => value !== undefined && value !== null && value !== 'live'))
|
|
46
|
+
continue;
|
|
47
|
+
delete parsed.status;
|
|
41
48
|
retained.push(JSON.stringify(parsed));
|
|
42
49
|
}
|
|
43
50
|
catch { }
|
|
@@ -58,11 +65,24 @@ export function migrateSqliteArtifactDatabase(db) {
|
|
|
58
65
|
|
|
59
66
|
CREATE UNIQUE INDEX IF NOT EXISTS artifact_records_relative_path
|
|
60
67
|
ON artifact_records(relative_path);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS artifact_upgrade_orphan_paths (
|
|
70
|
+
relative_path TEXT PRIMARY KEY
|
|
71
|
+
);
|
|
72
|
+
`);
|
|
73
|
+
const carried = decodeArtifactRecordJsons(retained);
|
|
74
|
+
const kept = new Set(carried.map((record) => record.relativePath));
|
|
75
|
+
const orphan = db.prepare(`
|
|
76
|
+
INSERT INTO artifact_upgrade_orphan_paths VALUES (?)
|
|
77
|
+
ON CONFLICT(relative_path) DO NOTHING
|
|
61
78
|
`);
|
|
79
|
+
for (const relativePath of scanned)
|
|
80
|
+
if (!kept.has(relativePath))
|
|
81
|
+
orphan.run(relativePath);
|
|
62
82
|
const insert = db.prepare(`
|
|
63
83
|
INSERT INTO artifact_records VALUES (?, ?, ?, ?, ?)
|
|
64
84
|
`);
|
|
65
|
-
for (const record of
|
|
85
|
+
for (const record of carried) {
|
|
66
86
|
insert.run(record.id, record.sessionId, record.createdAt, record.relativePath, JSON.stringify(record));
|
|
67
87
|
}
|
|
68
88
|
}
|
|
@@ -45,6 +45,9 @@ const SQLITE_TRANSCRIPT_MESSAGE_LOOKUP_BATCH_SIZE = 256;
|
|
|
45
45
|
const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES = 1_024;
|
|
46
46
|
const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
47
47
|
const SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES = 32;
|
|
48
|
+
// Each target Session binds three parameters in the linkage query. Stay well
|
|
49
|
+
// inside SQLite's bound-parameter limit.
|
|
50
|
+
const WORKHUB_TARGET_LINKAGE_MAX_SESSIONS = 256;
|
|
48
51
|
function decodeStoredMessage(value) {
|
|
49
52
|
return decodePersistedStoredMessage(markPersisted(value));
|
|
50
53
|
}
|
|
@@ -1474,6 +1477,108 @@ export class SqliteSessionMetadataStore {
|
|
|
1474
1477
|
return rows.map((row) => decodeMessageAdmissionRow(sessionId, row));
|
|
1475
1478
|
});
|
|
1476
1479
|
}
|
|
1480
|
+
async readActiveWorkHubAssignmentsByTarget(targetSessionIds, maxAssignmentsPerTarget) {
|
|
1481
|
+
this.assertOpen();
|
|
1482
|
+
for (const sessionId of targetSessionIds)
|
|
1483
|
+
assertSafeSessionId(sessionId);
|
|
1484
|
+
if (targetSessionIds.length > WORKHUB_TARGET_LINKAGE_MAX_SESSIONS) {
|
|
1485
|
+
throw new Error('Invalid WorkHub target Session count');
|
|
1486
|
+
}
|
|
1487
|
+
if (maxAssignmentsPerTarget !== undefined &&
|
|
1488
|
+
(!Number.isSafeInteger(maxAssignmentsPerTarget) ||
|
|
1489
|
+
maxAssignmentsPerTarget < 1 ||
|
|
1490
|
+
maxAssignmentsPerTarget > 256)) {
|
|
1491
|
+
throw new Error('Invalid WorkHub target Message limit');
|
|
1492
|
+
}
|
|
1493
|
+
const targets = [...new Set(targetSessionIds)];
|
|
1494
|
+
if (targets.length === 0)
|
|
1495
|
+
return [];
|
|
1496
|
+
return this.readTransaction(() => {
|
|
1497
|
+
const list = targets.map(() => '?').join(', ');
|
|
1498
|
+
// One Message moves between these lifecycle tables. Combine every target's
|
|
1499
|
+
// identities once, then resolve activity from the canonical Coordination
|
|
1500
|
+
// ledger in this same read transaction. That avoids rebuilding the target
|
|
1501
|
+
// set once per page or once per candidate, without introducing another
|
|
1502
|
+
// durable representation.
|
|
1503
|
+
const rows = this.db
|
|
1504
|
+
.prepare(`
|
|
1505
|
+
WITH target_messages(session_id, message_id) AS (
|
|
1506
|
+
SELECT session_id, message_id
|
|
1507
|
+
FROM message_admissions
|
|
1508
|
+
WHERE session_id IN (${list})
|
|
1509
|
+
AND message_id GLOB 'whm_*'
|
|
1510
|
+
AND length(message_id) = 52
|
|
1511
|
+
UNION
|
|
1512
|
+
SELECT session_id, message_id
|
|
1513
|
+
FROM session_messages
|
|
1514
|
+
WHERE session_id IN (${list})
|
|
1515
|
+
AND message_id GLOB 'whm_*'
|
|
1516
|
+
AND length(message_id) = 52
|
|
1517
|
+
UNION
|
|
1518
|
+
SELECT session_id, message_id
|
|
1519
|
+
FROM cancelled_message_admissions
|
|
1520
|
+
WHERE session_id IN (${list})
|
|
1521
|
+
AND message_id GLOB 'whm_*'
|
|
1522
|
+
AND length(message_id) = 52
|
|
1523
|
+
)
|
|
1524
|
+
SELECT target.session_id, target.message_id
|
|
1525
|
+
FROM target_messages AS target
|
|
1526
|
+
CROSS JOIN session_messages AS assignment INDEXED BY session_messages_by_identity
|
|
1527
|
+
WHERE assignment.session_id = ?
|
|
1528
|
+
AND assignment.message_id = 'wha_' || substr(target.message_id, 5)
|
|
1529
|
+
ORDER BY assignment.sequence DESC
|
|
1530
|
+
`)
|
|
1531
|
+
.iterate(...targets, ...targets, ...targets, WORKHUB_COORDINATION_SESSION_ID);
|
|
1532
|
+
const assignments = [];
|
|
1533
|
+
const acceptedPerTarget = new Map();
|
|
1534
|
+
for (const row of rows) {
|
|
1535
|
+
if (typeof row.message_id !== 'string' || typeof row.session_id !== 'string') {
|
|
1536
|
+
throw new SessionMetadataConflictError('Invalid WorkHub target Message identity');
|
|
1537
|
+
}
|
|
1538
|
+
const targetSessionId = row.session_id;
|
|
1539
|
+
if (maxAssignmentsPerTarget !== undefined &&
|
|
1540
|
+
(acceptedPerTarget.get(targetSessionId) ?? 0) >= maxAssignmentsPerTarget) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const assignment = this.readMessageByIdSync(WORKHUB_COORDINATION_SESSION_ID, `wha_${row.message_id.slice('whm_'.length)}`);
|
|
1544
|
+
if (assignment?.type !== 'workhub_coordination' ||
|
|
1545
|
+
assignment.kind !== 'delegation_assigned' ||
|
|
1546
|
+
assignment.targetSessionId !== targetSessionId ||
|
|
1547
|
+
assignment.targetMessageId !== row.message_id) {
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
const terminalSuffix = createHash('sha256')
|
|
1551
|
+
.update(assignment.delegationId, 'utf8')
|
|
1552
|
+
.digest('hex')
|
|
1553
|
+
.slice(0, 48);
|
|
1554
|
+
const supersession = this.readMessageByIdSync(WORKHUB_COORDINATION_SESSION_ID, `whx_${terminalSuffix}`);
|
|
1555
|
+
if (supersession?.type === 'workhub_coordination' &&
|
|
1556
|
+
supersession.kind === 'delegation_superseded') {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
const replacementAbort = this.readMessageByIdSync(WORKHUB_COORDINATION_SESSION_ID, `whb_${terminalSuffix}`);
|
|
1560
|
+
if (replacementAbort?.type === 'workhub_coordination' &&
|
|
1561
|
+
replacementAbort.kind === 'delegation_replacement_aborted') {
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const stopResolution = this.readMessageByIdSync(WORKHUB_COORDINATION_SESSION_ID, `whz_${terminalSuffix}`);
|
|
1565
|
+
if (stopResolution?.type === 'workhub_coordination' &&
|
|
1566
|
+
stopResolution.kind === 'delegation_stop_resolved' &&
|
|
1567
|
+
stopResolution.outcome !== 'not_owned') {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
assignments.push(assignment);
|
|
1571
|
+
acceptedPerTarget.set(targetSessionId, (acceptedPerTarget.get(targetSessionId) ?? 0) + 1);
|
|
1572
|
+
}
|
|
1573
|
+
return assignments;
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
async readMessageById(sessionId, messageId) {
|
|
1577
|
+
this.assertOpen();
|
|
1578
|
+
assertSafeSessionId(sessionId);
|
|
1579
|
+
assertSafeSessionId(messageId);
|
|
1580
|
+
return this.readTransaction(() => this.readMessageByIdSync(sessionId, messageId));
|
|
1581
|
+
}
|
|
1477
1582
|
async markMessagesHandedOff(input) {
|
|
1478
1583
|
this.assertOpen();
|
|
1479
1584
|
assertSafeSessionId(input.sessionId);
|