@zq-silk/yui 0.6.8 → 0.6.10

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.
Files changed (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -1,4 +1,5 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
+ const RECENT_DEDUPE_KEY_LIMIT = 256;
2
3
  function requireText(value, label) {
3
4
  const normalized = value.trim();
4
5
  if (normalized.length === 0) {
@@ -128,7 +129,15 @@ function mergeBatches(earlier, later) {
128
129
  refs: appendUnique(earlier.refs, later.refs, mailboxEntityRefKey),
129
130
  requestCount: earlier.requestCount + later.requestCount,
130
131
  firstQueuedAt: earlier.firstQueuedAt,
131
- lastQueuedAt: later.lastQueuedAt
132
+ lastQueuedAt: later.lastQueuedAt,
133
+ sources: appendUnique(earlier.sources, later.sources, (source) => source),
134
+ dedupeKeys: appendUnique(earlier.dedupeKeys, later.dedupeKeys, (key) => key),
135
+ deliveryModes: appendUnique(earlier.deliveryModes, later.deliveryModes, (mode) => mode),
136
+ ...((earlier.highestFactRevision ?? later.highestFactRevision) === undefined
137
+ ? {}
138
+ : {
139
+ highestFactRevision: Math.max(earlier.highestFactRevision ?? 0, later.highestFactRevision ?? 0)
140
+ })
132
141
  };
133
142
  return merged;
134
143
  }
@@ -144,32 +153,87 @@ function requireProcessing(mailbox, batchId) {
144
153
  }
145
154
  export function createWorkMailbox(target) {
146
155
  return {
147
- schemaVersion: 1,
156
+ schemaVersion: 2,
148
157
  target: copyTarget(target),
149
158
  nextSequence: 1,
150
159
  processing: null,
151
- pending: null
160
+ pending: {
161
+ normal: null,
162
+ userCorrection: null,
163
+ cursors: { normal: 0, userCorrection: 0 },
164
+ recentDedupeKeys: []
165
+ },
166
+ inputDelivery: null
152
167
  };
153
168
  }
154
169
  export function validateWorkMailbox(value) {
155
170
  const mailbox = record(value, "WorkMailbox");
156
- exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending"], "WorkMailbox");
157
- if (mailbox.schemaVersion !== 1)
158
- throw new Error("WorkMailbox must use schemaVersion 1");
171
+ exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending", "inputDelivery"], "WorkMailbox");
172
+ if (mailbox.schemaVersion !== 2)
173
+ throw new Error("WorkMailbox must use schemaVersion 2");
159
174
  const target = parseTarget(mailbox.target);
160
175
  const nextSequence = requireInteger(mailbox.nextSequence, 1, "WorkMailbox nextSequence");
161
176
  const processing = mailbox.processing === null ? null : parseProcessing(mailbox.processing);
162
- const pending = mailbox.pending === null ? null : parseBatch(mailbox.pending, "WorkMailbox pending");
163
- const batches = [processing?.batch, pending].filter((batch) => batch !== null && batch !== undefined);
177
+ const pending = parsePendingLanes(mailbox.pending);
178
+ for (const cursor of [pending.cursors.normal, pending.cursors.userCorrection]) {
179
+ if (cursor >= nextSequence) {
180
+ throw new Error("WorkMailbox lane cursor must be lower than nextSequence");
181
+ }
182
+ }
183
+ if (pending.normal !== null && pending.normal.fromSequence <= pending.cursors.normal) {
184
+ throw new Error("WorkMailbox normal pending batch is behind its cursor");
185
+ }
186
+ if (pending.userCorrection !== null
187
+ && pending.userCorrection.fromSequence <= pending.cursors.userCorrection) {
188
+ throw new Error("WorkMailbox user-correction pending batch is behind its cursor");
189
+ }
190
+ const inputDelivery = mailbox.inputDelivery === null
191
+ ? null
192
+ : parseInputDelivery(mailbox.inputDelivery);
193
+ const batches = [
194
+ processing?.batch,
195
+ pending.normal,
196
+ pending.userCorrection,
197
+ inputDelivery?.batch
198
+ ].filter((batch) => batch !== null && batch !== undefined);
164
199
  for (const batch of batches) {
165
200
  if (batch.toSequence >= nextSequence) {
166
201
  throw new Error("WorkMailbox batch sequence must be lower than nextSequence");
167
202
  }
168
203
  }
169
- if (processing !== null && pending !== null && processing.batch.toSequence >= pending.fromSequence) {
170
- throw new Error("WorkMailbox processing and pending sequences overlap");
204
+ const ranges = batches
205
+ .map((batch) => [batch.fromSequence, batch.toSequence])
206
+ .sort((left, right) => left[0] - right[0]);
207
+ for (let index = 1; index < ranges.length; index += 1) {
208
+ if (ranges[index - 1][1] >= ranges[index][0]) {
209
+ throw new Error("WorkMailbox batch sequences overlap");
210
+ }
211
+ }
212
+ if (inputDelivery !== null && target.kind !== "role" && target.kind !== "operator") {
213
+ throw new Error("WorkMailbox inputDelivery requires a Role or Operator target");
214
+ }
215
+ return { schemaVersion: 2, target, nextSequence, processing, pending, inputDelivery };
216
+ }
217
+ function parsePendingLanes(value) {
218
+ const lanes = record(value, "WorkMailbox pending");
219
+ exact(lanes, ["normal", "userCorrection", "cursors", "recentDedupeKeys"], "WorkMailbox pending");
220
+ const cursors = record(lanes.cursors, "WorkMailbox pending cursors");
221
+ exact(cursors, ["normal", "userCorrection"], "WorkMailbox pending cursors");
222
+ const recentDedupeKeys = requireStringArray(lanes.recentDedupeKeys, "WorkMailbox recent dedupe keys");
223
+ if (recentDedupeKeys.length > RECENT_DEDUPE_KEY_LIMIT) {
224
+ throw new Error("WorkMailbox recent dedupe keys exceed the bounded limit");
171
225
  }
172
- return { schemaVersion: 1, target, nextSequence, processing, pending };
226
+ return {
227
+ normal: lanes.normal === null ? null : parseBatch(lanes.normal, "WorkMailbox pending normal"),
228
+ userCorrection: lanes.userCorrection === null
229
+ ? null
230
+ : parseBatch(lanes.userCorrection, "WorkMailbox pending userCorrection"),
231
+ cursors: {
232
+ normal: requireInteger(cursors.normal, 0, "WorkMailbox normal cursor"),
233
+ userCorrection: requireInteger(cursors.userCorrection, 0, "WorkMailbox user-correction cursor")
234
+ },
235
+ recentDedupeKeys
236
+ };
173
237
  }
174
238
  function parseTarget(value) {
175
239
  const target = record(value, "WorkMailbox target");
@@ -206,7 +270,9 @@ function parseTarget(value) {
206
270
  }
207
271
  function parseRef(value, label) {
208
272
  const ref = record(value, label);
209
- const types = ["task", "run", "work-item", "input", "session", "message"];
273
+ const types = [
274
+ "task", "run", "work-item", "input", "session", "message", "event"
275
+ ];
210
276
  if (!types.includes(ref.type))
211
277
  throw new Error(`${label} type is invalid`);
212
278
  if (ref.type === "task" || ref.type === "session") {
@@ -222,8 +288,19 @@ function parseRef(value, label) {
222
288
  }
223
289
  function parseBatch(value, label) {
224
290
  const batch = record(value, label);
225
- const required = ["fromSequence", "toSequence", "reasons", "refs", "requestCount", "firstQueuedAt", "lastQueuedAt"];
226
- exact(batch, required, label);
291
+ const required = [
292
+ "fromSequence",
293
+ "toSequence",
294
+ "reasons",
295
+ "refs",
296
+ "requestCount",
297
+ "firstQueuedAt",
298
+ "lastQueuedAt",
299
+ "sources",
300
+ "dedupeKeys",
301
+ "deliveryModes"
302
+ ];
303
+ exact(batch, batch.highestFactRevision === undefined ? required : [...required, "highestFactRevision"], label);
227
304
  const fromSequence = requireInteger(batch.fromSequence, 1, `${label} fromSequence`);
228
305
  const toSequence = requireInteger(batch.toSequence, fromSequence, `${label} toSequence`);
229
306
  const requestCount = requireInteger(batch.requestCount, 1, `${label} requestCount`);
@@ -246,16 +323,34 @@ function parseBatch(value, label) {
246
323
  refs,
247
324
  requestCount,
248
325
  firstQueuedAt: requireTimestamp(batch.firstQueuedAt, `${label} firstQueuedAt`),
249
- lastQueuedAt: requireTimestamp(batch.lastQueuedAt, `${label} lastQueuedAt`)
326
+ lastQueuedAt: requireTimestamp(batch.lastQueuedAt, `${label} lastQueuedAt`),
327
+ sources: requireStringArray(batch.sources, `${label} sources`),
328
+ dedupeKeys: requireStringArray(batch.dedupeKeys, `${label} dedupeKeys`),
329
+ deliveryModes: parseDeliveryModes(batch.deliveryModes, `${label} deliveryModes`),
330
+ ...(batch.highestFactRevision === undefined
331
+ ? {}
332
+ : {
333
+ highestFactRevision: requireInteger(batch.highestFactRevision, 0, `${label} highestFactRevision`)
334
+ })
250
335
  };
251
336
  return result;
252
337
  }
338
+ function parseDeliveryModes(value, label) {
339
+ const modes = requireStringArray(value, label);
340
+ for (const mode of modes) {
341
+ if (mode !== "followup" && mode !== "steer-if-safe" && mode !== "inject") {
342
+ throw new Error(`${label} contains an invalid delivery mode`);
343
+ }
344
+ }
345
+ return modes;
346
+ }
253
347
  function parseProcessing(value) {
254
348
  const processing = record(value, "WorkMailbox processing");
255
- const required = ["batchId", "batch", "owner", "startedAt"];
349
+ const required = ["batchId", "lane", "batch", "owner", "startedAt"];
256
350
  exact(processing, processing.executionRef === undefined ? required : [...required, "executionRef"], "WorkMailbox processing");
257
351
  const result = {
258
352
  batchId: requireString(processing.batchId, "WorkMailbox processing batchId"),
353
+ lane: parseLane(processing.lane, "WorkMailbox processing lane"),
259
354
  batch: parseBatch(processing.batch, "WorkMailbox processing batch"),
260
355
  owner: requireString(processing.owner, "WorkMailbox processing owner"),
261
356
  startedAt: requireTimestamp(processing.startedAt, "WorkMailbox processing startedAt")
@@ -264,11 +359,97 @@ function parseProcessing(value) {
264
359
  ? result
265
360
  : { ...result, executionRef: parseRef(processing.executionRef, "WorkMailbox processing executionRef") };
266
361
  }
362
+ function parseInputDelivery(value) {
363
+ const input = record(value, "WorkMailbox inputDelivery");
364
+ const required = [
365
+ "attemptId",
366
+ "lane",
367
+ "mode",
368
+ "batch",
369
+ "owner",
370
+ "status",
371
+ "startedAt",
372
+ "executionRef"
373
+ ];
374
+ const optional = [
375
+ ...(input.pushedAt === undefined ? [] : ["pushedAt"]),
376
+ ...(input.unknownReason === undefined ? [] : ["unknownReason"]),
377
+ ...(input.providerFence === undefined ? [] : ["providerFence"])
378
+ ];
379
+ exact(input, [...required, ...optional], "WorkMailbox inputDelivery");
380
+ const lane = parseLane(input.lane, "WorkMailbox inputDelivery lane");
381
+ const mode = parseDeliveryMode(input.mode, "WorkMailbox inputDelivery mode");
382
+ if (input.status !== "dispatching" && input.status !== "delivery-unknown") {
383
+ throw new Error("WorkMailbox inputDelivery status is invalid");
384
+ }
385
+ const status = input.status;
386
+ if ((status === "delivery-unknown") !== (input.unknownReason !== undefined)) {
387
+ throw new Error("WorkMailbox delivery-unknown requires exactly one unknownReason");
388
+ }
389
+ const result = {
390
+ attemptId: requireString(input.attemptId, "WorkMailbox inputDelivery attemptId"),
391
+ lane,
392
+ mode,
393
+ batch: parseBatch(input.batch, "WorkMailbox inputDelivery batch"),
394
+ owner: requireString(input.owner, "WorkMailbox inputDelivery owner"),
395
+ status,
396
+ startedAt: requireTimestamp(input.startedAt, "WorkMailbox inputDelivery startedAt"),
397
+ executionRef: parseRef(input.executionRef, "WorkMailbox inputDelivery executionRef"),
398
+ ...(input.pushedAt === undefined
399
+ ? {}
400
+ : { pushedAt: requireTimestamp(input.pushedAt, "WorkMailbox inputDelivery pushedAt") }),
401
+ ...(input.unknownReason === undefined
402
+ ? {}
403
+ : { unknownReason: requireString(input.unknownReason, "WorkMailbox inputDelivery unknownReason") }),
404
+ ...(input.providerFence === undefined
405
+ ? {}
406
+ : { providerFence: parseProviderFence(input.providerFence) })
407
+ };
408
+ if (result.pushedAt !== undefined
409
+ && Date.parse(result.pushedAt) < Date.parse(result.startedAt)) {
410
+ throw new Error("WorkMailbox inputDelivery pushedAt is earlier than startedAt");
411
+ }
412
+ return result;
413
+ }
414
+ function parseProviderFence(value) {
415
+ const fence = record(value, "WorkMailbox inputDelivery providerFence");
416
+ exact(fence, fence.nativeTurnId === undefined
417
+ ? ["conversationId", "activationId"]
418
+ : ["conversationId", "activationId", "nativeTurnId"], "WorkMailbox inputDelivery providerFence");
419
+ return {
420
+ conversationId: requireString(fence.conversationId, "Provider Conversation id"),
421
+ activationId: requireString(fence.activationId, "Provider Activation id"),
422
+ ...(fence.nativeTurnId === undefined
423
+ ? {}
424
+ : { nativeTurnId: requireString(fence.nativeTurnId, "Provider Turn id") })
425
+ };
426
+ }
427
+ function parseLane(value, label) {
428
+ if (value !== "normal" && value !== "user-correction") {
429
+ throw new Error(`${label} is invalid`);
430
+ }
431
+ return value;
432
+ }
433
+ function parseDeliveryMode(value, label) {
434
+ if (value !== "followup" && value !== "steer-if-safe" && value !== "inject") {
435
+ throw new Error(`${label} is invalid`);
436
+ }
437
+ return value;
438
+ }
267
439
  export function enqueueSignal(mailbox, signal) {
268
440
  const reason = requireText(signal.reason, "signal reason");
269
441
  const occurredAt = requireText(signal.occurredAt, "signal occurredAt");
442
+ const lane = signal.lane ?? "normal";
443
+ const laneKey = lane === "normal" ? "normal" : "userCorrection";
444
+ const deliveryMode = signal.deliveryMode ?? "followup";
445
+ const source = requireText(signal.source ?? "yui", "signal source");
270
446
  const refs = signal.refs.map(copyRef);
271
447
  const sequence = mailbox.nextSequence;
448
+ const dedupeKey = signal.dedupeKey === undefined
449
+ ? `sequence:${sequence}`
450
+ : requireText(signal.dedupeKey, "signal dedupeKey");
451
+ if (mailboxContainsDedupeKey(mailbox, dedupeKey))
452
+ return mailbox;
272
453
  const incoming = {
273
454
  fromSequence: sequence,
274
455
  toSequence: sequence,
@@ -276,38 +457,98 @@ export function enqueueSignal(mailbox, signal) {
276
457
  refs: appendUnique([], refs, mailboxEntityRefKey),
277
458
  requestCount: 1,
278
459
  firstQueuedAt: occurredAt,
279
- lastQueuedAt: occurredAt
460
+ lastQueuedAt: occurredAt,
461
+ sources: [source],
462
+ dedupeKeys: [dedupeKey],
463
+ deliveryModes: [deliveryMode],
464
+ ...(signal.factRevision === undefined
465
+ ? {}
466
+ : { highestFactRevision: requireInteger(signal.factRevision, 0, "signal factRevision") })
280
467
  };
281
468
  return {
282
469
  ...mailbox,
283
470
  nextSequence: sequence + 1,
284
- pending: mailbox.pending === null
285
- ? incoming
286
- : mergeBatches(mailbox.pending, incoming)
471
+ pending: {
472
+ ...mailbox.pending,
473
+ [laneKey]: mailbox.pending[laneKey] === null
474
+ ? incoming
475
+ : mergeBatches(mailbox.pending[laneKey], incoming)
476
+ }
287
477
  };
288
478
  }
479
+ function mailboxContainsDedupeKey(mailbox, key) {
480
+ if (mailbox.pending.recentDedupeKeys.includes(key))
481
+ return true;
482
+ return [
483
+ mailbox.pending.normal,
484
+ mailbox.pending.userCorrection,
485
+ mailbox.processing?.batch,
486
+ mailbox.inputDelivery?.batch
487
+ ].some((batch) => batch?.dedupeKeys.includes(key) === true);
488
+ }
489
+ export function pendingLane(mailbox, lane) {
490
+ return lane === "normal" ? mailbox.pending.normal : mailbox.pending.userCorrection;
491
+ }
492
+ export function mailboxHasPending(mailbox) {
493
+ return mailbox.pending.normal !== null || mailbox.pending.userCorrection !== null;
494
+ }
495
+ export function nextPendingLane(mailbox) {
496
+ if (mailbox.pending.userCorrection !== null)
497
+ return "user-correction";
498
+ return mailbox.pending.normal === null ? null : "normal";
499
+ }
500
+ export function nextPendingBatch(mailbox) {
501
+ const lane = nextPendingLane(mailbox);
502
+ return lane === null ? null : pendingLane(mailbox, lane);
503
+ }
504
+ export function mailboxHasWork(mailbox) {
505
+ return mailbox.processing !== null
506
+ || mailbox.inputDelivery !== null
507
+ || mailboxHasPending(mailbox);
508
+ }
509
+ export function mailboxBatches(mailbox) {
510
+ return [
511
+ mailbox.processing?.batch,
512
+ mailbox.inputDelivery?.batch,
513
+ mailbox.pending.userCorrection,
514
+ mailbox.pending.normal
515
+ ].filter((batch) => batch !== null && batch !== undefined);
516
+ }
289
517
  function mailboxTaskRecordKind(type) {
290
518
  switch (type) {
291
519
  case "run": return "agentRun";
292
520
  case "work-item": return "workItem";
293
521
  case "input": return "inputRequest";
294
522
  case "message": return "message";
523
+ case "event": return "event";
295
524
  }
296
525
  }
297
526
  export function claimPending(mailbox, options) {
298
527
  if (mailbox.processing !== null) {
299
528
  throw new Error("Mailbox is already processing a batch");
300
529
  }
301
- if (mailbox.pending === null) {
530
+ const lane = options.lane ?? nextPendingLane(mailbox);
531
+ if (lane === null) {
302
532
  throw new Error("Mailbox has no pending work to claim");
303
533
  }
534
+ const batch = pendingLane(mailbox, lane);
535
+ if (batch === null)
536
+ throw new Error(`Mailbox has no pending ${lane} work to claim`);
304
537
  const processing = {
305
538
  batchId: requireText(options.batchId, "batch id"),
306
- batch: mailbox.pending,
539
+ lane,
540
+ batch,
307
541
  owner: requireText(options.owner, "claim owner"),
308
542
  startedAt: requireText(options.startedAt, "claim startedAt")
309
543
  };
310
- return { ...mailbox, processing, pending: null };
544
+ return {
545
+ ...mailbox,
546
+ processing,
547
+ pending: {
548
+ ...mailbox.pending,
549
+ [lane === "normal" ? "normal" : "userCorrection"]: null
550
+ }
551
+ };
311
552
  }
312
553
  export function bindExecution(mailbox, batchId, executionRef) {
313
554
  const processing = requireProcessing(mailbox, batchId);
@@ -317,13 +558,152 @@ export function bindExecution(mailbox, batchId, executionRef) {
317
558
  };
318
559
  }
319
560
  export function completeProcessing(mailbox, batchId) {
320
- requireProcessing(mailbox, batchId);
321
- return { ...mailbox, processing: null };
561
+ const processing = requireProcessing(mailbox, batchId);
562
+ const lane = processing.lane;
563
+ return validateWorkMailbox({
564
+ ...mailbox,
565
+ processing: null,
566
+ pending: advanceLaneCursor(mailbox.pending, lane, processing.batch)
567
+ });
322
568
  }
323
569
  export function releaseProcessing(mailbox, batchId) {
324
570
  const processing = requireProcessing(mailbox, batchId);
325
- const released = mailbox.pending === null
571
+ const lane = processing.lane;
572
+ const key = lane === "normal" ? "normal" : "userCorrection";
573
+ const released = mailbox.pending[key] === null
326
574
  ? processing.batch
327
- : mergeBatches(processing.batch, mailbox.pending);
328
- return { ...mailbox, processing: null, pending: released };
575
+ : mergeBatches(processing.batch, mailbox.pending[key]);
576
+ return {
577
+ ...mailbox,
578
+ processing: null,
579
+ pending: { ...mailbox.pending, [key]: released }
580
+ };
581
+ }
582
+ function requireInputDelivery(mailbox, attemptId) {
583
+ const id = requireText(attemptId, "input delivery attempt id");
584
+ if (mailbox.inputDelivery === null)
585
+ throw new Error("Mailbox has no input delivery");
586
+ if (mailbox.inputDelivery.attemptId !== id) {
587
+ throw new Error(`Mailbox input delivery does not match ${id}`);
588
+ }
589
+ return mailbox.inputDelivery;
590
+ }
591
+ export function claimInputDelivery(mailbox, options) {
592
+ if (mailbox.inputDelivery !== null) {
593
+ if (mailbox.inputDelivery.attemptId === options.attemptId)
594
+ return mailbox;
595
+ throw new Error("Mailbox already has an input delivery");
596
+ }
597
+ const batch = pendingLane(mailbox, options.lane);
598
+ if (batch === null)
599
+ throw new Error(`Mailbox has no pending ${options.lane} input`);
600
+ const inputDelivery = {
601
+ attemptId: requireText(options.attemptId, "input delivery attempt id"),
602
+ lane: options.lane,
603
+ mode: options.mode,
604
+ batch,
605
+ owner: requireText(options.owner, "input delivery owner"),
606
+ status: "dispatching",
607
+ startedAt: requireTimestamp(options.startedAt, "input delivery startedAt"),
608
+ executionRef: copyRef(options.executionRef),
609
+ ...(options.providerFence === undefined
610
+ ? {}
611
+ : { providerFence: parseProviderFence(options.providerFence) })
612
+ };
613
+ return validateWorkMailbox({
614
+ ...mailbox,
615
+ pending: {
616
+ ...mailbox.pending,
617
+ [options.lane === "normal" ? "normal" : "userCorrection"]: null
618
+ },
619
+ inputDelivery
620
+ });
621
+ }
622
+ export function markInputDeliveryPushed(mailbox, attemptId, pushedAt) {
623
+ const delivery = requireInputDelivery(mailbox, attemptId);
624
+ if (delivery.pushedAt !== undefined)
625
+ return mailbox;
626
+ const timestamp = requireTimestamp(pushedAt.toISOString(), "input delivery pushedAt");
627
+ return validateWorkMailbox({
628
+ ...mailbox,
629
+ inputDelivery: { ...delivery, pushedAt: timestamp }
630
+ });
631
+ }
632
+ export function markInputDeliveryUnknown(mailbox, attemptId, reason, observedAt) {
633
+ const delivery = requireInputDelivery(mailbox, attemptId);
634
+ const timestamp = requireTimestamp(observedAt.toISOString(), "delivery unknown observedAt");
635
+ return validateWorkMailbox({
636
+ ...mailbox,
637
+ inputDelivery: {
638
+ ...delivery,
639
+ status: "delivery-unknown",
640
+ unknownReason: requireText(reason, "delivery unknown reason"),
641
+ pushedAt: delivery.pushedAt ?? timestamp
642
+ }
643
+ });
644
+ }
645
+ export function completeInputDelivery(mailbox, attemptId, _acceptedAt) {
646
+ const delivery = requireInputDelivery(mailbox, attemptId);
647
+ return validateWorkMailbox({
648
+ ...mailbox,
649
+ inputDelivery: null,
650
+ pending: advanceLaneCursor(mailbox.pending, delivery.lane, delivery.batch)
651
+ });
652
+ }
653
+ export function consumePendingBatch(mailbox, lane) {
654
+ const key = lane === "normal" ? "normal" : "userCorrection";
655
+ const batch = mailbox.pending[key];
656
+ if (batch === null)
657
+ throw new Error(`Mailbox has no pending ${lane} batch`);
658
+ return validateWorkMailbox({
659
+ ...mailbox,
660
+ pending: {
661
+ ...advanceLaneCursor(mailbox.pending, lane, batch),
662
+ [key]: null
663
+ }
664
+ });
665
+ }
666
+ function advanceLaneCursor(pending, lane, batch) {
667
+ const key = lane === "normal" ? "normal" : "userCorrection";
668
+ const recentDedupeKeys = appendUnique(pending.recentDedupeKeys, batch.dedupeKeys, (value) => value).slice(-RECENT_DEDUPE_KEY_LIMIT);
669
+ return {
670
+ ...pending,
671
+ cursors: {
672
+ ...pending.cursors,
673
+ [key]: Math.max(pending.cursors[key], batch.toSequence)
674
+ },
675
+ recentDedupeKeys
676
+ };
677
+ }
678
+ export function releaseInputDelivery(mailbox, attemptId) {
679
+ const delivery = requireInputDelivery(mailbox, attemptId);
680
+ if (delivery.pushedAt !== undefined || delivery.status === "delivery-unknown") {
681
+ throw new Error("A pushed or delivery-unknown input cannot return to pending");
682
+ }
683
+ const key = delivery.lane === "normal" ? "normal" : "userCorrection";
684
+ return validateWorkMailbox({
685
+ ...mailbox,
686
+ pending: {
687
+ ...mailbox.pending,
688
+ [key]: mailbox.pending[key] === null
689
+ ? delivery.batch
690
+ : mergeBatches(delivery.batch, mailbox.pending[key])
691
+ },
692
+ inputDelivery: null
693
+ });
694
+ }
695
+ /** Exact Provider readback proved that a previously uncertain mutation is absent. */
696
+ export function resolveInputDeliveryNotAccepted(mailbox, attemptId) {
697
+ const delivery = requireInputDelivery(mailbox, attemptId);
698
+ const key = delivery.lane === "normal" ? "normal" : "userCorrection";
699
+ return validateWorkMailbox({
700
+ ...mailbox,
701
+ pending: {
702
+ ...mailbox.pending,
703
+ [key]: mailbox.pending[key] === null
704
+ ? delivery.batch
705
+ : mergeBatches(delivery.batch, mailbox.pending[key])
706
+ },
707
+ inputDelivery: null
708
+ });
329
709
  }
@@ -1,11 +1,12 @@
1
- import { completeProcessing, createWorkMailbox, enqueueSignal, mailboxEntityRefKey } from "./workMailbox.js";
1
+ import { completeProcessing, consumePendingBatch, createWorkMailbox, enqueueSignal, mailboxBatches, mailboxEntityRefKey } from "./workMailbox.js";
2
2
  /** Atomically useful when called inside the caller's TaskStore transaction. */
3
- export function enqueueWork(store, target, reason, occurredAt, refs = []) {
3
+ export function enqueueWork(store, target, reason, occurredAt, refs = [], metadata = {}) {
4
4
  const mailbox = store.getWorkMailbox(target) ?? createWorkMailbox(target);
5
5
  const queued = enqueueSignal(mailbox, {
6
6
  reason,
7
7
  refs,
8
- occurredAt: timestamp(occurredAt)
8
+ occurredAt: timestamp(occurredAt),
9
+ ...metadata
9
10
  });
10
11
  store.saveWorkMailbox(queued);
11
12
  return queued;
@@ -59,17 +60,18 @@ export function settleExactWorkExecution(store, target, executionRef) {
59
60
  const mailbox = store.getWorkMailbox(target);
60
61
  if (mailbox === null)
61
62
  return "absent";
62
- const pending = mailbox.pending;
63
- if (pending === null)
63
+ const matching = mailboxBatches(mailbox).filter((batch) => batch.refs.some((ref) => mailboxEntityRefKey(ref) === mailboxEntityRefKey(executionRef)));
64
+ if (matching.length === 0)
64
65
  return "absent";
65
- const matches = pending.refs.some((ref) => mailboxEntityRefKey(ref) === mailboxEntityRefKey(executionRef));
66
- if (!matches)
67
- return "absent";
68
- if (pending.requestCount !== 1) {
66
+ if (matching.length !== 1 || matching[0].requestCount !== 1) {
69
67
  throw new Error(`Cannot settle a merged pending mailbox batch for ${targetLabel(target)}: `
70
68
  + `${executionRef.type}/${executionRef.id}.`);
71
69
  }
72
- store.saveWorkMailbox({ ...mailbox, pending: null });
70
+ const pending = matching[0];
71
+ const pendingKey = mailbox.pending.normal === pending ? "normal" : "userCorrection";
72
+ if (mailbox.pending[pendingKey] !== pending)
73
+ return "absent";
74
+ store.saveWorkMailbox(consumePendingBatch(mailbox, pendingKey === "normal" ? "normal" : "user-correction"));
73
75
  return "pending";
74
76
  }
75
77
  function timestamp(value) {