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.
@@ -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 = 116;
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 = requireExactRecord(value, 'WorkHub Coordination 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 = await messages.readMessageExecutionDisposition(assignment.targetSessionId, assignment.targetMessageId);
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 active = await this.#effects.listActiveAssignments();
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
- listActiveAssignments: () => this.#listActiveAssignments(),
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, messages] = await Promise.all([
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.readMessagesSnapshot(WORKHUB_COORDINATION_SESSION_ID),
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 targetActive = activeAssignments.filter((assignment) => assignment.targetSessionId === input.targetSessionId);
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 targetActive) {
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
- return { ok: true, result: await this.#actionGate.candidates() };
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
- const throughSequence = await this.metadata.readTranscriptHighWater(WORKHUB_COORDINATION_SESSION_ID);
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
- if (columns.some(({ name }) => name === 'status' || name === 'storage_key')) {
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 (columns.some(({ name }) => name === 'status') && row.status !== 'live')
28
- continue;
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 decodeArtifactRecordJsons(retained)) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maka-agent",
3
- "version": "0.2.0-dev.21.20260905",
3
+ "version": "0.2.0-dev.22.20260905",
4
4
  "description": "Apache Maka (Incubating) developer snapshot; not an Apache release.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",