@holon-run/agentinbox 0.1.4 → 0.2.0

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.
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveSourceIdentity = resolveSourceIdentity;
4
+ exports.resolveSourceSchema = resolveSourceSchema;
5
+ exports.withResolvedIdentity = withResolvedIdentity;
6
+ const paths_1 = require("./paths");
7
+ const remote_profiles_1 = require("./sources/remote_profiles");
8
+ const source_schema_1 = require("./source_schema");
9
+ async function resolveSourceIdentity(source, context = {}) {
10
+ if (source.sourceType === "local_event") {
11
+ return {
12
+ hostType: "local_event",
13
+ sourceKind: "local_event",
14
+ implementationId: "builtin.local_event",
15
+ };
16
+ }
17
+ const builtinImplementationId = (0, remote_profiles_1.builtInProfileIdForSourceType)(source.sourceType);
18
+ if (builtinImplementationId) {
19
+ return {
20
+ hostType: "remote_source",
21
+ sourceKind: source.sourceType,
22
+ implementationId: builtinImplementationId,
23
+ };
24
+ }
25
+ if (source.sourceType !== "remote_source") {
26
+ throw new Error(`unsupported source type for source resolution: ${source.sourceType}`);
27
+ }
28
+ const profileRegistry = context.profileRegistry ?? new remote_profiles_1.RemoteSourceProfileRegistry();
29
+ const homeDir = context.homeDir ?? (0, paths_1.resolveAgentInboxHome)(process.env);
30
+ const profile = await profileRegistry.resolve(source, homeDir);
31
+ const capabilityDescription = typeof profile.describeCapabilities === "function"
32
+ ? profile.describeCapabilities(profileInputSource(source))
33
+ : undefined;
34
+ return {
35
+ hostType: "remote_source",
36
+ sourceKind: capabilityDescription?.sourceKind?.trim() || `remote:${profile.id}`,
37
+ implementationId: profile.id,
38
+ };
39
+ }
40
+ async function resolveSourceSchema(source, context = {}) {
41
+ const profileRegistry = context.profileRegistry ?? new remote_profiles_1.RemoteSourceProfileRegistry();
42
+ const homeDir = context.homeDir ?? (0, paths_1.resolveAgentInboxHome)(process.env);
43
+ const resolvedContext = { ...context, profileRegistry, homeDir };
44
+ const identity = await resolveSourceIdentity(source, resolvedContext);
45
+ const staticSchema = (0, source_schema_1.getSourceSchema)(source.sourceType);
46
+ let capabilityDescription;
47
+ let profile = null;
48
+ if (identity.hostType === "remote_source") {
49
+ profile = await profileRegistry.resolve(source, homeDir);
50
+ capabilityDescription = typeof profile.describeCapabilities === "function"
51
+ ? profile.describeCapabilities(profileInputSource(source))
52
+ : undefined;
53
+ }
54
+ return withResolvedIdentity(source.sourceId, {
55
+ sourceType: staticSchema.sourceType,
56
+ metadataFields: capabilityDescription?.metadataFields ?? staticSchema.metadataFields,
57
+ payloadExamples: capabilityDescription?.payloadExamples ?? staticSchema.payloadExamples,
58
+ eventVariantExamples: capabilityDescription?.eventVariantExamples ?? staticSchema.eventVariantExamples,
59
+ configFields: capabilityDescription?.configSchema ?? staticSchema.configFields,
60
+ }, identity, identity.hostType === "remote_source"
61
+ ? {
62
+ aliases: capabilityDescription?.aliases,
63
+ supportsTrackedResourceRef: typeof profile?.deriveTrackedResource === "function",
64
+ supportsLifecycleSignals: typeof profile?.projectLifecycleSignal === "function",
65
+ shortcuts: typeof profile?.listSubscriptionShortcuts === "function"
66
+ ? profile.listSubscriptionShortcuts(profileInputSource(source))
67
+ : [],
68
+ }
69
+ : undefined);
70
+ }
71
+ function withResolvedIdentity(sourceId, schema, identity, capabilityMetadata) {
72
+ return {
73
+ sourceId,
74
+ sourceType: schema.sourceType,
75
+ metadataFields: schema.metadataFields,
76
+ payloadExamples: schema.payloadExamples,
77
+ eventVariantExamples: schema.eventVariantExamples,
78
+ configFields: schema.configFields,
79
+ hostType: identity.hostType,
80
+ sourceKind: identity.sourceKind,
81
+ implementationId: identity.implementationId,
82
+ ...(capabilityMetadata?.aliases?.length ? { aliases: capabilityMetadata.aliases } : {}),
83
+ ...(capabilityMetadata ? {
84
+ subscriptionSchema: {
85
+ supportsTrackedResourceRef: capabilityMetadata.supportsTrackedResourceRef ?? false,
86
+ supportsLifecycleSignals: capabilityMetadata.supportsLifecycleSignals ?? false,
87
+ shortcuts: capabilityMetadata.shortcuts ?? [],
88
+ },
89
+ } : {}),
90
+ };
91
+ }
92
+ function profileInputSource(source) {
93
+ if (source.sourceType !== "remote_source") {
94
+ return source;
95
+ }
96
+ return {
97
+ ...source,
98
+ config: (0, remote_profiles_1.profileConfigForSource)(source),
99
+ };
100
+ }
@@ -32,6 +32,7 @@ const SOURCE_SCHEMAS = {
32
32
  { name: "action", type: "string", description: "GitHub event action suffix such as created." },
33
33
  { name: "author", type: "string|null", description: "Actor login for the event." },
34
34
  { name: "isPullRequest", type: "boolean", description: "Whether the event targets a pull request surface." },
35
+ { name: "reviewState", type: "string|null", description: "Review decision state for PullRequestReviewEvent such as approved or changes_requested." },
35
36
  { name: "labels", type: "string[]", description: "Labels extracted from the issue or pull request." },
36
37
  { name: "mentions", type: "string[]", description: "Mention handles extracted from title/body/comment text." },
37
38
  { name: "number", type: "number|null", description: "Issue or pull request number when present." },
@@ -49,8 +50,29 @@ const SOURCE_SCHEMAS = {
49
50
  issue: { number: 12, title: "Track filtering work" },
50
51
  comment: { body: "@alpha please look" },
51
52
  },
53
+ {
54
+ id: "1234567891",
55
+ type: "PullRequestReviewEvent",
56
+ action: "created",
57
+ actor: "Copilot",
58
+ pull_request: { number: 67, title: "feat: add remote module capability hooks" },
59
+ review: { state: "commented", body: "review summary" },
60
+ },
61
+ {
62
+ id: "1234567892",
63
+ type: "PullRequestEvent",
64
+ action: "closed",
65
+ actor: "jolestar",
66
+ pull_request: { number: 72, title: "feat: add cleanup policy lifecycle engine", merged: true },
67
+ },
68
+ ],
69
+ eventVariantExamples: [
70
+ "IssueCommentEvent.created",
71
+ "PullRequestEvent.opened",
72
+ "PullRequestEvent.closed",
73
+ "PullRequestReviewEvent.created",
74
+ "PullRequestReviewCommentEvent.created",
52
75
  ],
