@holon-run/agentinbox 1.0.0-beta.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ manually or relying on the agent runtime to expose its own notification API.
66
66
 
67
67
  ## v1.0.0 Beta Upgrade Notes
68
68
 
69
- `v1.0.0-beta.0` is the first release on the final v1 storage and API boundary.
69
+ `v1.0.0-beta.0` was the first release on the final v1 storage and API boundary.
70
70
 
71
71
  - pre-v1 local databases are archived and replaced with a fresh v1 database
72
72
  - old local state is not imported into the new v1 database
@@ -171,6 +171,10 @@ agentinbox inbox read --agent-id <agent_id>
171
171
  agentinbox inbox ack --agent-id <agent_id> --through <last_entry_id>
172
172
  ```
173
173
 
174
+ The HTTP `POST /subscriptions` endpoint always returns
175
+ `{ "subscriptions": [...] }`, even when only one subscription is created. That
176
+ keeps shortcut expansion consistent for external callers.
177
+
174
178
  When acking a reviewed batch, prefer `--through <last_entry_id>` over `--all`.
175
179
  That preserves the boundary of the items you actually inspected. Use
176
180
  `ack --all` only when you have intentionally verified that every current
package/dist/src/cli.js CHANGED
@@ -19,8 +19,11 @@ const store_1 = require("./store");
19
19
  const terminal_1 = require("./terminal");
20
20
  const util_1 = require("./util");
21
21
  async function main() {
22
- const args = process.argv.slice(2);
23
- const normalized = normalizeHelpArgs(args);
22
+ const rawArgs = process.argv.slice(2);
23
+ const detectedArgs = normalizeHelpArgs(rawArgs);
24
+ const normalized = shouldTreatJsonFlagAsNoop(detectedArgs)
25
+ ? stripNoopJsonFlag(detectedArgs)
26
+ : detectedArgs;
24
27
  const command = normalized[0];
25
28
  if (!command || command === "help" || command === "--help" || command === "-h") {
26
29
  printHelp(normalized.slice(1));
@@ -445,7 +448,7 @@ async function main() {
445
448
  startOffset: parseOptionalNumber(takeFlagValue(normalized, "--start-offset")),
446
449
  startTime: takeFlagValue(normalized, "--start-time") ?? undefined,
447
450
  });
448
- console.log((0, util_1.jsonResponse)(withCommandMetadata(response.data, selection)));
451
+ console.log((0, util_1.jsonResponse)(withCommandMetadata(normalizeSubscriptionAddOutput(response.data, Boolean(shortcutName)), selection)));
449
452
  return;
450
453
  }
451
454
  if (command === "subscription" && normalized[1] === "list") {
@@ -767,7 +770,7 @@ async function main() {
767
770
  printHelp([command]);
768
771
  return;
769
772
  }
770
- throw new Error(`unknown command: ${args.join(" ")}`);
773
+ throw new Error(`unknown command: ${normalized.join(" ")}`);
771
774
  }
772
775
  async function runServe(args) {
773
776
  const port = parseOptionalNumber(takeFlagValue(args, "--port"));
@@ -1000,6 +1003,26 @@ function normalizeHelpArgs(args) {
1000
1003
  }
1001
1004
  return [args[1], "--help"];
1002
1005
  }
1006
+ function stripNoopJsonFlag(args) {
1007
+ return args.filter((arg) => arg !== "--json");
1008
+ }
1009
+ function shouldTreatJsonFlagAsNoop(args) {
1010
+ if (!args.includes("--json")) {
1011
+ return false;
1012
+ }
1013
+ const command = args[0];
1014
+ if (!command) {
1015
+ return false;
1016
+ }
1017
+ if (args.includes("--help") || args.includes("-h")) {
1018
+ return false;
1019
+ }
1020
+ return command !== "help" &&
1021
+ command !== "version" &&
1022
+ command !== "--version" &&
1023
+ command !== "-v" &&
1024
+ command !== "serve";
1025
+ }
1003
1026
  function hasHelpFlag(args) {
1004
1027
  return args.includes("--help") || args.includes("-h");
1005
1028
  }
@@ -1047,6 +1070,19 @@ function withCommandMetadata(data, selection) {
1047
1070
  ...(selection.warnings.length > 0 ? { warnings: selection.warnings } : {}),
1048
1071
  };
1049
1072
  }
1073
+ function normalizeSubscriptionAddOutput(data, shortcutUsed) {
1074
+ if (shortcutUsed) {
1075
+ return data;
1076
+ }
1077
+ const subscriptions = data.subscriptions;
1078
+ if (Array.isArray(subscriptions) &&
1079
+ subscriptions.length === 1 &&
1080
+ subscriptions[0] &&
1081
+ typeof subscriptions[0] === "object") {
1082
+ return subscriptions[0];
1083
+ }
1084
+ return data;
1085
+ }
1050
1086
  function getRequiredTerminalContext() {
1051
1087
  try {
1052
1088
  return (0, terminal_1.detectTerminalContext)(process.env);
package/dist/src/http.js CHANGED
@@ -29,6 +29,14 @@ const errorResponseSchema = {
29
29
  error: { type: "string" },
30
30
  },
31
31
  };
32
+ const subscriptionCollectionSchema = {
33
+ type: "object",
34
+ required: ["subscriptions"],
35
+ additionalProperties: false,
36
+ properties: {
37
+ subscriptions: { type: "array", items: jsonObjectSchema },
38
+ },
39
+ };
32
40
  const deliveryHandleSchema = {
33
41
  type: "object",
34
42
  additionalProperties: false,
@@ -1041,13 +1049,7 @@ function buildFastifyServer(service) {
1041
1049
  },
1042
1050
  },
1043
1051
  response: {
1044
- 200: {
1045
- type: "object",
1046
- required: ["subscriptions"],
1047
- properties: {
1048
- subscriptions: { type: "array", items: jsonObjectSchema },
1049
- },
1050
- },
1052
+ 200: subscriptionCollectionSchema,
1051
1053
  },
1052
1054
  },
1053
1055
  }, async (request) => {
@@ -1087,13 +1089,13 @@ function buildFastifyServer(service) {
1087
1089
  },
1088
1090
  },
1089
1091
  response: {
1090
- 200: jsonObjectSchema,
1092
+ 200: subscriptionCollectionSchema,
1091
1093
  400: errorResponseSchema,
1092
1094
  },
1093
1095
  },
1094
1096
  }, async (request) => {
1095
1097
  const body = request.body;
1096
- return service.registerSubscription({
1098
+ const input = {
1097
1099
  agentId: String(body.agentId),
1098
1100
  sourceId: String(body.sourceId),
1099
1101
  shortcut: body.shortcut && typeof body.shortcut === "object"
@@ -1110,7 +1112,8 @@ function buildFastifyServer(service) {
1110
1112
  startPolicy: optionalString(body.startPolicy),
1111
1113
  startOffset: typeof body.startOffset === "number" ? body.startOffset : undefined,
1112
1114
  startTime: optionalString(body.startTime) ?? undefined,
1113
- });
1115
+ };
1116
+ return { subscriptions: await service.registerSubscriptions(input) };
1114
1117
  });
1115
1118
  app.get("/subscriptions/:subscriptionId", {
1116
1119
  schema: {
@@ -707,26 +707,93 @@ class AgentInboxService {
707
707
  return result;
708
708
  }
709
709
  async registerSubscription(input) {
710
- if (input.shortcut) {
711
- const source = this.store.getSource(input.sourceId);
712
- if (!source) {
713
- throw new Error(`unknown source: ${input.sourceId}`);
710
+ const expanded = await this.expandShortcutRegistration(input);
711
+ if (!expanded) {
712
+ return this.registerSingleSubscription(input);
713
+ }
714
+ if (expanded.plan.members.length !== 1) {
715
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} for source ${input.sourceId} expands to ${expanded.plan.members.length} subscriptions; use registerSubscriptions() instead`);
716
+ }
717
+ const subscriptions = await this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
718
+ return subscriptions[0];
719
+ }
720
+ async registerSubscriptions(input) {
721
+ const expanded = await this.expandShortcutRegistration(input);
722
+ if (expanded) {
723
+ return this.registerExpandedShortcutSubscriptions(input, expanded.source, expanded.plan);
724
+ }
725
+ return [await this.registerSingleSubscription(input)];
726
+ }
727
+ async expandShortcutRegistration(input) {
728
+ if (!input.shortcut) {
729
+ return null;
730
+ }
731
+ const source = this.store.getSource(input.sourceId);
732
+ if (!source) {
733
+ throw new Error(`unknown source: ${input.sourceId}`);
734
+ }
735
+ if (hasSubscriptionFieldOverride(input)) {
736
+ throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
737
+ }
738
+ const plan = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
739
+ if (!plan) {
740
+ throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
741
+ }
742
+ return { source, plan };
743
+ }
744
+ async registerExpandedShortcutSubscriptions(input, source, plan) {
745
+ if (plan.members.length === 0) {
746
+ throw new Error(`subscription shortcut ${input.shortcut?.name ?? "unknown"} did not produce any subscriptions`);
747
+ }
748
+ const resolvedMembers = plan.members.map((member) => ({
749
+ source: this.resolveShortcutMemberSource(source, member),
750
+ member,
751
+ }));
752
+ const created = [];
753
+ try {
754
+ for (const { source: memberSource, member } of resolvedMembers) {
755
+ created.push(await this.registerSingleSubscription({
756
+ ...input,
757
+ sourceId: memberSource.sourceId,
758
+ shortcut: undefined,
759
+ filter: member.filter,
760
+ trackedResourceRef: member.trackedResourceRef ?? null,
761
+ cleanupPolicy: member.cleanupPolicy ?? null,
762
+ }));
714
763
  }
715
- if (hasSubscriptionFieldOverride(input)) {
716
- throw new Error("subscription add shortcut does not allow filter, trackedResourceRef, or cleanupPolicy overrides");
764
+ }
765
+ catch (error) {
766
+ await this.rollbackRegisteredSubscriptions(created);
767
+ throw error;
768
+ }
769
+ return created;
770
+ }
771
+ async rollbackRegisteredSubscriptions(subscriptions) {
772
+ for (const subscription of [...subscriptions].reverse()) {
773
+ try {
774
+ await this.removeSubscription(subscription.subscriptionId);
717
775
  }
718
- const expanded = await this.adapters.expandSubscriptionShortcut(source, input.shortcut);
719
- if (!expanded) {
720
- throw new Error(`unknown subscription shortcut ${input.shortcut.name} for source ${input.sourceId}`);
776
+ catch (error) {
777
+ console.warn(`failed to roll back subscription ${subscription.subscriptionId}:`, error);
721
778
  }
722
- return this.registerSubscription({
723
- ...input,
724
- shortcut: undefined,
725
- filter: expanded.filter,
726
- trackedResourceRef: expanded.trackedResourceRef ?? null,
727
- cleanupPolicy: expanded.cleanupPolicy,
728
- });
729
779
  }
780
+ }
781
+ resolveShortcutMemberSource(source, member) {
782
+ if (!member.streamKind || member.streamKind === source.streamKind) {
783
+ return source;
784
+ }
785
+ if (!source.hostId || !source.streamKey) {
786
+ throw new Error(`subscription shortcut ${source.sourceId} requires a host-scoped source to resolve ${member.streamKind}`);
787
+ }
788
+ const sibling = this.store.listSources().find((candidate) => candidate.hostId === source.hostId &&
789
+ candidate.streamKind === member.streamKind &&
790
+ candidate.streamKey === source.streamKey);
791
+ if (!sibling) {
792
+ throw new Error(`subscription shortcut ${source.sourceId} requires an existing sibling ${member.streamKind} stream under host ${source.hostId} with streamKey ${source.streamKey}`);
793
+ }
794
+ return sibling;
795
+ }
796
+ async registerSingleSubscription(input) {
730
797
  this.getAgent(input.agentId);
731
798
  const source = this.store.getSource(input.sourceId);
732
799
  if (!source) {
@@ -755,17 +822,25 @@ class AgentInboxService {
755
822
  };
756
823
  this.ensureInboxForAgent(subscription.agentId);
757
824
  this.store.insertSubscription(subscription);
758
- const stream = await this.ensureStreamForSource(source);
759
- await this.backend.ensureConsumer({
760
- streamId: stream.streamId,
761
- subscriptionId: subscription.subscriptionId,
762
- consumerKey: `subscription:${subscription.subscriptionId}`,
763
- startPolicy: subscription.startPolicy,
764
- startOffset: subscription.startOffset ?? null,
765
- startTime: subscription.startTime ?? null,
766
- });
767
- this.store.deleteSourceIdleState(source.sourceId);
768
- return subscription;
825
+ try {
826
+ const stream = await this.ensureStreamForSource(source);
827
+ await this.backend.ensureConsumer({
828
+ streamId: stream.streamId,
829
+ subscriptionId: subscription.subscriptionId,
830
+ consumerKey: `subscription:${subscription.subscriptionId}`,
831
+ startPolicy: subscription.startPolicy,
832
+ startOffset: subscription.startOffset ?? null,
833
+ startTime: subscription.startTime ?? null,
834
+ });
835
+ this.store.deleteSourceIdleState(source.sourceId);
836
+ return subscription;
837
+ }
838
+ catch (error) {
839
+ this.store.deleteSubscription(subscription.subscriptionId);
840
+ this.clearSubscriptionRuntimeState(subscription);
841
+ this.refreshSourceIdleState(source.sourceId);
842
+ throw error;
843
+ }
769
844
  }
770
845
  async removeSubscription(subscriptionId) {
771
846
  const subscription = this.getSubscription(subscriptionId);
@@ -1322,7 +1397,7 @@ class AgentInboxService {
1322
1397
  });
1323
1398
  }
1324
1399
  for (const signal of lifecycleSignals.values()) {
1325
- this.scheduleLifecycleRetirements(source, subscription, signal);
1400
+ this.scheduleLifecycleRetirements(source, signal);
1326
1401
  }
1327
1402
  return {
1328
1403
  subscriptionId: subscription.subscriptionId,
@@ -1483,31 +1558,33 @@ class AgentInboxService {
1483
1558
  signals.set(normalized.ref, normalized);
1484
1559
  }
1485
1560
  }
1486
- scheduleLifecycleRetirements(source, subscription, signal) {
1561
+ scheduleLifecycleRetirements(source, signal) {
1487
1562
  if (!signal.terminal) {
1488
1563
  return;
1489
1564
  }
1490
- if (!subscription.trackedResourceRef || subscription.trackedResourceRef !== signal.ref) {
1565
+ if (!source.hostId) {
1491
1566
  return;
1492
1567
  }
1493
1568
  const signalOccurredAt = signal.occurredAt ?? (0, util_1.nowIso)();
1494
1569
  const signalOccurredAtMs = Date.parse(signalOccurredAt);
1495
- const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1496
- if (!retireAt) {
1497
- return;
1570
+ for (const subscription of this.store.listSubscriptionsForHostTrackedResourceRef(source.hostId, signal.ref)) {
1571
+ const retireAt = lifecycleRetireAtForSignal(subscription.cleanupPolicy, signalOccurredAtMs);
1572
+ if (!retireAt) {
1573
+ continue;
1574
+ }
1575
+ const now = (0, util_1.nowIso)();
1576
+ this.store.upsertSubscriptionLifecycleRetirement({
1577
+ subscriptionId: subscription.subscriptionId,
1578
+ hostId: source.hostId,
1579
+ trackedResourceRef: signal.ref,
1580
+ retireAt,
1581
+ terminalState: signal.state ?? null,
1582
+ terminalResult: signal.result ?? null,
1583
+ terminalOccurredAt: signalOccurredAt,
1584
+ createdAt: now,
1585
+ updatedAt: now,
1586
+ });
1498
1587
  }
1499
- const now = (0, util_1.nowIso)();
1500
- this.store.upsertSubscriptionLifecycleRetirement({
1501
- subscriptionId: subscription.subscriptionId,
1502
- sourceId: source.sourceId,
1503
- trackedResourceRef: signal.ref,
1504
- retireAt,
1505
- terminalState: signal.state ?? null,
1506
- terminalResult: signal.result ?? null,
1507
- terminalOccurredAt: signalOccurredAt,
1508
- createdAt: now,
1509
- updatedAt: now,
1510
- });
1511
1588
  }
1512
1589
  notifyInboxWatchers(agentId, items) {
1513
1590
  const watchers = this.inboxWatchers.get(agentId);
@@ -4,6 +4,7 @@ exports.GithubDeliveryAdapter = exports.GithubUxcClient = exports.DEFAULT_GITHUB
4
4
  exports.githubDeliveryOperationsForHandle = githubDeliveryOperationsForHandle;
5
5
  exports.invokeGithubDeliveryOperation = invokeGithubDeliveryOperation;
6
6
  exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
7
+ exports.canonicalGithubPullRequestRef = canonicalGithubPullRequestRef;
7
8
  exports.deriveGithubTrackedResource = deriveGithubTrackedResource;
8
9
  exports.githubSubscriptionShortcutSpec = githubSubscriptionShortcutSpec;
9
10
  exports.expandGithubSubscriptionShortcut = expandGithubSubscriptionShortcut;
@@ -282,7 +283,11 @@ function normalizeGithubRepoEvent(source, config, raw) {
282
283
  deliveryHandle,
283
284
  };
284
285
  }
285
- function deriveGithubTrackedResource(filter) {
286
+ function canonicalGithubPullRequestRef(source, number) {
287
+ const config = parseGithubSourceConfig(source);
288
+ return `repo:${config.owner}/${config.repo}:pr:${number}`;
289
+ }
290
+ function deriveGithubTrackedResource(filter, source) {
286
291
  const metadata = asRecord(filter.metadata);
287
292
  const payload = asRecord(filter.payload);
288
293
  const number = asNumber(metadata.number) ?? asNumber(payload.number) ?? asNumber(payload.ref);
@@ -290,13 +295,16 @@ function deriveGithubTrackedResource(filter) {
290
295
  if (!number || isPullRequest !== true) {
291
296
  return null;
292
297
  }
293
- return { ref: `pr:${number}` };
298
+ return { ref: canonicalGithubPullRequestRef(source, number) };
294
299
  }
295
300
  function githubSubscriptionShortcutSpec() {
296
301
  return [{
297
302
  name: "pr",
298
303
  description: "Follow one pull request and auto-retire when it closes.",
299
- argsSchema: [{ name: "number", type: "number", required: true, description: "Pull request number." }],
304
+ argsSchema: [
305
+ { name: "number", type: "number", required: true, description: "Pull request number." },
306
+ { name: "withCi", type: "boolean", required: false, description: "Also create a sibling ci_runs subscription under the same host and stream key." },
307
+ ],
300
308
  }];
301
309
  }
302
310
  function expandGithubSubscriptionShortcut(input) {
@@ -307,18 +315,32 @@ function expandGithubSubscriptionShortcut(input) {
307
315
  if (!number || !Number.isInteger(number) || number <= 0) {
308
316
  throw new Error("subscription shortcut pr requires a positive integer number");
309
317
  }
318
+ const trackedResourceRef = canonicalGithubPullRequestRef(input.source, number);
319
+ const withCi = input.args?.withCi === true;
310
320
  return {
311
- filter: {
312
- metadata: {
313
- number,
314
- isPullRequest: true,
321
+ members: [
322
+ {
323
+ streamKind: "repo_events",
324
+ filter: {
325
+ metadata: {
326
+ number,
327
+ isPullRequest: true,
328
+ },
329
+ },
330
+ trackedResourceRef,
331
+ cleanupPolicy: { mode: "on_terminal" },
315
332
  },
316
- },
317
- trackedResourceRef: `pr:${number}`,
318
- cleanupPolicy: { mode: "on_terminal" },
333
+ ...(withCi
334
+ ? [{
335
+ streamKind: "ci_runs",
336
+ trackedResourceRef,
337
+ cleanupPolicy: { mode: "on_terminal" },
338
+ }]
339
+ : []),
340
+ ],
319
341
  };
320
342
  }
321
- function projectGithubLifecycleSignal(rawPayload) {
343
+ function projectGithubLifecycleSignal(rawPayload, source) {
322
344
  const eventType = asString(rawPayload.type);
323
345
  if (eventType !== "PullRequestEvent") {
324
346
  return null;
@@ -334,7 +356,7 @@ function projectGithubLifecycleSignal(rawPayload) {
334
356
  }
335
357
  const merged = action === "merged" || asBoolean(rawPayload.merged) === true || asBoolean(pullRequest.merged) === true;
336
358
  return {
337
- ref: `pr:${number}`,
359
+ ref: canonicalGithubPullRequestRef(source, number),
338
360
  terminal: true,
339
361
  state: "closed",
340
362
  result: merged ? "merged" : "closed",
@@ -185,11 +185,12 @@ class RemoteSourceRuntime {
185
185
  if (typeof module.expandSubscriptionShortcut !== "function") {
186
186
  return null;
187
187
  }
188
- return module.expandSubscriptionShortcut({
188
+ const expanded = module.expandSubscriptionShortcut({
189
189
  name: input.name,
190
190
  args: input.args,
191
191
  source: moduleInputSource(source),
192
192
  });
193
+ return normalizeExpandedSubscriptionPlan(expanded);
193
194
  }
194
195
  async deriveInlinePreview(source, item) {
195
196
  if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
@@ -368,6 +369,32 @@ class RemoteSourceRuntime {
368
369
  }
369
370
  }
370
371
  exports.RemoteSourceRuntime = RemoteSourceRuntime;
372
+ function normalizeExpandedSubscriptionPlan(value) {
373
+ if (!value) {
374
+ return null;
375
+ }
376
+ if (isExpandedSubscriptionPlan(value)) {
377
+ return {
378
+ members: value.members.map((member) => ({
379
+ streamKind: typeof member.streamKind === "string" ? member.streamKind : null,
380
+ filter: member.filter,
381
+ trackedResourceRef: member.trackedResourceRef ?? null,
382
+ cleanupPolicy: member.cleanupPolicy ?? null,
383
+ })),
384
+ };
385
+ }
386
+ return {
387
+ members: [{
388
+ streamKind: null,
389
+ filter: value.filter,
390
+ trackedResourceRef: value.trackedResourceRef ?? null,
391
+ cleanupPolicy: value.cleanupPolicy ?? null,
392
+ }],
393
+ };
394
+ }
395
+ function isExpandedSubscriptionPlan(value) {
396
+ return "members" in value && Array.isArray(value.members);
397
+ }
371
398
  function moduleInputSource(source) {
372
399
  if (source.sourceType !== "remote_source") {
373
400
  return source;
@@ -200,8 +200,8 @@ const GITHUB_REPO_MODULE = {
200
200
  expandSubscriptionShortcut(input) {
201
201
  return (0, github_1.expandGithubSubscriptionShortcut)(input);
202
202
  },
203
- deriveTrackedResource(filter) {
204
- return (0, github_1.deriveGithubTrackedResource)(filter);
203
+ deriveTrackedResource(filter, source) {
204
+ return (0, github_1.deriveGithubTrackedResource)(filter, source);
205
205
  },
206
206
  deriveNotificationGrouping(item) {
207
207
  const number = typeof item.metadata.number === "number" ? item.metadata.number : null;
@@ -251,8 +251,8 @@ const GITHUB_REPO_MODULE = {
251
251
  const count = items.length;
252
252
  return `${count} ${grouping.summaryHint}`;
253
253
  },
254
- projectLifecycleSignal(rawPayload) {
255
- return (0, github_1.projectGithubLifecycleSignal)(rawPayload);
254
+ projectLifecycleSignal(rawPayload, source) {
255
+ return (0, github_1.projectGithubLifecycleSignal)(rawPayload, source);
256
256
  },
257
257
  validateConfig(source) {
258
258
  (0, github_1.parseGithubSourceConfig)(source);
package/dist/src/store.js CHANGED
@@ -595,6 +595,16 @@ class AgentInboxStore {
595
595
  const rows = this.getAll("select * from subscriptions where agent_id = ? order by created_at asc", [agentId]);
596
596
  return rows.map((row) => this.mapSubscription(row));
597
597
  }
598
+ listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
599
+ const rows = this.getAll(`
600
+ select subscriptions.*
601
+ from subscriptions
602
+ inner join sources on sources.source_id = subscriptions.source_id
603
+ where sources.host_id = ? and subscriptions.tracked_resource_ref = ?
604
+ order by subscriptions.created_at asc
605
+ `, [hostId, trackedResourceRef]);
606
+ return rows.map((row) => this.mapSubscription(row));
607
+ }
598
608
  deleteSubscription(subscriptionId) {
599
609
  const subscription = this.getSubscription(subscriptionId);
600
610
  if (!subscription) {
@@ -628,10 +638,10 @@ class AgentInboxStore {
628
638
  : retirement;
629
639
  this.db.run(`
630
640
  insert into subscription_lifecycle_retirements (
631
- subscription_id, source_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
641
+ subscription_id, host_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
632
642
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
633
643
  on conflict(subscription_id) do update set
634
- source_id = excluded.source_id,
644
+ host_id = excluded.host_id,
635
645
  tracked_resource_ref = excluded.tracked_resource_ref,
636
646
  retire_at = excluded.retire_at,
637
647
  terminal_state = excluded.terminal_state,
@@ -641,7 +651,7 @@ class AgentInboxStore {
641
651
  updated_at = excluded.updated_at
642
652
  `, [
643
653
  next.subscriptionId,
644
- next.sourceId,
654
+ next.hostId,
645
655
  next.trackedResourceRef,
646
656
  next.retireAt,
647
657
  next.terminalState ?? null,
@@ -1673,7 +1683,7 @@ class AgentInboxStore {
1673
1683
  mapSubscriptionLifecycleRetirement(row) {
1674
1684
  return {
1675
1685
  subscriptionId: String(row.subscription_id),
1676
- sourceId: String(row.source_id),
1686
+ hostId: String(row.host_id),
1677
1687
  trackedResourceRef: String(row.tracked_resource_ref),
1678
1688
  retireAt: String(row.retire_at),
1679
1689
  terminalState: row.terminal_state ? String(row.terminal_state) : null,
@@ -0,0 +1,42 @@
1
+ pragma foreign_keys=off;
2
+
3
+ create table if not exists subscription_lifecycle_retirements__new (
4
+ subscription_id text primary key,
5
+ host_id text not null,
6
+ tracked_resource_ref text not null,
7
+ retire_at text not null,
8
+ terminal_state text,
9
+ terminal_result text,
10
+ terminal_occurred_at text not null,
11
+ created_at text not null,
12
+ updated_at text not null
13
+ );
14
+
15
+ insert into subscription_lifecycle_retirements__new (
16
+ subscription_id,
17
+ host_id,
18
+ tracked_resource_ref,
19
+ retire_at,
20
+ terminal_state,
21
+ terminal_result,
22
+ terminal_occurred_at,
23
+ created_at,
24
+ updated_at
25
+ )
26
+ select
27
+ retirement.subscription_id,
28
+ coalesce(source.host_id, retirement.source_id),
29
+ retirement.tracked_resource_ref,
30
+ retirement.retire_at,
31
+ retirement.terminal_state,
32
+ retirement.terminal_result,
33
+ retirement.terminal_occurred_at,
34
+ retirement.created_at,
35
+ retirement.updated_at
36
+ from subscription_lifecycle_retirements as retirement
37
+ left join sources as source on source.source_id = retirement.source_id;
38
+
39
+ drop table subscription_lifecycle_retirements;
40
+ alter table subscription_lifecycle_retirements__new rename to subscription_lifecycle_retirements;
41
+
42
+ pragma foreign_keys=on;
@@ -0,0 +1,5 @@
1
+ create index if not exists idx_subscriptions_tracked_resource_ref
2
+ on subscriptions(tracked_resource_ref);
3
+
4
+ create index if not exists idx_subscriptions_tracked_resource_source
5
+ on subscriptions(tracked_resource_ref, source_id);
package/drizzle/schema.ts CHANGED
@@ -97,11 +97,14 @@ export const subscriptions = sqliteTable("subscriptions", {
97
97
  startOffset: integer("start_offset"),
98
98
  startTime: text("start_time"),
99
99
  createdAt: text("created_at").notNull(),
100
- });
100
+ }, (table) => ({
101
+ trackedResourceRefIdx: index("idx_subscriptions_tracked_resource_ref").on(table.trackedResourceRef),
102
+ trackedResourceRefSourceIdx: index("idx_subscriptions_tracked_resource_source").on(table.trackedResourceRef, table.sourceId),
103
+ }));
101
104
 
102
105
  export const subscriptionLifecycleRetirements = sqliteTable("subscription_lifecycle_retirements", {
103
106
  subscriptionId: text("subscription_id").primaryKey(),
104
- sourceId: text("source_id").notNull(),
107
+ hostId: text("host_id").notNull(),
105
108
  trackedResourceRef: text("tracked_resource_ref").notNull(),
106
109
  retireAt: text("retire_at").notNull(),
107
110
  terminalState: text("terminal_state"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {