@holon-run/agentinbox 0.5.0 → 0.7.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.
@@ -1,387 +1,13 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.RemoteSourceProfileRegistry = void 0;
7
- exports.builtInProfileIdForSourceType = builtInProfileIdForSourceType;
8
- exports.profileConfigForSource = profileConfigForSource;
9
- const node_fs_1 = __importDefault(require("node:fs"));
10
- const node_path_1 = __importDefault(require("node:path"));
11
- const node_url_1 = require("node:url");
12
- const github_1 = require("./github");
13
- const github_ci_1 = require("./github_ci");
14
- const feishu_1 = require("./feishu");
15
- const REMOTE_USER_PROFILE_ROOT_DIR = "source-profiles";
16
- const BUILTIN_PROFILE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot"]);
17
- class RemoteSourceProfileRegistry {
18
- profileCache = new Map();
19
- resolve(source, homeDir) {
20
- if (source.sourceType === "github_repo") {
21
- return Promise.resolve(GITHUB_REPO_PROFILE);
22
- }
23
- if (source.sourceType === "github_repo_ci") {
24
- return Promise.resolve(GITHUB_REPO_CI_PROFILE);
25
- }
26
- if (source.sourceType === "feishu_bot") {
27
- return Promise.resolve(FEISHU_BOT_PROFILE);
28
- }
29
- if (source.sourceType !== "remote_source") {
30
- throw new Error(`unsupported source type for remote profile: ${source.sourceType}`);
31
- }
32
- return this.loadUserProfile(source, homeDir);
33
- }
34
- async loadUserProfile(source, homeDir) {
35
- const config = source.config ?? {};
36
- const profilePath = asNonEmptyString(config.profilePath);
37
- if (!profilePath) {
38
- throw new Error("remote_source requires config.profilePath");
39
- }
40
- const profileRoot = node_path_1.default.resolve(homeDir, REMOTE_USER_PROFILE_ROOT_DIR);
41
- const resolvedPath = resolveAndValidateProfilePath(profileRoot, profilePath);
42
- const cacheKey = `${resolvedPath}:${source.sourceKey}`;
43
- const cached = this.profileCache.get(cacheKey);
44
- if (cached) {
45
- return cached;
46
- }
47
- if (!node_fs_1.default.existsSync(resolvedPath)) {
48
- throw new Error(`remote_source profile not found: ${resolvedPath}`);
49
- }
50
- const imported = await dynamicImportModule((0, node_url_1.pathToFileURL)(resolvedPath).href);
51
- const profile = (imported.default ?? imported);
52
- if (!profile || typeof profile !== "object") {
53
- throw new Error(`remote_source profile must export default object: ${resolvedPath}`);
54
- }
55
- validateProfileContract(profile, resolvedPath);
56
- this.profileCache.set(cacheKey, profile);
57
- return profile;
58
- }
3
+ exports.profileConfigForSource = exports.builtInProfileIdForSourceType = exports.RemoteSourceProfileRegistry = exports.moduleConfigForSource = exports.builtInModuleIdForSourceType = exports.RemoteSourceModuleRegistry = void 0;
4
+ var remote_modules_1 = require("./remote_modules");
5
+ Object.defineProperty(exports, "RemoteSourceModuleRegistry", { enumerable: true, get: function () { return remote_modules_1.RemoteSourceModuleRegistry; } });
6
+ Object.defineProperty(exports, "builtInModuleIdForSourceType", { enumerable: true, get: function () { return remote_modules_1.builtInModuleIdForSourceType; } });
7
+ Object.defineProperty(exports, "moduleConfigForSource", { enumerable: true, get: function () { return remote_modules_1.moduleConfigForSource; } });
8
+ const remote_modules_2 = require("./remote_modules");
9
+ class RemoteSourceProfileRegistry extends remote_modules_2.RemoteSourceModuleRegistry {
59
10
  }
60
11
  exports.RemoteSourceProfileRegistry = RemoteSourceProfileRegistry;
61
- async function dynamicImportModule(moduleUrl) {
62
- const importer = new Function("url", "return import(url);");
63
- return importer(moduleUrl);
64
- }
65
- function builtInProfileIdForSourceType(sourceType) {
66
- if (sourceType === "github_repo") {
67
- return "builtin.github_repo";
68
- }
69
- if (sourceType === "github_repo_ci") {
70
- return "builtin.github_repo_ci";
71
- }
72
- if (sourceType === "feishu_bot") {
73
- return "builtin.feishu_bot";
74
- }
75
- return null;
76
- }
77
- function validateProfileContract(profile, sourcePath) {
78
- if (!profile.id || typeof profile.id !== "string") {
79
- throw new Error(`remote_source profile id must be a non-empty string: ${sourcePath}`);
80
- }
81
- if (BUILTIN_PROFILE_IDS.has(profile.id)) {
82
- throw new Error(`remote_source profile id is reserved: ${profile.id}`);
83
- }
84
- if (typeof profile.validateConfig !== "function") {
85
- throw new Error(`remote_source profile missing validateConfig(): ${sourcePath}`);
86
- }
87
- if (typeof profile.buildManagedSourceSpec !== "function") {
88
- throw new Error(`remote_source profile missing buildManagedSourceSpec(): ${sourcePath}`);
89
- }
90
- if (typeof profile.mapRawEvent !== "function") {
91
- throw new Error(`remote_source profile missing mapRawEvent(): ${sourcePath}`);
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
- }
103
- }
104
- function resolveAndValidateProfilePath(profileRoot, profilePath) {
105
- const resolved = node_path_1.default.isAbsolute(profilePath)
106
- ? node_path_1.default.resolve(profilePath)
107
- : node_path_1.default.resolve(profileRoot, profilePath);
108
- const normalizedRoot = ensureTrailingSep(node_path_1.default.resolve(profileRoot));
109
- const normalizedResolved = node_path_1.default.resolve(resolved);
110
- if (!normalizedResolved.startsWith(normalizedRoot) && normalizedResolved !== node_path_1.default.resolve(profileRoot)) {
111
- throw new Error(`remote_source profilePath must stay under ${profileRoot}`);
112
- }
113
- return normalizedResolved;
114
- }
115
- function ensureTrailingSep(input) {
116
- return input.endsWith(node_path_1.default.sep) ? input : `${input}${node_path_1.default.sep}`;
117
- }
118
- function asRecord(value) {
119
- if (!value || typeof value !== "object" || Array.isArray(value)) {
120
- return {};
121
- }
122
- return value;
123
- }
124
- function asNonEmptyString(value) {
125
- if (typeof value !== "string") {
126
- return null;
127
- }
128
- const trimmed = value.trim();
129
- return trimmed.length > 0 ? trimmed : null;
130
- }
131
- const GITHUB_REPO_PROFILE = {
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
- },
198
- validateConfig(source) {
199
- (0, github_1.parseGithubSourceConfig)(source);
200
- },
201
- buildManagedSourceSpec(source) {
202
- const config = (0, github_1.parseGithubSourceConfig)(source);
203
- return {
204
- endpoint: github_1.GITHUB_ENDPOINT,
205
- operation_id: "get:/repos/{owner}/{repo}/events",
206
- args: {
207
- owner: config.owner,
208
- repo: config.repo,
209
- per_page: config.perPage ?? 10,
210
- },
211
- mode: "poll",
212
- poll_config: {
213
- interval_secs: config.pollIntervalSecs ?? 30,
214
- extract_items_pointer: "",
215
- checkpoint_strategy: {
216
- type: "item_key",
217
- item_key_pointer: "/id",
218
- seen_window: 1024,
219
- },
220
- },
221
- options: { auth: config.uxcAuth },
222
- };
223
- },
224
- mapRawEvent(rawPayload, source) {
225
- const config = (0, github_1.parseGithubSourceConfig)(source);
226
- const normalized = (0, github_1.normalizeGithubRepoEvent)(source, config, rawPayload);
227
- if (!normalized) {
228
- return null;
229
- }
230
- return {
231
- sourceNativeId: normalized.sourceNativeId,
232
- eventVariant: normalized.eventVariant,
233
- metadata: normalized.metadata ?? {},
234
- rawPayload: normalized.rawPayload ?? rawPayload,
235
- occurredAt: normalized.occurredAt,
236
- deliveryHandle: normalized.deliveryHandle,
237
- };
238
- },
239
- };
240
- const GITHUB_REPO_CI_PROFILE = {
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
- },
270
- validateConfig(source) {
271
- (0, github_ci_1.parseGithubCiSourceConfig)(source);
272
- },
273
- buildManagedSourceSpec(source) {
274
- const config = (0, github_ci_1.parseGithubCiSourceConfig)(source);
275
- const args = {
276
- owner: config.owner,
277
- repo: config.repo,
278
- per_page: config.perPage ?? github_ci_1.DEFAULT_GITHUB_CI_PER_PAGE,
279
- };
280
- if (config.eventFilter) {
281
- args.event = config.eventFilter;
282
- }
283
- if (config.branch) {
284
- args.branch = config.branch;
285
- }
286
- if (config.statusFilter) {
287
- args.status = config.statusFilter;
288
- }
289
- return {
290
- endpoint: github_ci_1.GITHUB_CI_ENDPOINT,
291
- operation_id: "get:/repos/{owner}/{repo}/actions/runs",
292
- args,
293
- mode: "poll",
294
- poll_config: {
295
- interval_secs: config.pollIntervalSecs ?? github_ci_1.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS,
296
- extract_items_pointer: "/workflow_runs",
297
- checkpoint_strategy: {
298
- type: "content_hash",
299
- seen_window: 1024,
300
- },
301
- },
302
- options: { auth: config.uxcAuth },
303
- };
304
- },
305
- mapRawEvent(rawPayload, source) {
306
- const config = (0, github_ci_1.parseGithubCiSourceConfig)(source);
307
- const normalized = (0, github_ci_1.normalizeGithubWorkflowRunEvent)(source, config, rawPayload);
308
- if (!normalized) {
309
- return null;
310
- }
311
- return {
312
- sourceNativeId: normalized.sourceNativeId,
313
- eventVariant: normalized.eventVariant,
314
- metadata: normalized.metadata ?? {},
315
- rawPayload: normalized.rawPayload ?? rawPayload,
316
- occurredAt: normalized.occurredAt,
317
- deliveryHandle: normalized.deliveryHandle,
318
- };
319
- },
320
- };
321
- const FEISHU_BOT_PROFILE = {
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
- },
353
- validateConfig(source) {
354
- (0, feishu_1.parseFeishuSourceConfig)(source);
355
- },
356
- buildManagedSourceSpec(source) {
357
- const config = (0, feishu_1.parseFeishuSourceConfig)(source);
358
- return {
359
- endpoint: config.endpoint ?? feishu_1.FEISHU_OPENAPI_ENDPOINT,
360
- mode: "stream",
361
- transport_hint: "feishu_long_connection",
362
- options: { auth: config.uxcAuth },
363
- };
364
- },
365
- mapRawEvent(rawPayload, source) {
366
- const config = (0, feishu_1.parseFeishuSourceConfig)(source);
367
- const normalized = (0, feishu_1.normalizeFeishuBotEvent)(source, config, rawPayload);
368
- if (!normalized) {
369
- return null;
370
- }
371
- return {
372
- sourceNativeId: normalized.sourceNativeId,
373
- eventVariant: normalized.eventVariant,
374
- metadata: normalized.metadata ?? {},
375
- rawPayload: normalized.rawPayload ?? rawPayload,
376
- occurredAt: normalized.occurredAt,
377
- deliveryHandle: normalized.deliveryHandle,
378
- };
379
- },
380
- };
381
- function profileConfigForSource(source) {
382
- const config = asRecord(source.config);
383
- if (source.sourceType === "remote_source") {
384
- return asRecord(config.profileConfig);
385
- }
386
- return config;
387
- }
12
+ exports.builtInProfileIdForSourceType = remote_modules_2.builtInModuleIdForSourceType;
13
+ exports.profileConfigForSource = remote_modules_2.moduleConfigForSource;
package/dist/src/store.js CHANGED
@@ -780,6 +780,12 @@ class AgentInboxStore {
780
780
  ]);