53
- eventVariantExamples: ["IssueCommentEvent.created", "PullRequestEvent.opened", "PullRequestReviewCommentEvent.created"],
54
76
  configFields: [
55
77
  { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
56
78
  { name: "repo", type: "string", required: true, description: "GitHub repository name." },
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GithubDeliveryAdapter = exports.GithubUxcClient = exports.DEFAULT_GITHUB_EVENT_TYPES = exports.GITHUB_ENDPOINT = void 0;
4
4
  exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
5
+ exports.deriveGithubTrackedResource = deriveGithubTrackedResource;
6
+ exports.githubSubscriptionShortcutSpec = githubSubscriptionShortcutSpec;
7
+ exports.expandGithubSubscriptionShortcut = expandGithubSubscriptionShortcut;
8
+ exports.projectGithubLifecycleSignal = projectGithubLifecycleSignal;
5
9
  exports.parseGithubSourceConfig = parseGithubSourceConfig;
6
10
  const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
7
11
  exports.GITHUB_ENDPOINT = "https://api.github.com";
@@ -9,6 +13,7 @@ exports.DEFAULT_GITHUB_EVENT_TYPES = [
9
13
  "IssuesEvent",
10
14
  "IssueCommentEvent",
11
15
  "PullRequestEvent",
16
+ "PullRequestReviewEvent",
12
17
  "PullRequestReviewCommentEvent",
13
18
  ];
14
19
  class GithubUxcClient {
@@ -97,15 +102,18 @@ function normalizeGithubRepoEvent(source, config, raw) {
97
102
  const repo = asRecord(event.repo);
98
103
  const issue = asRecord(payload.issue);
99
104
  const pullRequest = asRecord(payload.pull_request);
105
+ const review = asRecord(payload.review);
100
106
  const comment = asRecord(payload.comment);
101
107
  const action = asString(payload.action) ?? "observed";
102
108
  const number = asNumber(issue.number) ?? asNumber(pullRequest.number);
103
109
  const isPullRequest = eventType.startsWith("PullRequest");
104
110
  const title = asString(issue.title) ?? asString(pullRequest.title) ?? null;
105
- const body = asString(comment.body) ?? asString(issue.body) ?? asString(pullRequest.body) ?? null;
111
+ const reviewState = asString(review.state) ?? null;
112
+ const body = asString(comment.body) ?? asString(review.body) ?? asString(issue.body) ?? asString(pullRequest.body) ?? null;
106
113
  const labels = extractLabels(issue.labels);
107
114
  const mentions = extractMentions(title, body);
108
115
  const url = asString(comment.html_url) ??
116
+ asString(review.html_url) ??
109
117
  asString(issue.html_url) ??
110
118
  asString(pullRequest.html_url) ??
111
119
  asString(event.url);
@@ -125,6 +133,7 @@ function normalizeGithubRepoEvent(source, config, raw) {
125
133
  number,
126
134
  author: asString(actor.login) ?? null,
127
135
  isPullRequest,
136
+ reviewState,
128
137
  labels,
129
138
  mentions,
130
139
  title,
@@ -138,11 +147,71 @@ function normalizeGithubRepoEvent(source, config, raw) {
138
147
  actor: actor.login ?? null,
139
148
  issue,
140
149
  pull_request: pullRequest,
150
+ review,
141
151
  comment,
152
+ merged: asBoolean(pullRequest.merged),
142
153
  },
143
154
  deliveryHandle,
144
155
  };
145
156
  }
157
+ function deriveGithubTrackedResource(filter) {
158
+ const metadata = asRecord(filter.metadata);
159
+ const payload = asRecord(filter.payload);
160
+ const number = asNumber(metadata.number) ?? asNumber(payload.number) ?? asNumber(payload.ref);
161
+ const isPullRequest = asBoolean(metadata.isPullRequest);
162
+ if (!number || isPullRequest !== true) {
163
+ return null;
164
+ }
165
+ return { ref: `pr:${number}` };
166
+ }
167
+ function githubSubscriptionShortcutSpec() {
168
+ return [{
169
+ name: "pr",
170
+ description: "Follow one pull request and auto-retire when it closes.",
171
+ argsSchema: [{ name: "number", type: "number", required: true, description: "Pull request number." }],
172
+ }];
173
+ }
174
+ function expandGithubSubscriptionShortcut(input) {
175
+ if (input.name !== "pr") {
176
+ return null;
177
+ }
178
+ const number = typeof input.args?.number === "number" ? input.args.number : null;
179
+ if (!number || !Number.isInteger(number) || number <= 0) {
180
+ throw new Error("subscription shortcut pr requires a positive integer number");
181
+ }
182
+ return {
183
+ filter: {
184
+ metadata: {
185
+ number,
186
+ isPullRequest: true,
187
+ },
188
+ },
189
+ trackedResourceRef: `pr:${number}`,
190
+ cleanupPolicy: { mode: "on_terminal" },
191
+ };
192
+ }
193
+ function projectGithubLifecycleSignal(rawPayload) {
194
+ const eventType = asString(rawPayload.type);
195
+ if (eventType !== "PullRequestEvent") {
196
+ return null;
197
+ }
198
+ const action = asString(rawPayload.action);
199
+ if (action !== "closed" && action !== "merged") {
200
+ return null;
201
+ }
202
+ const pullRequest = asRecord(rawPayload.pull_request);
203
+ const number = asNumber(pullRequest.number);
204
+ if (!number) {
205
+ return null;
206
+ }
207
+ const merged = action === "merged" || asBoolean(rawPayload.merged) === true || asBoolean(pullRequest.merged) === true;
208
+ return {
209
+ ref: `pr:${number}`,
210
+ terminal: true,
211
+ state: "closed",
212
+ result: merged ? "merged" : "closed",
213
+ };
214
+ }
146
215
  function buildGithubDeliveryHandle(config, eventType, number, comment) {
147
216
  if (!number) {
148
217
  return null;
@@ -243,6 +312,9 @@ function asString(value) {
243
312
  function asNumber(value) {
244
313
  return typeof value === "number" ? value : null;
245
314
  }
315
+ function asBoolean(value) {
316
+ return typeof value === "boolean" ? value : null;
317
+ }
246
318
  function asStringArray(value) {
247
319
  if (!Array.isArray(value)) {
248
320
  return null;
@@ -167,6 +167,30 @@ class RemoteSourceRuntime {
167
167
  erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
168
168
  };
169
169
  }
170
+ async projectLifecycleSignal(source, rawPayload) {
171
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
172
+ return null;
173
+ }
174
+ const profile = await this.profileRegistry.resolve(source, this.homeDir);
175
+ if (typeof profile.projectLifecycleSignal !== "function") {
176
+ return null;
177
+ }
178
+ return profile.projectLifecycleSignal(rawPayload, profileInputSource(source));
179
+ }
180
+ async expandSubscriptionShortcut(source, input) {
181
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
182
+ return null;
183
+ }
184
+ const profile = await this.profileRegistry.resolve(source, this.homeDir);
185
+ if (typeof profile.expandSubscriptionShortcut !== "function") {
186
+ return null;
187
+ }
188
+ return profile.expandSubscriptionShortcut({
189
+ name: input.name,
190
+ args: input.args,
191
+ source: profileInputSource(source),
192
+ });
193
+ }
170
194
  async syncAll() {
171
195
  const sources = this.store
172
196
  .listSources()
@@ -90,6 +90,16 @@ function validateProfileContract(profile, sourcePath) {
90
90
  if (typeof profile.mapRawEvent !== "function") {
91
91
  throw new Error(`remote_source profile missing mapRawEvent(): ${sourcePath}`);
92
92
  }
93
+ validateOptionalHook(profile.describeCapabilities, "describeCapabilities", sourcePath);
94
+ validateOptionalHook(profile.listSubscriptionShortcuts, "listSubscriptionShortcuts", sourcePath);
95
+ validateOptionalHook(profile.expandSubscriptionShortcut, "expandSubscriptionShortcut", sourcePath);
96
+ validateOptionalHook(profile.deriveTrackedResource, "deriveTrackedResource", sourcePath);
97
+ validateOptionalHook(profile.projectLifecycleSignal, "projectLifecycleSignal", sourcePath);
98
+ }
99
+ function validateOptionalHook(value, name, sourcePath) {
100
+ if (value !== undefined && typeof value !== "function") {
101
+ throw new Error(`remote_source profile ${name} must be a function when provided: ${sourcePath}`);
102
+ }
93
103
  }
94
104
  function resolveAndValidateProfilePath(profileRoot, profilePath) {
95
105
  const resolved = node_path_1.default.isAbsolute(profilePath)
@@ -120,6 +130,71 @@ function asNonEmptyString(value) {
120
130
  }
121
131
  const GITHUB_REPO_PROFILE = {
122
132
  id: "builtin.github_repo",
133
+ describeCapabilities(source) {
134
+ const config = (0, github_1.parseGithubSourceConfig)(source);
135
+ return {
136
+ sourceKind: "github_repo",
137
+ aliases: ["github_repo"],
138
+ configSchema: [
139
+ { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
140
+ { name: "repo", type: "string", required: true, description: "GitHub repository name." },
141
+ { name: "uxcAuth", type: "string", required: false, description: "Optional uxc auth profile." },
142
+ { name: "eventTypes", type: "string[]", required: false, description: "Optional GitHub event type allowlist." },
143
+ { name: "perPage", type: "number", required: false, description: `Repository events requested per poll. Default ${config.perPage ?? 10}.` },
144
+ { name: "pollIntervalSecs", type: "number", required: false, description: `Polling interval in seconds. Default ${config.pollIntervalSecs ?? 30}.` },
145
+ ],
146
+ metadataFields: [
147
+ { name: "eventType", type: "string", description: "GitHub event type such as IssueCommentEvent." },
148
+ { name: "action", type: "string", description: "GitHub event action suffix such as created." },
149
+ { name: "author", type: "string|null", description: "Actor login for the event." },
150
+ { name: "isPullRequest", type: "boolean", description: "Whether the event targets a pull request surface." },
151
+ { name: "reviewState", type: "string|null", description: "Review decision state for PullRequestReviewEvent such as approved or changes_requested." },
152
+ { name: "labels", type: "string[]", description: "Labels extracted from the issue or pull request." },
153
+ { name: "mentions", type: "string[]", description: "Mention handles extracted from title/body/comment text." },
154
+ { name: "number", type: "number|null", description: "Issue or pull request number when present." },
155
+ { name: "repoFullName", type: "string", description: "Repository full name in owner/repo form." },
156
+ { name: "title", type: "string|null", description: "Issue, pull request, or comment title." },
157
+ { name: "body", type: "string|null", description: "Issue, pull request, or comment body text." },
158
+ { name: "url", type: "string|null", description: "Primary GitHub HTML URL for the event target." },
159
+ ],
160
+ payloadExamples: [
161
+ {
162
+ id: "1234567891",
163
+ type: "PullRequestReviewEvent",
164
+ action: "created",
165
+ actor: "Copilot",
166
+ pull_request: { number: 67, title: "feat: add remote module capability hooks" },
167
+ review: { state: "commented", body: "review summary" },
168
+ },
169
+ {
170
+ id: "1234567892",
171
+ type: "PullRequestEvent",
172
+ action: "closed",
173
+ actor: "jolestar",
174
+ pull_request: { number: 72, title: "feat: add cleanup policy lifecycle engine", merged: true },
175
+ },
176
+ ],
177
+ eventVariantExamples: [
178
+ "IssueCommentEvent.created",
179
+ "PullRequestEvent.opened",
180
+ "PullRequestEvent.closed",
181
+ "PullRequestReviewEvent.created",
182
+ "PullRequestReviewCommentEvent.created",
183
+ ],
184
+ };
185
+ },
186
+ listSubscriptionShortcuts() {
187
+ return (0, github_1.githubSubscriptionShortcutSpec)();
188
+ },
189
+ expandSubscriptionShortcut(input) {
190
+ return (0, github_1.expandGithubSubscriptionShortcut)(input);
191
+ },
192
+ deriveTrackedResource(filter) {
193
+ return (0, github_1.deriveGithubTrackedResource)(filter);
194
+ },
195
+ projectLifecycleSignal(rawPayload) {
196
+ return (0, github_1.projectGithubLifecycleSignal)(rawPayload);
197
+ },
123
198
  validateConfig(source) {
124
199
  (0, github_1.parseGithubSourceConfig)(source);
125
200
  },
@@ -164,6 +239,34 @@ const GITHUB_REPO_PROFILE = {
164
239
  };
165
240
  const GITHUB_REPO_CI_PROFILE = {
166
241
  id: "builtin.github_repo_ci",
242
+ describeCapabilities() {
243
+ return {
244
+ sourceKind: "github_repo_ci",
245
+ aliases: ["github_repo_ci"],
246
+ configSchema: [
247
+ { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
248
+ { name: "repo", type: "string", required: true, description: "GitHub repository name." },
249
+ { name: "uxcAuth", type: "string", required: false, description: "Optional uxc auth profile." },
250
+ { name: "pollIntervalSecs", type: "number", required: false, description: "Polling interval in seconds." },
251
+ { name: "perPage", type: "number", required: false, description: "Workflow runs requested per poll." },
252
+ { name: "eventFilter", type: "string", required: false, description: "Optional GitHub workflow event filter." },
253
+ { name: "branch", type: "string", required: false, description: "Optional branch filter for workflow runs." },
254
+ { name: "statusFilter", type: "string", required: false, description: "Optional workflow status filter." },
255
+ ],
256
+ metadataFields: [
257
+ { name: "name", type: "string|null", description: "Workflow run name." },
258
+ { name: "status", type: "string", description: "Normalized workflow run status." },
259
+ { name: "conclusion", type: "string|null", description: "Workflow run conclusion when completed." },
260
+ { name: "event", type: "string|null", description: "GitHub trigger event for the workflow run." },
261
+ { name: "headBranch", type: "string|null", description: "Head branch for the workflow run." },
262
+ { name: "headSha", type: "string|null", description: "Head commit SHA for the workflow run." },
263
+ { name: "actor", type: "string|null", description: "Actor login for the workflow run." },
264
+ { name: "commitMessage", type: "string|null", description: "Head commit message when present." },
265
+ { name: "htmlUrl", type: "string|null", description: "GitHub Actions run URL." },
266
+ ],
267
+ eventVariantExamples: ["workflow_run.ci.completed.failure", "workflow_run.nightly_checks.observed"],
268
+ };
269
+ },
167
270
  validateConfig(source) {
168
271
  (0, github_ci_1.parseGithubCiSourceConfig)(source);
169
272
  },
@@ -217,6 +320,36 @@ const GITHUB_REPO_CI_PROFILE = {
217
320
  };
218
321
  const FEISHU_BOT_PROFILE = {
219
322
  id: "builtin.feishu_bot",
323
+ describeCapabilities() {
324
+ return {
325
+ sourceKind: "feishu_bot",
326
+ aliases: ["feishu_bot"],
327
+ configSchema: [
328
+ { name: "appId", type: "string", required: true, description: "Feishu app ID." },
329
+ { name: "appSecret", type: "string", required: true, description: "Feishu app secret." },
330
+ { name: "eventTypes", type: "string[]", required: false, description: "Optional Feishu event type allowlist." },
331
+ { name: "chatIds", type: "string[]", required: false, description: "Optional Feishu chat allowlist." },
332
+ { name: "schemaUrl", type: "string", required: false, description: "Optional Feishu OpenAPI schema URL." },
333
+ { name: "replyInThread", type: "boolean", required: false, description: "Reply in thread when sending outbound messages." },
334
+ { name: "uxcAuth", type: "string", required: false, description: "Optional uxc auth profile." },
335
+ ],
336
+ metadataFields: [
337
+ { name: "eventType", type: "string", description: "Feishu event type." },
338
+ { name: "chatId", type: "string", description: "Target chat ID." },
339
+ { name: "chatType", type: "string|null", description: "Feishu chat type." },
340
+ { name: "messageId", type: "string", description: "Feishu message ID." },
341
+ { name: "messageType", type: "string", description: "Feishu message type such as text." },
342
+ { name: "senderOpenId", type: "string|null", description: "Sender open_id when present." },
343
+ { name: "senderType", type: "string|null", description: "Sender type." },
344
+ { name: "mentions", type: "string[]", description: "Mention names extracted from the message." },
345
+ { name: "mentionOpenIds", type: "string[]", description: "Mention open_ids extracted from the message." },
346
+ { name: "content", type: "string|null", description: "Normalized message content string." },
347
+ { name: "threadId", type: "string|null", description: "Thread or root message ID when present." },
348
+ { name: "parentId", type: "string|null", description: "Parent message ID when present." },
349
+ ],
350
+ eventVariantExamples: ["im.message.receive_v1.text"],
351
+ };
352
+ },
220
353
  validateConfig(source) {
221
354
  (0, feishu_1.parseFeishuSourceConfig)(source);
222
355
  },
package/dist/src/store.js CHANGED
@@ -16,6 +16,17 @@ function parseJson(value) {
16
16
  }
17
17
  return JSON.parse(value);
18
18
  }
19
+ function earlierLifecycleRetirement(left, right) {
20
+ const leftAt = Date.parse(left.retireAt);
21
+ const rightAt = Date.parse(right.retireAt);
22
+ if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt <= rightAt) {
23
+ return {
24
+ ...left,
25
+ updatedAt: right.updatedAt,
26
+ };
27
+ }
28
+ return right;
29
+ }
19
30
  class AgentInboxStore {
20
31
  dbPath;
21
32
  db;
@@ -193,6 +204,38 @@ class AgentInboxStore {
193
204
  const rows = this.getAll("select * from sources order by created_at asc");
194
205
  return rows.map((row) => this.mapSource(row));
195
206
  }
207
+ getSourceIdleState(sourceId) {
208
+ const row = this.getOne("select * from source_idle_states where source_id = ?", [sourceId]);
209
+ return row ? this.mapSourceIdleState(row) : null;
210
+ }
211
+ listSourceIdleStatesDue(cutoffIso) {
212
+ const rows = this.getAll("select * from source_idle_states where auto_pause_at <= ? and auto_paused_at is null order by auto_pause_at asc", [cutoffIso]);
213
+ return rows.map((row) => this.mapSourceIdleState(row));
214
+ }
215
+ upsertSourceIdleState(idleState) {
216
+ this.db.run(`
217
+ insert into source_idle_states (
218
+ source_id, idle_since, auto_pause_at, auto_paused_at, updated_at
219
+ ) values (?, ?, ?, ?, ?)
220
+ on conflict(source_id) do update set
221
+ idle_since = excluded.idle_since,
222
+ auto_pause_at = excluded.auto_pause_at,
223
+ auto_paused_at = excluded.auto_paused_at,
224
+ updated_at = excluded.updated_at
225
+ `, [
226
+ idleState.sourceId,
227
+ idleState.idleSince,
228
+ idleState.autoPauseAt,
229
+ idleState.autoPausedAt ?? null,
230
+ idleState.updatedAt,
231
+ ]);
232
+ this.persist();
233
+ return this.getSourceIdleState(idleState.sourceId);
234
+ }
235
+ deleteSourceIdleState(sourceId) {
236
+ this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
237
+ this.persist();
238
+ }
196
239
  updateSourceDefinition(sourceId, input) {
197
240
  const current = this.getSource(sourceId);
198
241
  if (!current) {
@@ -245,6 +288,7 @@ class AgentInboxStore {
245
288
  this.db.run("delete from stream_events where stream_id = ?", [stream.streamId]);
246
289
  this.db.run("delete from streams where stream_id = ?", [stream.streamId]);
247
290
  }
291
+ this.db.run("delete from source_idle_states where source_id = ?", [sourceId]);
248
292
  this.db.run("delete from sources where source_id = ?", [sourceId]);
249
293
  });
250
294
  this.persist();
@@ -319,15 +363,15 @@ class AgentInboxStore {
319
363
  insertSubscription(subscription) {
320
364
  this.db.run(`
321
365
  insert into subscriptions (
322
- subscription_id, agent_id, source_id, filter_json, lifecycle_mode, expires_at, start_policy, start_offset, start_time, created_at
366
+ subscription_id, agent_id, source_id, filter_json, tracked_resource_ref, cleanup_policy_json, start_policy, start_offset, start_time, created_at
323
367
  ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
324
368
  `, [
325
369
  subscription.subscriptionId,
326
370
  subscription.agentId,
327
371
  subscription.sourceId,
328
372
  JSON.stringify(subscription.filter),
329
- subscription.lifecycleMode,
330
- subscription.expiresAt ?? null,
373
+ subscription.trackedResourceRef ?? null,
374
+ JSON.stringify(subscription.cleanupPolicy),
331
375
  subscription.startPolicy,
332
376
  subscription.startOffset ?? null,
333
377
  subscription.startTime ?? null,
@@ -352,10 +396,63 @@ class AgentInboxStore {
352
396
  if (!subscription) {
353
397
  return null;
354
398
  }
355
- this.db.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
399
+ this.inTransaction(() => {
400
+ this.db.run("delete from consumer_commits where consumer_id in (select consumer_id from consumers where subscription_id = ?)", [subscriptionId]);
401
+ this.db.run("delete from consumers where subscription_id = ?", [subscriptionId]);
402
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
403
+ this.db.run("delete from subscriptions where subscription_id = ?", [subscriptionId]);
404
+ });
356
405
  this.persist();
357
406
  return subscription;
358
407
  }
408
+ getSubscriptionLifecycleRetirement(subscriptionId) {
409
+ const row = this.getOne("select * from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
410
+ return row ? this.mapSubscriptionLifecycleRetirement(row) : null;
411
+ }
412
+ listSubscriptionLifecycleRetirements() {
413
+ const rows = this.getAll("select * from subscription_lifecycle_retirements order by retire_at asc, created_at asc");
414
+ return rows.map((row) => this.mapSubscriptionLifecycleRetirement(row));
415
+ }
416
+ listSubscriptionLifecycleRetirementsDue(cutoffIso) {
417
+ const rows = this.getAll("select * from subscription_lifecycle_retirements where retire_at <= ? order by retire_at asc, created_at asc", [cutoffIso]);
418
+ return rows.map((row) => this.mapSubscriptionLifecycleRetirement(row));
419
+ }
420
+ upsertSubscriptionLifecycleRetirement(retirement) {
421
+ const existing = this.getSubscriptionLifecycleRetirement(retirement.subscriptionId);
422
+ const next = existing
423
+ ? earlierLifecycleRetirement(existing, retirement)
424
+ : retirement;
425
+ this.db.run(`
426
+ insert into subscription_lifecycle_retirements (
427
+ subscription_id, source_id, tracked_resource_ref, retire_at, terminal_state, terminal_result, terminal_occurred_at, created_at, updated_at
428
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
429
+ on conflict(subscription_id) do update set
430
+ source_id = excluded.source_id,
431
+ tracked_resource_ref = excluded.tracked_resource_ref,
432
+ retire_at = excluded.retire_at,
433
+ terminal_state = excluded.terminal_state,
434
+ terminal_result = excluded.terminal_result,
435
+ terminal_occurred_at = excluded.terminal_occurred_at,
436
+ created_at = excluded.created_at,
437
+ updated_at = excluded.updated_at
438
+ `, [
439
+ next.subscriptionId,
440
+ next.sourceId,
441
+ next.trackedResourceRef,
442
+ next.retireAt,
443
+ next.terminalState ?? null,
444
+ next.terminalResult ?? null,
445
+ next.terminalOccurredAt ?? null,
446
+ next.createdAt,
447
+ next.updatedAt,
448
+ ]);
449
+ this.persist();
450
+ return this.getSubscriptionLifecycleRetirement(retirement.subscriptionId);
451
+ }
452
+ deleteSubscriptionLifecycleRetirement(subscriptionId) {
453
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id = ?", [subscriptionId]);
454
+ this.persist();
455
+ }
359
456
  getActivationTarget(targetId) {
360
457
  const row = this.getOne("select * from activation_targets where target_id = ?", [targetId]);
361
458
  return row ? this.mapActivationTarget(row) : null;
@@ -507,6 +604,8 @@ class AgentInboxStore {
507
604
  ]);
508
605
  this.db.run("delete from consumers where subscription_id = ?", [subscription.subscriptionId]);
509
606
  }
607
+ this.db.run("delete from subscription_lifecycle_retirements where subscription_id in (select subscription_id from subscriptions where agent_id = ?)", [agentId]);
608
+ this.db.run("delete from source_idle_states where source_id in (select distinct source_id from subscriptions where agent_id = ?)", [agentId]);
510
609
  this.db.run("delete from subscriptions where agent_id = ?", [agentId]);
511
610
  this.db.run("delete from activation_dispatch_states where agent_id = ?", [agentId]);
512
611
  this.db.run("delete from activation_targets where agent_id = ?", [agentId]);
@@ -895,6 +994,8 @@ class AgentInboxStore {
895
994
  agents: this.count("agents"),
896
995
  offlineAgents: this.count("agents where status = 'offline'"),
897
996
  subscriptions: this.count("subscriptions"),
997
+ subscriptionLifecycleRetirements: this.count("subscription_lifecycle_retirements"),
998
+ sourceIdleStates: this.count("source_idle_states"),
898
999
  inboxes: this.count("inboxes"),
899
1000
  activationTargets: this.count("activation_targets"),
900
1001
  offlineActivationTargets: this.count("activation_targets where status = 'offline'"),
@@ -959,6 +1060,15 @@ class AgentInboxStore {
959
1060
  updatedAt: String(row.updated_at),
960
1061
  };
961
1062
  }
1063
+ mapSourceIdleState(row) {
1064
+ return {
1065
+ sourceId: String(row.source_id),
1066
+ idleSince: String(row.idle_since),
1067
+ autoPauseAt: String(row.auto_pause_at),
1068
+ autoPausedAt: row.auto_paused_at ? String(row.auto_paused_at) : null,
1069
+ updatedAt: String(row.updated_at),
1070
+ };
1071
+ }
962
1072
  mapAgent(row) {
963
1073
  return {
964
1074
  agentId: String(row.agent_id),
@@ -984,14 +1094,27 @@ class AgentInboxStore {
984
1094
  agentId: String(row.agent_id),
985
1095
  sourceId: String(row.source_id),
986
1096
  filter: parseJson(row.filter_json),
987
- lifecycleMode: row.lifecycle_mode,
988
- expiresAt: row.expires_at ? String(row.expires_at) : null,
1097
+ trackedResourceRef: row.tracked_resource_ref ? String(row.tracked_resource_ref) : null,
1098
+ cleanupPolicy: parseJson(row.cleanup_policy_json),
989
1099
  startPolicy: row.start_policy,
990
1100
  startOffset: row.start_offset != null ? Number(row.start_offset) : null,
991
1101
  startTime: row.start_time ? String(row.start_time) : null,
992
1102
  createdAt: String(row.created_at),
993
1103
  };
994
1104
  }
1105
+ mapSubscriptionLifecycleRetirement(row) {
1106
+ return {
1107
+ subscriptionId: String(row.subscription_id),
1108
+ sourceId: String(row.source_id),
1109
+ trackedResourceRef: String(row.tracked_resource_ref),
1110
+ retireAt: String(row.retire_at),
1111
+ terminalState: row.terminal_state ? String(row.terminal_state) : null,
1112
+ terminalResult: row.terminal_result ? String(row.terminal_result) : null,
1113
+ terminalOccurredAt: row.terminal_occurred_at ? String(row.terminal_occurred_at) : null,
1114
+ createdAt: String(row.created_at),
1115
+ updatedAt: String(row.updated_at),
1116
+ };
1117
+ }
995
1118
  mapActivationTarget(row) {
996
1119
  if (String(row.kind) === "webhook") {
997
1120
  const url = typeof row.url === "string" && row.url.trim().length > 0 ? row.url.trim() : null;