stitchkit 0.100.0 → 0.100.2

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 (79) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/dist/agent-runtime/invocation-attempt.d.ts +6 -0
  3. package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -1
  4. package/dist/agent-runtime/invocation-payload.d.ts +2 -0
  5. package/dist/agent-runtime/invocation-payload.d.ts.map +1 -1
  6. package/dist/agent-runtime/invocation-read.d.ts.map +1 -1
  7. package/dist/agent-runtime/invocations.d.ts +6 -0
  8. package/dist/agent-runtime/invocations.d.ts.map +1 -1
  9. package/dist/agent-runtime/projections.d.ts.map +1 -1
  10. package/dist/agent-runtime/schedule-dispatch.d.ts +9 -2
  11. package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -1
  12. package/dist/agent-runtime/schedule-records.d.ts +8 -2
  13. package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
  14. package/dist/agent-runtime/schedules.d.ts +5 -0
  15. package/dist/agent-runtime/schedules.d.ts.map +1 -1
  16. package/dist/agent-runtime/sqlite-events.d.ts.map +1 -1
  17. package/dist/agent-runtime/store-create.d.ts.map +1 -1
  18. package/dist/agent-runtime/store-driver-contract.d.ts +9 -0
  19. package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
  20. package/dist/agent-runtime/store-event-once.d.ts +9 -1
  21. package/dist/agent-runtime/store-event-once.d.ts.map +1 -1
  22. package/dist/agent-runtime/store-memory.d.ts.map +1 -1
  23. package/dist/agent-runtime/store.d.ts +6 -0
  24. package/dist/agent-runtime/store.d.ts.map +1 -1
  25. package/dist/application/change-subscription.d.ts.map +1 -1
  26. package/dist/application/notification-delivery.d.ts +24 -1
  27. package/dist/application/notification-delivery.d.ts.map +1 -1
  28. package/dist/application/notification-outbox-state.d.ts +4 -0
  29. package/dist/application/notification-outbox-state.d.ts.map +1 -1
  30. package/dist/application/notification-outbox.d.ts.map +1 -1
  31. package/dist/application/process-lifecycle-transitions.d.ts +13 -2
  32. package/dist/application/process-lifecycle-transitions.d.ts.map +1 -1
  33. package/dist/application/process-lifecycle.d.ts +2 -1
  34. package/dist/application/process-lifecycle.d.ts.map +1 -1
  35. package/dist/application/watch-hub-core.d.ts +14 -79
  36. package/dist/application/watch-hub-core.d.ts.map +1 -1
  37. package/dist/application/watch-hub-source.d.ts +98 -0
  38. package/dist/application/watch-hub-source.d.ts.map +1 -0
  39. package/dist/browser/session-credentials.d.ts +6 -0
  40. package/dist/browser/session-credentials.d.ts.map +1 -1
  41. package/dist/browser/session.d.ts +3 -1
  42. package/dist/browser/session.d.ts.map +1 -1
  43. package/dist/entrypoints/agent-runtime/harness.js +1 -1
  44. package/dist/entrypoints/agent-runtime/sqlite/bun.js +2 -2
  45. package/dist/entrypoints/agent-runtime/sqlite/node.js +2 -2
  46. package/dist/entrypoints/agent-runtime/testing.js +2 -2
  47. package/dist/entrypoints/agent-runtime.js +53 -19
  48. package/dist/entrypoints/application/diagnostic-journal.js +1 -1
  49. package/dist/entrypoints/application.d.ts +2 -2
  50. package/dist/entrypoints/application.d.ts.map +1 -1
  51. package/dist/entrypoints/application.js +285 -193
  52. package/dist/entrypoints/files.js +8 -2
  53. package/dist/entrypoints/index.js +33 -6
  54. package/dist/entrypoints/live.js +16 -12
  55. package/dist/entrypoints/node.js +1 -1
  56. package/dist/entrypoints/server.js +4 -4
  57. package/dist/{index-s84he4v7.js → index-10x2hbx0.js} +4 -1
  58. package/dist/{index-js7gqrb1.js → index-c6bxbsnc.js} +5 -1
  59. package/dist/{index-y9qn426m.js → index-eaysmdtq.js} +10 -1
  60. package/dist/{index-xwhjjw2x.js → index-f4fx2tza.js} +119 -49
  61. package/dist/{index-p3vsv1fb.js → index-hadmp193.js} +14 -5
  62. package/dist/{index-0b1cnze2.js → index-vqr7f7v8.js} +31 -15
  63. package/dist/internal/coalesced-task.d.ts +2 -0
  64. package/dist/internal/coalesced-task.d.ts.map +1 -1
  65. package/dist/internal/process-instance.d.ts.map +1 -1
  66. package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
  67. package/dist/server/realtime.d.ts.map +1 -1
  68. package/llms/agent-runtime.2.txt +13 -5
  69. package/llms/agent-runtime.3.txt +9 -4
  70. package/llms/agent-runtime.txt +8 -3
  71. package/llms/application.2.txt +47 -6
  72. package/llms/application.txt +18 -27
  73. package/llms/live.txt +2 -1
  74. package/llms/server.3.txt +22 -0
  75. package/llms/stitchkit.3.txt +9 -5
  76. package/llms/upgrading-0.74.0-0.100.0.txt +15 -3
  77. package/llms-full.txt +118 -29
  78. package/llms.txt +5 -5
  79. package/package.json +1 -1
@@ -58,9 +58,6 @@ import {
58
58
  defineManagedResource,
59
59
  managedResourceDependencyId
60
60
  } from "../index-2k4yrqkc.js";
61
- import {
62
- withDeadline
63
- } from "../index-b9aefrqd.js";
64
61
  import"../index-3z73fh2c.js";
65
62
  import {
66
63
  createBoundedSinkManager
@@ -69,6 +66,9 @@ import {
69
66
  backoffDelay,
70
67
  createBackoff
71
68
  } from "../index-ngfhvfpd.js";
69
+ import {
70
+ withDeadline
71
+ } from "../index-b9aefrqd.js";
72
72
  import {
73
73
  deltaWins,
74
74
  diff,
@@ -90,7 +90,9 @@ import {
90
90
  import {
91
91
  argumentsDigest
92
92
  } from "../index-0b1ess5e.js";
93
- import"../index-2vtcmrj9.js";
93
+ import {
94
+ AppError
95
+ } from "../index-2vtcmrj9.js";
94
96
  import {
95
97
  serializeCanonicalJson
96
98
  } from "../index-k5hsfc3j.js";
@@ -528,6 +530,9 @@ function createCoalescedTask(config) {
528
530
  get running() {
529
531
  return running;
530
532
  },
533
+ get pending() {
534
+ return dirty;
535
+ },
531
536
  trigger() {
532
537
  if (closed)
533
538
  return;
@@ -651,7 +656,7 @@ class ChangeSubscription {
651
656
  this.connection = undefined;
652
657
  this.attempt?.abort();
653
658
  this.report(error);
654
- this.release(handle).then(() => this.schedule(), (cause) => this.report(cause));
659
+ this.release(handle).catch((cause) => this.report(cause)).then(() => this.schedule());
655
660
  }
656
661
  open() {
657
662
  if (this.opening)
@@ -722,7 +727,6 @@ class ChangeSubscription {
722
727
  await this.release(handle);
723
728
  } catch (cause) {
724
729
  this.report(cause);
725
- return;
726
730
  }
727
731
  }
728
732
  if (!lifetime.signal.aborted) {
@@ -1110,13 +1114,97 @@ function sqliteKeyspaceBackend(declaration, config) {
1110
1114
  };
1111
1115
  }
1112
1116
  // src/application/notification-delivery.ts
1117
+ import { z as z7 } from "zod";
1118
+
1119
+ // src/application/notification-outbox-state.ts
1113
1120
  import { z as z6 } from "zod";
1114
- var NotificationDeliveryStateSchema = z6.object({
1115
- version: z6.string().min(1).max(128),
1116
- actions: z6.array(z6.string().min(1).max(128)).min(1).max(128),
1117
- receipts: z6.array(z6.object({ action: z6.string(), receipt: z6.json() }).strict()).max(128),
1118
- projectedAt: z6.string().datetime({ offset: true }).optional()
1121
+ var sizeOf = (value) => measureSize(value).responseBytes;
1122
+ var DEFAULT_OUTBOX_BACKOFF = {
1123
+ minDelayMs: 1000,
1124
+ maxDelayMs: 60000,
1125
+ jitter: 0
1126
+ };
1127
+ function resolveOutboxLimits(config) {
1128
+ const pollIntervalMs = z6.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1129
+ const leaseMs = z6.number().int().min(100).parse(config.leaseMs ?? 30000);
1130
+ const maxAttempts = z6.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1131
+ const maxQueue = z6.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1132
+ const maxStateBytes = z6.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1133
+ const retainReceipts = z6.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1134
+ return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
1135
+ }
1136
+
1137
+ class OutboxStateLimitError extends Error {
1138
+ constructor(maxStateBytes) {
1139
+ super(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
1140
+ this.name = "OutboxStateLimitError";
1141
+ }
1142
+ }
1143
+ function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
1144
+ if (parsed.queue.length > maxQueue) {
1145
+ throw new Error(`[stitchkit] notification outbox queue limit (${maxQueue}) exceeded`);
1146
+ }
1147
+ let receipts = parsed.receipts.slice(0, retainReceipts);
1148
+ let next = { ...parsed, receipts };
1149
+ while (sizeOf(next) > maxStateBytes && receipts.length > 0) {
1150
+ receipts = receipts.slice(0, -1);
1151
+ next = { ...parsed, receipts };
1152
+ }
1153
+ if (sizeOf(next) > maxStateBytes)
1154
+ throw new OutboxStateLimitError(maxStateBytes);
1155
+ return next;
1156
+ }
1157
+ function dropReason(classification, attempts, maxAttempts) {
1158
+ if (classification.recipientUnreachable === true)
1159
+ return "recipient-unreachable";
1160
+ if (!classification.retryable)
1161
+ return "terminal";
1162
+ return attempts >= maxAttempts ? "attempt-limit" : null;
1163
+ }
1164
+ var RetryDelaySchema = z6.number().nonnegative().max(24 * 60 * 60 * 1000);
1165
+ var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
1166
+ function claimDue(queue, now, ownerId, leaseMs) {
1167
+ const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
1168
+ if (!candidate)
1169
+ return null;
1170
+ return {
1171
+ ...candidate,
1172
+ attempts: candidate.attempts + 1,
1173
+ leaseOwner: ownerId,
1174
+ leaseId: crypto.randomUUID(),
1175
+ leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
1176
+ };
1177
+ }
1178
+
1179
+ // src/application/notification-delivery.ts
1180
+ var NotificationDeliveryStateSchema = z7.object({
1181
+ version: z7.string().min(1).max(128),
1182
+ actions: z7.array(z7.string().min(1).max(128)).min(1).max(128),
1183
+ receipts: z7.array(z7.object({
1184
+ action: z7.string(),
1185
+ receipt: z7.json(),
1186
+ unrecorded: z7.enum(["not-json", "too-large"]).optional()
1187
+ }).strict()).max(128),
1188
+ projectedAt: z7.string().datetime({ offset: true }).optional()
1119
1189
  }).strict().refine((value) => new Set(value.actions).size === value.actions.length && new Set(value.receipts.map((receipt) => receipt.action)).size === value.receipts.length && value.receipts.every((receipt) => value.actions.includes(receipt.action)), "Invalid action plan or receipts");
1190
+
1191
+ class NotificationPlanVersionError extends Error {
1192
+ version;
1193
+ constructor(version) {
1194
+ super(`Notification action plan version "${version}" has no executor`);
1195
+ this.name = "NotificationPlanVersionError";
1196
+ this.version = version;
1197
+ }
1198
+ }
1199
+ function projectionFor(config, version) {
1200
+ if (version === config.version)
1201
+ return config.project;
1202
+ return config.retiredVersions && Object.hasOwn(config.retiredVersions, version) ? config.retiredVersions[version] : undefined;
1203
+ }
1204
+ function recordable(result) {
1205
+ const parsed = z7.json().safeParse(result ?? null);
1206
+ return parsed.success ? { receipt: parsed.data } : { receipt: null, unrecorded: "not-json" };
1207
+ }
1120
1208
  function notificationDeliveryPlan(config, payload) {
1121
1209
  return NotificationDeliveryStateSchema.parse({
1122
1210
  version: config.version,
@@ -1127,8 +1215,11 @@ function notificationDeliveryPlan(config, payload) {
1127
1215
  async function deliverNotification(input) {
1128
1216
  const { item, config } = input;
1129
1217
  const plan = item.delivery;
1130
- if (!plan || plan.version !== config.version)
1131
- throw new Error("Notification action plan version does not match the executor");
1218
+ if (!plan)
1219
+ throw new Error("Notification has no action plan");
1220
+ const project = projectionFor(config, plan.version);
1221
+ if (!project)
1222
+ throw new NotificationPlanVersionError(plan.version);
1132
1223
  const owns = (current2) => current2.key === item.key && current2.leaseId === item.leaseId;
1133
1224
  const checkpoint = async (change) => input.store.update((state) => {
1134
1225
  if (!state)
@@ -1153,7 +1244,7 @@ async function deliverNotification(input) {
1153
1244
  for (const action of plan.actions) {
1154
1245
  if (!input.active())
1155
1246
  return false;
1156
- if (plan.receipts.some((receipt2) => receipt2.action === action))
1247
+ if (plan.receipts.some((receipt) => receipt.action === action))
1157
1248
  continue;
1158
1249
  if (!await checkpoint((value) => value))
1159
1250
  return false;
@@ -1162,11 +1253,24 @@ async function deliverNotification(input) {
1162
1253
  action,
1163
1254
  idempotencyKey: JSON.stringify([item.key, plan.version, action])
1164
1255
  });
1165
- const receipt = z6.json().parse(result ?? null);
1166
- if (!await checkpoint((value) => ({
1167
- ...value,
1168
- receipts: [...value.receipts, { action, receipt }]
1169
- })))
1256
+ const entry = recordable(result);
1257
+ const confirm = (receipt) => checkpoint((value) => ({ ...value, receipts: [...value.receipts, receipt] }));
1258
+ let confirmed;
1259
+ let dropped;
1260
+ try {
1261
+ confirmed = await confirm({ action, ...entry });
1262
+ } catch (error) {
1263
+ if (entry.unrecorded || !(error instanceof OutboxStateLimitError))
1264
+ throw error;
1265
+ dropped = error;
1266
+ confirmed = await confirm({ action, receipt: null, unrecorded: "too-large" });
1267
+ }
1268
+ if (dropped)
1269
+ await input.report(dropped);
1270
+ if (entry.unrecorded) {
1271
+ await input.report(new Error(`Notification action "${action}" returned a receipt that is not JSON`));
1272
+ }
1273
+ if (!confirmed)
1170
1274
  return false;
1171
1275
  }
1172
1276
  if (!input.active())
@@ -1176,7 +1280,7 @@ async function deliverNotification(input) {
1176
1280
  if (!live?.delivery)
1177
1281
  return false;
1178
1282
  if (live.delivery.projectedAt === undefined) {
1179
- await config.project(notification, live.delivery.receipts);
1283
+ await project(notification, live.delivery.receipts);
1180
1284
  if (!await checkpoint((value) => ({ ...value, projectedAt: input.clock().toISOString() })))
1181
1285
  return false;
1182
1286
  }
@@ -1184,62 +1288,6 @@ async function deliverNotification(input) {
1184
1288
  }
1185
1289
  // src/application/notification-outbox.ts
1186
1290
  import { z as z8 } from "zod";
1187
-
1188
- // src/application/notification-outbox-state.ts
1189
- import { z as z7 } from "zod";
1190
- var sizeOf = (value) => measureSize(value).responseBytes;
1191
- var DEFAULT_OUTBOX_BACKOFF = {
1192
- minDelayMs: 1000,
1193
- maxDelayMs: 60000,
1194
- jitter: 0
1195
- };
1196
- function resolveOutboxLimits(config) {
1197
- const pollIntervalMs = z7.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1198
- const leaseMs = z7.number().int().min(100).parse(config.leaseMs ?? 30000);
1199
- const maxAttempts = z7.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1200
- const maxQueue = z7.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1201
- const maxStateBytes = z7.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1202
- const retainReceipts = z7.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1203
- return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
1204
- }
1205
- function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
1206
- if (parsed.queue.length > maxQueue) {
1207
- throw new Error(`[stitchkit] notification outbox queue limit (${maxQueue}) exceeded`);
1208
- }
1209
- let receipts = parsed.receipts.slice(0, retainReceipts);
1210
- let next = { ...parsed, receipts };
1211
- while (sizeOf(next) > maxStateBytes && receipts.length > 0) {
1212
- receipts = receipts.slice(0, -1);
1213
- next = { ...parsed, receipts };
1214
- }
1215
- if (sizeOf(next) > maxStateBytes) {
1216
- throw new Error(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
1217
- }
1218
- return next;
1219
- }
1220
- function dropReason(classification, attempts, maxAttempts) {
1221
- if (classification.recipientUnreachable === true)
1222
- return "recipient-unreachable";
1223
- if (!classification.retryable)
1224
- return "terminal";
1225
- return attempts >= maxAttempts ? "attempt-limit" : null;
1226
- }
1227
- var RetryDelaySchema = z7.number().nonnegative().max(24 * 60 * 60 * 1000);
1228
- var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
1229
- function claimDue(queue, now, ownerId, leaseMs) {
1230
- const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
1231
- if (!candidate)
1232
- return null;
1233
- return {
1234
- ...candidate,
1235
- attempts: candidate.attempts + 1,
1236
- leaseOwner: ownerId,
1237
- leaseId: crypto.randomUUID(),
1238
- leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
1239
- };
1240
- }
1241
-
1242
- // src/application/notification-outbox.ts
1243
1291
  function notificationOutboxStateSchema(payload) {
1244
1292
  return z8.object({
1245
1293
  schemaVersion: z8.literal(1),
@@ -1271,16 +1319,19 @@ async function reportOutboxError(config, error) {
1271
1319
  await config.onError?.(error);
1272
1320
  } catch {}
1273
1321
  }
1274
- function notificationRejection(config, parse, bounded, clock, backoffMs) {
1322
+ function notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs) {
1275
1323
  const maxAttempts = config.maxAttempts ?? 100;
1276
1324
  return async (claimed, error) => {
1277
- const classification = await config.classify(error);
1325
+ const unknownPlan = error instanceof NotificationPlanVersionError;
1326
+ if (unknownPlan)
1327
+ await reportOutboxError(config, error);
1328
+ const classification = unknownPlan ? { retryable: true } : await config.classify(error);
1278
1329
  return config.store.update((current) => {
1279
1330
  const state = parse(current);
1280
1331
  const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1281
1332
  if (!live)
1282
1333
  return { state, result: null };
1283
- const attempts = live.attempts;
1334
+ const attempts = unknownPlan ? Math.max(0, live.attempts - 1) : live.attempts;
1284
1335
  const reason = dropReason(classification, attempts, maxAttempts);
1285
1336
  if (reason) {
1286
1337
  return {
@@ -1291,9 +1342,11 @@ function notificationRejection(config, parse, bounded, clock, backoffMs) {
1291
1342
  result: { item: { ...live, attempts }, error, reason }
1292
1343
  };
1293
1344
  }
1294
- const wait = RetryDelaySchema.parse(backoffMs(attempts));
1345
+ const backoff = RetryDelaySchema.parse(backoffMs(Math.max(1, attempts)));
1346
+ const wait = unknownPlan ? Math.max(backoff, pollIntervalMs) : backoff;
1295
1347
  const retry = {
1296
1348
  ...live,
1349
+ attempts,
1297
1350
  nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
1298
1351
  ...NO_LEASE
1299
1352
  };
@@ -1387,7 +1440,7 @@ function createNotificationOutbox(config) {
1387
1440
  };
1388
1441
  });
1389
1442
  };
1390
- const reject = notificationRejection(config, parse, bounded, clock, backoffMs);
1443
+ const reject = notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs);
1391
1444
  const flushPass = async () => {
1392
1445
  let delivered = 0;
1393
1446
  while (!interrupted) {
@@ -1404,7 +1457,8 @@ function createNotificationOutbox(config) {
1404
1457
  clock,
1405
1458
  bounded,
1406
1459
  leaseMs,
1407
- active: () => !interrupted
1460
+ active: () => !interrupted,
1461
+ report: (error) => reportOutboxError(config, error)
1408
1462
  });
1409
1463
  if (!finished)
1410
1464
  return delivered;
@@ -1559,7 +1613,8 @@ function previousExitOf(previous, run, overlap) {
1559
1613
  return ended;
1560
1614
  if (previous.pid === run.pid)
1561
1615
  return "hot-reload";
1562
- if (isVersionChange(previous.version, run.version) || overlap === "handoff")
1616
+ const declared = isVersionChange(previous.version, run.version) ? overlap.versionChange : overlap.sameVersion;
1617
+ if (declared === "handoff")
1563
1618
  return "handoff";
1564
1619
  return previous.readyAt === null ? "startup-failed" : "abnormal";
1565
1620
  }
@@ -1569,7 +1624,8 @@ function closeAbandoned(candidate, previous, previousExit, run, overlap) {
1569
1624
  const closed = { ...candidate, stoppedAt: run.startedAt };
1570
1625
  if (candidate.pid === run.pid)
1571
1626
  return { ...closed, termination: "hot-reload" };
1572
- if (overlap === "handoff")
1627
+ const overlapsThisBuild = overlap.versionChange === "handoff" || !isVersionChange(candidate.version, run.version);
1628
+ if (overlap.sameVersion === "handoff" && overlapsThisBuild)
1573
1629
  return candidate;
1574
1630
  if (candidate.runId === previous?.runId && previousExit === "handoff")
1575
1631
  return candidate;
@@ -1580,7 +1636,10 @@ function closeAbandoned(candidate, previous, previousExit, run, overlap) {
1580
1636
  }
1581
1637
  function transitionProcessStart(source, input) {
1582
1638
  const retain = RetainSchema.parse(input.retain ?? 20);
1583
- const overlap = input.sameVersionOverlap ?? "abnormal";
1639
+ const overlap = {
1640
+ sameVersion: input.sameVersionOverlap ?? "abnormal",
1641
+ versionChange: input.versionChangeOverlap ?? "handoff"
1642
+ };
1584
1643
  const state = normalizeLifecycleState(source, 1000);
1585
1644
  const run = LifecycleRunSchema.parse({
1586
1645
  runId: input.runId,
@@ -1761,7 +1820,8 @@ function createProcessLifecycleLedger(config) {
1761
1820
  version,
1762
1821
  now: clock().toISOString(),
1763
1822
  retain,
1764
- sameVersionOverlap: config.sameVersionOverlap
1823
+ sameVersionOverlap: config.sameVersionOverlap,
1824
+ versionChangeOverlap: config.versionChangeOverlap
1765
1825
  });
1766
1826
  return { state: transition.state, result: transition.fact };
1767
1827
  });
@@ -2088,6 +2148,78 @@ function managedServerResource(config) {
2088
2148
  }
2089
2149
  });
2090
2150
  }
2151
+ // src/application/watch-hub-source.ts
2152
+ function remember(source, deltaMemoryBytes, revision, value, fingerprint) {
2153
+ if (deltaMemoryBytes === 0)
2154
+ return;
2155
+ const bytes = (JSON.stringify(value) ?? "null").length;
2156
+ if (bytes > deltaMemoryBytes)
2157
+ return;
2158
+ source.history.set(revision, { value, fingerprint, bytes });
2159
+ source.historyBytes += bytes;
2160
+ for (const [oldest, entry] of source.history) {
2161
+ if (source.historyBytes <= deltaMemoryBytes)
2162
+ break;
2163
+ source.history.delete(oldest);
2164
+ source.historyBytes -= entry.bytes;
2165
+ }
2166
+ }
2167
+ function fullFrame(source) {
2168
+ const value = source.value;
2169
+ return {
2170
+ kind: "full",
2171
+ key: source.key,
2172
+ revision: source.revision,
2173
+ fingerprint: source.fingerprint ?? valueFingerprint(value),
2174
+ value
2175
+ };
2176
+ }
2177
+ function frameFor(source, subscriber) {
2178
+ const value = source.value;
2179
+ const full = fullFrame(source);
2180
+ const fingerprint = full.fingerprint;
2181
+ const base = source.baselines.get(subscriber);
2182
+ if (base === undefined)
2183
+ return full;
2184
+ if (base === source.revision) {
2185
+ return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
2186
+ }
2187
+ const held = source.history.get(base);
2188
+ if (!held)
2189
+ return full;
2190
+ const delta = diff(held.value, value);
2191
+ if (delta === undefined || !deltaWins(delta, value))
2192
+ return full;
2193
+ return {
2194
+ kind: "delta",
2195
+ key: source.key,
2196
+ revision: source.revision,
2197
+ fingerprint,
2198
+ base,
2199
+ delta
2200
+ };
2201
+ }
2202
+ function adoptBaseline(source, subscriber, have) {
2203
+ if (source.fingerprint === have.fingerprint) {
2204
+ source.baselines.set(subscriber, source.revision);
2205
+ return;
2206
+ }
2207
+ const named = source.history.get(have.revision);
2208
+ if (named?.fingerprint === have.fingerprint) {
2209
+ source.baselines.set(subscriber, have.revision);
2210
+ return;
2211
+ }
2212
+ for (const [revision, entry] of source.history) {
2213
+ if (entry.fingerprint !== have.fingerprint)
2214
+ continue;
2215
+ source.baselines.set(subscriber, revision);
2216
+ return;
2217
+ }
2218
+ }
2219
+ function valueFingerprint(value) {
2220
+ return argumentsDigest({ value });
2221
+ }
2222
+
2091
2223
  // src/application/watch-hub-core.ts
2092
2224
  var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
2093
2225
 
@@ -2137,82 +2269,24 @@ class WatchHubCore {
2137
2269
  announceState(source, state) {
2138
2270
  source.state = state;
2139
2271
  for (const subscriber of source.subscribers) {
2140
- this.tell(source.key, () => subscriber.state(state));
2141
- }
2142
- }
2143
- remember(source, revision, value, fingerprint) {
2144
- if (this.deltaMemoryBytes === 0)
2145
- return;
2146
- const bytes = (JSON.stringify(value) ?? "null").length;
2147
- if (bytes > this.deltaMemoryBytes)
2148
- return;
2149
- source.history.set(revision, { value, fingerprint, bytes });
2150
- source.historyBytes += bytes;
2151
- for (const [oldest, entry] of source.history) {
2152
- if (source.historyBytes <= this.deltaMemoryBytes)
2153
- break;
2154
- source.history.delete(oldest);
2155
- source.historyBytes -= entry.bytes;
2156
- }
2157
- }
2158
- frameFor(source, subscriber) {
2159
- const value = source.value;
2160
- const fingerprint = source.fingerprint ?? valueFingerprint(value);
2161
- const full = {
2162
- kind: "full",
2163
- key: source.key,
2164
- revision: source.revision,
2165
- fingerprint,
2166
- value
2167
- };
2168
- const base = source.baselines.get(subscriber);
2169
- if (base === undefined)
2170
- return full;
2171
- if (base === source.revision) {
2172
- return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
2173
- }
2174
- const held = source.history.get(base);
2175
- if (!held)
2176
- return full;
2177
- const delta = diff(held.value, value);
2178
- if (delta === undefined || !deltaWins(delta, value))
2179
- return full;
2180
- return {
2181
- kind: "delta",
2182
- key: source.key,
2183
- revision: source.revision,
2184
- fingerprint,
2185
- base,
2186
- delta
2187
- };
2188
- }
2189
- adoptBaseline(source, subscriber, have) {
2190
- if (source.fingerprint === have.fingerprint) {
2191
- source.baselines.set(subscriber, source.revision);
2192
- return;
2193
- }
2194
- const named = source.history.get(have.revision);
2195
- if (named?.fingerprint === have.fingerprint) {
2196
- source.baselines.set(subscriber, have.revision);
2197
- return;
2198
- }
2199
- for (const [revision, entry] of source.history) {
2200
- if (entry.fingerprint !== have.fingerprint)
2201
- continue;
2202
- source.baselines.set(subscriber, revision);
2203
- return;
2272
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2273
+ this.tell(key, () => subscriber.state({ ...state, key }));
2274
+ }
2204
2275
  }
2205
2276
  }
2206
2277
  deliver(source, subscriber) {
2207
- const frame = this.frameFor(source, subscriber);
2208
- const delivered = this.tell(source.key, () => subscriber.value(frame));
2278
+ const frame = frameFor(source, subscriber);
2279
+ let delivered = true;
2280
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2281
+ delivered = this.tell(key, () => subscriber.value({ ...frame, key })) && delivered;
2282
+ }
2209
2283
  if (delivered)
2210
2284
  source.baselines.set(subscriber, source.revision);
2211
2285
  return delivered;
2212
2286
  }
2213
2287
  publish(source, value, fingerprint) {
2214
2288
  if (source.fingerprint !== undefined) {
2215
- this.remember(source, source.revision, source.value, source.fingerprint);
2289
+ remember(source, this.deltaMemoryBytes, source.revision, source.value, source.fingerprint);
2216
2290
  }
2217
2291
  source.revision += 1;
2218
2292
  source.value = value;
@@ -2234,21 +2308,20 @@ class WatchHubCore {
2234
2308
  this.announceState(source, { key: source.key, phase: "live" });
2235
2309
  }
2236
2310
  readFailed(source, error) {
2237
- const code = readErrorCode(error);
2238
2311
  this.announceState(source, {
2239
2312
  key: source.key,
2240
2313
  phase: "unavailable",
2241
2314
  reason: "source-error",
2242
- ...code !== undefined && { code },
2243
- message: error instanceof Error ? error.message : String(error)
2315
+ ...AppError.is(error) ? { code: error.code, message: error.message } : { message: "The watched read failed" }
2244
2316
  });
2245
2317
  this.config.logger?.warn?.("[stitchkit] watched read failed", {
2246
2318
  key: watchKeyString(source.key),
2247
2319
  error
2248
2320
  });
2249
2321
  }
2250
- acquire(operation, key, args, scope) {
2251
- const id = JSON.stringify([scope ?? null, watchKeyString(key)]);
2322
+ acquire(operation, routed, args, scope) {
2323
+ const id = sourceId(scope, routed);
2324
+ const key = sharedKey(routed);
2252
2325
  const existing = this.sources.get(id);
2253
2326
  if (existing) {
2254
2327
  clearTimeout(existing.release);
@@ -2262,6 +2335,7 @@ class WatchHubCore {
2262
2335
  operation,
2263
2336
  args,
2264
2337
  subscribers: new Set,
2338
+ routes: new Map,
2265
2339
  unsubscribes: [],
2266
2340
  revision: 0,
2267
2341
  history: new Map,
@@ -2305,12 +2379,32 @@ class WatchHubCore {
2305
2379
  source.release = setTimeout(drop, this.holdMs);
2306
2380
  source.release.unref?.();
2307
2381
  }
2382
+ unroute(source, subscriber, route) {
2383
+ const routes = source.routes.get(subscriber);
2384
+ routes?.delete(route);
2385
+ if (routes && routes.size > 0)
2386
+ return;
2387
+ source.routes.delete(subscriber);
2388
+ source.subscribers.delete(subscriber);
2389
+ source.baselines.delete(subscriber);
2390
+ this.release(source);
2391
+ }
2308
2392
  attach(subscriber, scope) {
2309
- if (this.held.size >= (this.config.maxSubscribers ?? 1024))
2310
- throw new Error("Watch subscriber capacity exceeded");
2393
+ if (this.held.size >= (this.config.maxSubscribers ?? 1024)) {
2394
+ const refused = {
2395
+ open: () => ({ accepted: false, reason: "Watch subscriber capacity exceeded" }),
2396
+ close: () => {
2397
+ return;
2398
+ },
2399
+ detach: () => {
2400
+ return;
2401
+ }
2402
+ };
2403
+ return refused;
2404
+ }
2311
2405
  if (scope && (!scope.key || scope.key.length > 512))
2312
2406
  throw new Error("Invalid watch admission scope");
2313
- const keys = new Set;
2407
+ const keys = new Map;
2314
2408
  this.held.set(subscriber, keys);
2315
2409
  let detached = false;
2316
2410
  const attached = {
@@ -2326,8 +2420,9 @@ class WatchHubCore {
2326
2420
  reason: `${key.service}.${key.action} is not watchable`
2327
2421
  };
2328
2422
  }
2329
- const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
2330
- if (keys.has(id))
2423
+ const route = watchKeyString(key);
2424
+ const id = sourceId(scope?.key, key);
2425
+ if (keys.has(route))
2331
2426
  return { accepted: true };
2332
2427
  if (keys.size >= this.maxWatches) {
2333
2428
  return {
@@ -2338,52 +2433,50 @@ class WatchHubCore {
2338
2433
  if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
2339
2434
  return { accepted: false, reason: "Watch source capacity exceeded" };
2340
2435
  const source = this.acquire(operation, key, args, scope?.key);
2436
+ const alreadyRouted = source.routes.has(subscriber);
2341
2437
  source.subscribers.add(subscriber);
2342
- keys.add(id);
2438
+ const routes = source.routes.get(subscriber) ?? new Map;
2439
+ routes.set(route, key);
2440
+ source.routes.set(subscriber, routes);
2441
+ keys.set(route, id);
2343
2442
  if (have)
2344
- this.adoptBaseline(source, subscriber, have);
2443
+ adoptBaseline(source, subscriber, have);
2345
2444
  try {
2346
2445
  if (source.signature !== undefined) {
2347
- const frame = this.frameFor(source, subscriber);
2348
- subscriber.value(frame);
2446
+ const frame = alreadyRouted && !have ? fullFrame(source) : frameFor(source, subscriber);
2447
+ subscriber.value({ ...frame, key });
2349
2448
  source.baselines.set(subscriber, source.revision);
2350
2449
  }
2351
- subscriber.state(source.state);
2450
+ subscriber.state({ ...source.state, key });
2352
2451
  } catch (error) {
2353
- source.subscribers.delete(subscriber);
2354
- source.baselines.delete(subscriber);
2355
- keys.delete(id);
2356
- this.release(source);
2452
+ keys.delete(route);
2453
+ this.unroute(source, subscriber, route);
2357
2454
  return {
2358
2455
  accepted: false,
2359
2456
  reason: error instanceof Error ? error.message : String(error)
2360
2457
  };
2361
2458
  }
2362
- if (!source.task.running && (source.signature === undefined || source.state.phase !== "live"))
2459
+ if (!source.task.running && (source.signature === undefined || source.state.phase !== "live" || source.task.pending))
2363
2460
  source.task.trigger();
2364
2461
  return { accepted: true };
2365
2462
  },
2366
2463
  close: (key) => {
2367
- const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
2368
- if (!keys.delete(id))
2464
+ const route = watchKeyString(key);
2465
+ const id = keys.get(route);
2466
+ if (id === undefined)
2369
2467
  return;
2468
+ keys.delete(route);
2370
2469
  const source = this.sources.get(id);
2371
- if (!source)
2372
- return;
2373
- source.subscribers.delete(subscriber);
2374
- source.baselines.delete(subscriber);
2375
- this.release(source);
2470
+ if (source)
2471
+ this.unroute(source, subscriber, route);
2376
2472
  },
2377
2473
  detach: () => {
2378
2474
  detached = true;
2379
2475
  scope?.signal.removeEventListener("abort", onAbort);
2380
- for (const id of keys) {
2476
+ for (const [route, id] of keys) {
2381
2477
  const source = this.sources.get(id);
2382
- if (!source)
2383
- continue;
2384
- source.subscribers.delete(subscriber);
2385
- source.baselines.delete(subscriber);
2386
- this.release(source);
2478
+ if (source)
2479
+ this.unroute(source, subscriber, route);
2387
2480
  }
2388
2481
  keys.clear();
2389
2482
  this.held.delete(subscriber);
@@ -2416,14 +2509,12 @@ class WatchHubCore {
2416
2509
  }
2417
2510
  }
2418
2511
  }
2419
- function valueFingerprint(value) {
2420
- return argumentsDigest({ value });
2512
+ function sharedKey(key) {
2513
+ const { instance: _routing, ...shared } = key;
2514
+ return shared;
2421
2515
  }
2422
- function readErrorCode(error) {
2423
- if (typeof error !== "object" || error === null)
2424
- return;
2425
- const code = Reflect.get(error, "code");
2426
- return typeof code === "string" ? code : undefined;
2516
+ function sourceId(scope, key) {
2517
+ return JSON.stringify([scope ?? null, watchKeyString(sharedKey(key))]);
2427
2518
  }
2428
2519
 
2429
2520
  // src/application/watch-hub.ts
@@ -2487,6 +2578,7 @@ export {
2487
2578
  RevisionSignalAdvanceResultSchema,
2488
2579
  PreviousExitSchema,
2489
2580
  PolicyDecisionSchema,
2581
+ NotificationPlanVersionError,
2490
2582
  NotificationDeliveryStateSchema,
2491
2583
  ManagedScheduleStatusSchema,
2492
2584
  ManagedScheduleOverlapSchema,