781
781
  const inserted = this.changes() > before;
782
782
  if (inserted) {
783
+ const rowId = this.lastInsertRowId();
784
+ this.db.run(`
785
+ update inbox_items
786
+ set inbox_sequence = ?
787
+ where rowid = ? and inbox_sequence is null
788
+ `, [rowId, rowId]);
783
789
  this.persist();
784
790
  }
785
791
  return inserted;
@@ -792,14 +798,14 @@ class AgentInboxStore {
792
798
  filters.push("acked_at is null");
793
799
  }
794
800
  if (options?.afterItemId) {
795
- const anchor = this.getOne("select occurred_at, item_id from inbox_items where inbox_id = ? and item_id = ?", [inboxId, options.afterItemId]);
801
+ const anchor = this.getOne("select coalesce(inbox_sequence, rowid) as inbox_sequence from inbox_items where inbox_id = ? and item_id = ?", [inboxId, options.afterItemId]);
796
802
  if (!anchor) {
797
803
  throw new Error(`unknown inbox item: ${options.afterItemId}`);
798
804
  }
799
- filters.push("((occurred_at > ?) or (occurred_at = ? and item_id > ?))");
800
- params.push(String(anchor.occurred_at), String(anchor.occurred_at), String(anchor.item_id));
805
+ filters.push("coalesce(inbox_sequence, rowid) > ?");
806
+ params.push(Number(anchor.inbox_sequence));
801
807
  }
802
- const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by occurred_at asc, item_id asc`, params);
808
+ const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by coalesce(inbox_sequence, rowid) asc`, params);
803
809
  return rows.map((row) => this.mapInboxItem(row));
804
810
  }
805
811
  ackItems(inboxId, itemIds, ackedAt) {
@@ -818,7 +824,7 @@ class AgentInboxStore {
818
824
  return changes;
819
825
  }
820
826
  ackItemsThrough(inboxId, itemId, ackedAt) {
821
- const anchor = this.getOne("select occurred_at, item_id from inbox_items where inbox_id = ? and item_id = ?", [inboxId, itemId]);
827
+ const anchor = this.getOne("select coalesce(inbox_sequence, rowid) as inbox_sequence from inbox_items where inbox_id = ? and item_id = ?", [inboxId, itemId]);
822
828
  if (!anchor) {
823
829
  throw new Error(`unknown inbox item: ${itemId}`);
824
830
  }
@@ -827,8 +833,8 @@ class AgentInboxStore {
827
833
  set acked_at = ?
828
834
  where inbox_id = ?
829
835
  and acked_at is null
830
- and ((occurred_at < ?) or (occurred_at = ? and item_id <= ?))
831
- `, [ackedAt, inboxId, String(anchor.occurred_at), String(anchor.occurred_at), String(anchor.item_id)]);
836
+ and coalesce(inbox_sequence, rowid) <= ?
837
+ `, [ackedAt, inboxId, Number(anchor.inbox_sequence)]);
832
838
  const changes = this.changes();
833
839
  if (changes > 0) {
834
840
  this.persist();
@@ -7,6 +7,7 @@ exports.TerminalDispatcher = void 0;
7
7
  exports.detectTerminalContext = detectTerminalContext;
8
8
  exports.renderAgentPrompt = renderAgentPrompt;
9
9
  exports.deriveInlineItemPreview = deriveInlineItemPreview;
10
+ exports.normalizeInlinePreviewText = normalizeInlinePreviewText;
10
11
  exports.assignedAgentIdFromContext = assignedAgentIdFromContext;
11
12
  const node_child_process_1 = require("node:child_process");
12
13
  const node_util_1 = require("node:util");
@@ -78,13 +79,16 @@ function deriveInlineItemPreview(item, summary) {
78
79
  candidates.push(summary);
79
80
  }
80
81
  for (const candidate of candidates) {
81
- const preview = normalizeInlinePreview(candidate);
82
+ const preview = normalizeInlinePreviewText(candidate);
82
83
  if (preview) {
83
84
  return preview;
84
85
  }
85
86
  }
86
87
  return null;
87
88
  }
89
+ function normalizeInlinePreviewText(candidate) {
90
+ return normalizeInlinePreview(candidate);
91
+ }
88
92
  function assignedAgentIdFromContext(input) {
89
93
  const primary = normalizeOptionalString(input.runtimeSessionId)
90
94
  ?? normalizeOptionalString(input.tmuxPaneId)
@@ -0,0 +1,21 @@
1
+ alter table inbox_items add column inbox_sequence integer;
2
+
3
+ update inbox_items
4
+ set inbox_sequence = rowid
5
+ where inbox_sequence is null;
6
+
7
+ create unique index if not exists idx_inbox_items_inbox_sequence
8
+ on inbox_items(inbox_sequence);
9
+
10
+ create index if not exists idx_inbox_items_inbox_sequence_order
11
+ on inbox_items(inbox_id, inbox_sequence);
12
+
13
+ create trigger if not exists trg_inbox_items_set_sequence_after_insert
14
+ after insert on inbox_items
15
+ for each row
16
+ when new.inbox_sequence is null
17
+ begin
18
+ update inbox_items
19
+ set inbox_sequence = new.rowid
20
+ where rowid = new.rowid;
21
+ end;
package/drizzle/schema.ts CHANGED
@@ -139,6 +139,7 @@ export const inboxItems = sqliteTable("inbox_items", {
139
139
  sourceNativeId: text("source_native_id").notNull(),
140
140
  eventVariant: text("event_variant").notNull(),
141
141
  inboxId: text("inbox_id").notNull(),
142
+ inboxSequence: integer("inbox_sequence"),
142
143
  occurredAt: text("occurred_at").notNull(),
143
144
  metadataJson: text("metadata_json").notNull(),
144
145
  rawPayloadJson: text("raw_payload_json").notNull(),
@@ -147,8 +148,10 @@ export const inboxItems = sqliteTable("inbox_items", {
147
148
  }, (table) => ({
148
149
  sourceNativeEventInboxUnique: uniqueIndex("idx_inbox_items_source_native_event_inbox")
149
150
  .on(table.sourceId, table.sourceNativeId, table.eventVariant, table.inboxId),
151
+ inboxSequenceUnique: uniqueIndex("idx_inbox_items_inbox_sequence").on(table.inboxSequence),
150
152
  ackedAtIdx: index("idx_inbox_items_acked_at").on(table.ackedAt),
151
153
  inboxAckedAtIdx: index("idx_inbox_items_inbox_acked_at").on(table.inboxId, table.ackedAt),
154
+ inboxSequenceIdx: index("idx_inbox_items_inbox_sequence_order").on(table.inboxId, table.inboxSequence),
152
155
  sourceOccurredAtIdx: index("idx_inbox_items_source_occurred_at").on(table.sourceId, table.occurredAt),
153
156
  }));
154
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "files": [
21
21
  "dist/src/",
22
+ "scripts/",
22
23
  "drizzle/",
23
24
  "README.md",
24
25
  "LICENSE"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ iTerm2 Python API cursor probe for AgentInbox.
4
+ Returns session state including cursor position and buffer contents.
5
+ """
6
+
7
+ import sys
8
+ import json
9
+ import asyncio
10
+
11
+ # Keep this value in sync with the TypeScript probe normalization constant
12
+ # `NORMALIZED_BUFFER_TAIL_LINES`, since both are intended to mirror the same
13
+ # CLI/probe tail window.
14
+ CAPTURE_TAIL_LINES = 20
15
+
16
+
17
+ def probe_session(connection, session_id):
18
+ """Probe iTerm2 session state and return cursor position with buffer contents."""
19
+ try:
20
+ import iterm2
21
+
22
+ async def get_session_state():
23
+ try:
24
+ app = await iterm2.async_get_app(connection)
25
+ session = app.get_session_by_id(session_id)
26
+
27
+ if not session:
28
+ return {"status": "gone"}
29
+
30
+ screen = await session.async_get_screen_contents()
31
+ cursor = screen.cursor_coord
32
+
33
+ # Capture a tail comparable to the CLI probe window.
34
+ lines = []
35
+ start_line = max(0, screen.number_of_lines - CAPTURE_TAIL_LINES)
36
+ for i in range(start_line, screen.number_of_lines):
37
+ try:
38
+ line = screen.line(i)
39
+ line_str = line.string if hasattr(line, 'string') else str(line)
40
+ lines.append(line_str)
41
+ except Exception:
42
+ break
43
+
44
+ return {
45
+ "status": "available",
46
+ "cursor": {"x": cursor.x, "y": cursor.y},
47
+ "screen_height": screen.number_of_lines,
48
+ "start_line": start_line,
49
+ "lines": lines
50
+ }
51
+ except Exception as e:
52
+ return {"status": "error", "error": str(e)}
53
+
54
+ return get_session_state()
55
+
56
+ except ImportError:
57
+ return {"status": "error", "error": "iterm2 module not installed"}
58
+
59
+
60
+ def main():
61
+ if len(sys.argv) < 2:
62
+ result = {"status": "error", "error": "missing session_id argument"}
63
+ print(json.dumps(result))
64
+ sys.exit(1)
65
+
66
+ session_id = sys.argv[1]
67
+
68
+ # Try to import iterm2 and run the probe
69
+ try:
70
+ import iterm2
71
+ result = iterm2.run_until_complete(probe_session, session_id)
72
+ print(json.dumps(result))
73
+
74
+ # Exit with error code if status is not "available" or "gone"
75
+ if result.get("status") not in ["available", "gone"]:
76
+ sys.exit(1)
77
+ else:
78
+ sys.exit(0)
79
+
80
+ except ImportError:
81
+ result = {"status": "error", "error": "iterm2 module not installed"}
82
+ print(json.dumps(result))
83
+ sys.exit(1)
84
+ except Exception as e:
85
+ result = {"status": "error", "error": str(e)}
86
+ print(json.dumps(result))
87
+ sys.exit(1)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()