@holon-run/agentinbox 0.4.0 → 0.6.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.
- package/dist/src/adapters.js +6 -0
- package/dist/src/runtime_gate.js +237 -10
- package/dist/src/service.js +90 -5
- package/dist/src/sources/remote.js +10 -0
- package/dist/src/sources/remote_profiles.js +31 -0
- package/dist/src/store.js +18 -9
- package/dist/src/terminal.js +73 -0
- package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +2 -0
- package/drizzle/migrations/0008_inbox_item_sequence.sql +21 -0
- package/drizzle/schema.ts +4 -0
- package/package.json +4 -3
- package/scripts/iterm2_cursor_probe.py +86 -0
package/dist/src/adapters.js
CHANGED
|
@@ -140,6 +140,12 @@ class AdapterRegistry {
|
|
|
140
140
|
}
|
|
141
141
|
return this.remoteSource.expandSubscriptionShortcut(source, input);
|
|
142
142
|
}
|
|
143
|
+
async deriveInlinePreview(source, item) {
|
|
144
|
+
if (source.sourceType === "local_event") {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return this.remoteSource.deriveInlinePreview(source, item);
|
|
148
|
+
}
|
|
143
149
|
status() {
|
|
144
150
|
return {
|
|
145
151
|
remote: this.remoteSource.status?.() ?? {},
|
package/dist/src/runtime_gate.js
CHANGED
|
@@ -11,8 +11,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
13
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
14
|
-
const DEFAULT_ITERM2_SAMPLE_DELAY_MS =
|
|
15
|
-
const DEFAULT_TMUX_SAMPLE_DELAY_MS =
|
|
14
|
+
const DEFAULT_ITERM2_SAMPLE_DELAY_MS = 900;
|
|
15
|
+
const DEFAULT_TMUX_SAMPLE_DELAY_MS = 900;
|
|
16
16
|
const ACTIVE_BUFFER_MARKERS = [
|
|
17
17
|
"esc to interrupt",
|
|
18
18
|
"Working (",
|
|
@@ -170,6 +170,96 @@ class Iterm2TerminalStateProbe {
|
|
|
170
170
|
if (!sessionId) {
|
|
171
171
|
return { presence: "unknown", busy: "unknown" };
|
|
172
172
|
}
|
|
173
|
+
// Try Python API probe first (if enabled)
|
|
174
|
+
if (this.options.enablePythonProbe !== false) {
|
|
175
|
+
const pythonResult = await this.tryPythonApiProbe(sessionId, target.runtimeKind);
|
|
176
|
+
if (pythonResult && pythonResult.presence !== "unknown") {
|
|
177
|
+
return pythonResult;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Fallback to command-line tools
|
|
181
|
+
return this.checkWithCommandLineTools(sessionId);
|
|
182
|
+
}
|
|
183
|
+
async tryPythonApiProbe(sessionId, runtimeKind) {
|
|
184
|
+
try {
|
|
185
|
+
const scriptPath = this.options.pythonScriptPath ?? resolvePythonProbeScript();
|
|
186
|
+
const result = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
187
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
188
|
+
});
|
|
189
|
+
const data = JSON.parse(result.stdout);
|
|
190
|
+
if (data.status === "gone") {
|
|
191
|
+
return { presence: "gone", busy: "unknown" };
|
|
192
|
+
}
|
|
193
|
+
if (data.status === "available") {
|
|
194
|
+
// If we have cursor and lines data, use cursor-aware detection
|
|
195
|
+
if (data.cursor && data.lines) {
|
|
196
|
+
const bufferTail = data.lines.join("\n");
|
|
197
|
+
const cursorPosition = { x: data.cursor.x, y: data.cursor.y };
|
|
198
|
+
const screenHeight = data.screen_height;
|
|
199
|
+
const startLine = data.start_line ?? 0;
|
|
200
|
+
// Perform cursor-aware typing detection
|
|
201
|
+
const busyStatus = evaluatePythonCursorAwareTypingPrompt(bufferTail, cursorPosition, screenHeight, startLine, runtimeKind);
|
|
202
|
+
// If cursor-aware detection indicates busy, return immediately
|
|
203
|
+
if (busyStatus === "busy") {
|
|
204
|
+
return { presence: "available", busy: "busy" };
|
|
205
|
+
}
|
|
206
|
+
// If cursor-aware detection is certain it's not busy, don't apply prompt heuristics
|
|
207
|
+
if (busyStatus === "not_busy") {
|
|
208
|
+
// Still check for buffer changes and active markers
|
|
209
|
+
await (this.options.sleep ?? sleep)(this.options.sampleDelayMs ?? DEFAULT_ITERM2_SAMPLE_DELAY_MS);
|
|
210
|
+
const secondResult = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
211
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
212
|
+
});
|
|
213
|
+
const secondData = JSON.parse(secondResult.stdout);
|
|
214
|
+
if (secondData.status === "available" && secondData.lines) {
|
|
215
|
+
const secondBufferTail = secondData.lines.join("\n");
|
|
216
|
+
if (bufferTail !== secondBufferTail) {
|
|
217
|
+
return { presence: "available", busy: "busy" };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Check for active buffer markers
|
|
221
|
+
if (containsActiveBufferMarker(bufferTail)) {
|
|
222
|
+
return { presence: "available", busy: "busy" };
|
|
223
|
+
}
|
|
224
|
+
return { presence: "available", busy: "idle" };
|
|
225
|
+
}
|
|
226
|
+
// If cursor-aware detection is uncertain, fall back to buffer change detection only
|
|
227
|
+
// Do NOT use generic typing prompts to avoid false positives (see #116)
|
|
228
|
+
if (busyStatus === "unknown") {
|
|
229
|
+
await (this.options.sleep ?? sleep)(this.options.sampleDelayMs ?? DEFAULT_ITERM2_SAMPLE_DELAY_MS);
|
|
230
|
+
const secondResult = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
231
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
232
|
+
});
|
|
233
|
+
const secondData = JSON.parse(secondResult.stdout);
|
|
234
|
+
if (secondData.status === "available" && secondData.lines) {
|
|
235
|
+
const secondBufferTail = secondData.lines.join("\n");
|
|
236
|
+
if (bufferTail !== secondBufferTail) {
|
|
237
|
+
return { presence: "available", busy: "busy" };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Check for active buffer markers
|
|
241
|
+
if (containsActiveBufferMarker(bufferTail)) {
|
|
242
|
+
return { presence: "available", busy: "busy" };
|
|
243
|
+
}
|
|
244
|
+
// Return unknown when cursor-aware detection is uncertain
|
|
245
|
+
// Do NOT fall back to generic typing prompts to avoid false positives
|
|
246
|
+
return { presence: "available", busy: "unknown" };
|
|
247
|
+
}
|
|
248
|
+
// This should never be reached as all cases are handled above
|
|
249
|
+
return { presence: "available", busy: "unknown" };
|
|
250
|
+
}
|
|
251
|
+
// If we don't have cursor data, return unknown to trigger fallback
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Any other status means we should fall back
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Python API probe failed, fall back to command-line tools
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async checkWithCommandLineTools(sessionId) {
|
|
173
263
|
let it2api;
|
|
174
264
|
try {
|
|
175
265
|
it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
|
|
@@ -197,9 +287,7 @@ class Iterm2TerminalStateProbe {
|
|
|
197
287
|
if (containsActiveBufferMarker(second)) {
|
|
198
288
|
return { presence: "available", busy: "busy" };
|
|
199
289
|
}
|
|
200
|
-
|
|
201
|
-
return { presence: "available", busy: "busy" };
|
|
202
|
-
}
|
|
290
|
+
// Do NOT use generic typing prompts to avoid false positives (see #116)
|
|
203
291
|
return { presence: "available", busy: "unknown" };
|
|
204
292
|
}
|
|
205
293
|
async checkSessionPresence(it2api, sessionId) {
|
|
@@ -271,21 +359,33 @@ class TmuxTerminalStateProbe {
|
|
|
271
359
|
if (containsActiveBufferMarker(second)) {
|
|
272
360
|
return { presence: "available", busy: "busy" };
|
|
273
361
|
}
|
|
274
|
-
|
|
362
|
+
// Use cursor-aware detection when available
|
|
363
|
+
const cursorHint = evaluateCursorAwareTypingPrompt(target, paneState, second);
|
|
364
|
+
if (cursorHint === "busy") {
|
|
275
365
|
return { presence: "available", busy: "busy" };
|
|
276
366
|
}
|
|
367
|
+
// Do NOT fall back to generic typing prompts when cursorHint is unknown to avoid false positives (see #116)
|
|
277
368
|
return { presence: "available", busy: "unknown" };
|
|
278
369
|
}
|
|
279
370
|
async readPaneState(paneId) {
|
|
280
371
|
try {
|
|
281
|
-
const result = await this.execAsync("tmux", [
|
|
282
|
-
|
|
372
|
+
const result = await this.execAsync("tmux", [
|
|
373
|
+
"display-message",
|
|
374
|
+
"-p",
|
|
375
|
+
"-t",
|
|
376
|
+
paneId,
|
|
377
|
+
"#{pane_active} #{pane_dead} #{cursor_x} #{cursor_y} #{pane_height}",
|
|
378
|
+
]);
|
|
379
|
+
const [active, dead, cursorX, cursorY, height] = result.stdout.trim().split(/\s+/, 5);
|
|
283
380
|
if (!active || !dead) {
|
|
284
381
|
return "unknown";
|
|
285
382
|
}
|
|
286
383
|
return {
|
|
287
384
|
active: active === "1",
|
|
288
385
|
dead: dead === "1",
|
|
386
|
+
cursorX: parseOptionalInt(cursorX),
|
|
387
|
+
cursorY: parseOptionalInt(cursorY),
|
|
388
|
+
height: parseOptionalInt(height),
|
|
289
389
|
};
|
|
290
390
|
}
|
|
291
391
|
catch (error) {
|
|
@@ -309,7 +409,7 @@ exports.TmuxTerminalStateProbe = TmuxTerminalStateProbe;
|
|
|
309
409
|
function containsActiveBufferMarker(bufferTail) {
|
|
310
410
|
return ACTIVE_BUFFER_MARKERS.some((marker) => bufferTail.includes(marker));
|
|
311
411
|
}
|
|
312
|
-
function hasVisibleTypingPrompt(bufferTail) {
|
|
412
|
+
function hasVisibleTypingPrompt(bufferTail, prefixes = TYPING_PROMPT_PREFIXES) {
|
|
313
413
|
const lines = bufferTail
|
|
314
414
|
.split("\n")
|
|
315
415
|
.map((line) => line.trimEnd())
|
|
@@ -318,13 +418,64 @@ function hasVisibleTypingPrompt(bufferTail) {
|
|
|
318
418
|
if (!lastLine) {
|
|
319
419
|
return false;
|
|
320
420
|
}
|
|
321
|
-
for (const prefix of
|
|
421
|
+
for (const prefix of prefixes) {
|
|
322
422
|
if (lastLine.startsWith(prefix) && lastLine.slice(prefix.length).trim().length > 0) {
|
|
323
423
|
return true;
|
|
324
424
|
}
|
|
325
425
|
}
|
|
326
426
|
return false;
|
|
327
427
|
}
|
|
428
|
+
function evaluateCursorAwareTypingPrompt(target, paneState, bufferTail) {
|
|
429
|
+
const prefix = runtimeTypingPromptPrefix(target.runtimeKind);
|
|
430
|
+
if (!prefix) {
|
|
431
|
+
return "unknown";
|
|
432
|
+
}
|
|
433
|
+
if (paneState.cursorX == null || paneState.cursorY == null || paneState.height == null) {
|
|
434
|
+
return "unknown";
|
|
435
|
+
}
|
|
436
|
+
const lines = bufferTail
|
|
437
|
+
.replace(/\r/g, "")
|
|
438
|
+
.split("\n")
|
|
439
|
+
.map((line) => line.trimEnd());
|
|
440
|
+
if (lines.length === 0) {
|
|
441
|
+
return "unknown";
|
|
442
|
+
}
|
|
443
|
+
const captureStartRow = Math.max(0, paneState.height - lines.length);
|
|
444
|
+
const lineIndex = paneState.cursorY - captureStartRow;
|
|
445
|
+
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
446
|
+
return "unknown";
|
|
447
|
+
}
|
|
448
|
+
const line = lines[lineIndex] ?? "";
|
|
449
|
+
if (!line.startsWith(prefix)) {
|
|
450
|
+
return "not_busy";
|
|
451
|
+
}
|
|
452
|
+
return paneState.cursorX > prefix.length ? "busy" : "not_busy";
|
|
453
|
+
}
|
|
454
|
+
function runtimeTypingPromptPrefix(runtimeKind) {
|
|
455
|
+
if (runtimeKind === "codex") {
|
|
456
|
+
return "› ";
|
|
457
|
+
}
|
|
458
|
+
if (runtimeKind === "claude_code") {
|
|
459
|
+
return "❯ ";
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function typingPromptPrefixesForRuntime(runtimeKind) {
|
|
464
|
+
if (runtimeKind === "codex") {
|
|
465
|
+
return ["› "];
|
|
466
|
+
}
|
|
467
|
+
if (runtimeKind === "claude_code") {
|
|
468
|
+
return ["❯ ", "› "];
|
|
469
|
+
}
|
|
470
|
+
return TYPING_PROMPT_PREFIXES;
|
|
471
|
+
}
|
|
472
|
+
function parseOptionalInt(value) {
|
|
473
|
+
if (!value) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const parsed = Number.parseInt(value, 10);
|
|
477
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
478
|
+
}
|
|
328
479
|
function normalizeBufferTail(buffer) {
|
|
329
480
|
const normalized = buffer
|
|
330
481
|
.replace(/\r/g, "")
|
|
@@ -643,3 +794,79 @@ function parseIterm2SessionIds(stdout) {
|
|
|
643
794
|
})
|
|
644
795
|
.filter((id) => id !== null);
|
|
645
796
|
}
|
|
797
|
+
function resolvePythonProbeScript(override) {
|
|
798
|
+
if (override) {
|
|
799
|
+
return override;
|
|
800
|
+
}
|
|
801
|
+
const scriptRelativePath = node_path_1.default.join("scripts", "iterm2_cursor_probe.py");
|
|
802
|
+
const seen = new Set();
|
|
803
|
+
const candidates = [];
|
|
804
|
+
const addCandidate = (candidate) => {
|
|
805
|
+
const normalized = node_path_1.default.normalize(candidate);
|
|
806
|
+
if (!seen.has(normalized)) {
|
|
807
|
+
seen.add(normalized);
|
|
808
|
+
candidates.push(normalized);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
// Search upward from the current module's directory
|
|
812
|
+
let currentDir = __dirname;
|
|
813
|
+
for (let i = 0; i < 5; i++) { // Limit upward search depth
|
|
814
|
+
addCandidate(node_path_1.default.join(currentDir, scriptRelativePath));
|
|
815
|
+
addCandidate(node_path_1.default.join(currentDir, "dist", scriptRelativePath));
|
|
816
|
+
addCandidate(node_path_1.default.join(currentDir, "dist", "src", scriptRelativePath));
|
|
817
|
+
const parentDir = node_path_1.default.dirname(currentDir);
|
|
818
|
+
if (parentDir === currentDir) {
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
currentDir = parentDir;
|
|
822
|
+
}
|
|
823
|
+
// Check process working directory and its dist paths
|
|
824
|
+
addCandidate(node_path_1.default.join(process.cwd(), scriptRelativePath));
|
|
825
|
+
addCandidate(node_path_1.default.join(process.cwd(), "dist", scriptRelativePath));
|
|
826
|
+
addCandidate(node_path_1.default.join(process.cwd(), "dist", "src", scriptRelativePath));
|
|
827
|
+
// Check all candidates
|
|
828
|
+
for (const candidate of candidates) {
|
|
829
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
830
|
+
return candidate;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
throw new Error(`Unable to locate iterm2_cursor_probe.py. Checked: ${candidates.join(", ")}. ` +
|
|
834
|
+
"Ensure scripts/iterm2_cursor_probe.py is shipped with the package or copied into dist/ during build.");
|
|
835
|
+
}
|
|
836
|
+
function evaluatePythonCursorAwareTypingPrompt(bufferTail, cursorPosition, screenHeight, startLine, runtimeKind) {
|
|
837
|
+
const lines = bufferTail.split("\n");
|
|
838
|
+
const lastLineIndex = lines.length - 1;
|
|
839
|
+
// Cursor coordinates are in full-screen rows, while bufferTail only contains
|
|
840
|
+
// the lines from startLine to end of screen. First ensure the cursor is on the
|
|
841
|
+
// terminal's last visible row, then map that row into the captured window.
|
|
842
|
+
if (cursorPosition.y !== screenHeight - 1) {
|
|
843
|
+
return "not_busy";
|
|
844
|
+
}
|
|
845
|
+
// Map the cursor position into the captured buffer window using startLine
|
|
846
|
+
const cursorRowInBuffer = cursorPosition.y - startLine;
|
|
847
|
+
// If the last visible screen row is not represented by the captured tail,
|
|
848
|
+
// or does not map to the last captured line, we cannot evaluate reliably.
|
|
849
|
+
if (cursorRowInBuffer < 0 || cursorRowInBuffer > lastLineIndex) {
|
|
850
|
+
return "unknown";
|
|
851
|
+
}
|
|
852
|
+
if (cursorRowInBuffer !== lastLineIndex) {
|
|
853
|
+
return "not_busy";
|
|
854
|
+
}
|
|
855
|
+
const lastLine = lines[lastLineIndex] || "";
|
|
856
|
+
const promptPrefix = runtimeTypingPromptPrefix(runtimeKind);
|
|
857
|
+
if (!promptPrefix) {
|
|
858
|
+
return "unknown";
|
|
859
|
+
}
|
|
860
|
+
// Check if the last line starts with the runtime-specific prompt prefix
|
|
861
|
+
if (lastLine.startsWith(promptPrefix)) {
|
|
862
|
+
// Check if cursor is after the prompt prefix
|
|
863
|
+
if (cursorPosition.x > promptPrefix.length) {
|
|
864
|
+
const afterPrompt = lastLine.slice(promptPrefix.length).trim();
|
|
865
|
+
// If there's content after the prompt, user is typing
|
|
866
|
+
if (afterPrompt.length > 0) {
|
|
867
|
+
return "busy";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return "not_busy";
|
|
872
|
+
}
|
package/dist/src/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ActivationDispatcher = exports.AgentInboxService = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const backend_1 = require("./backend");
|
|
5
6
|
const util_1 = require("./util");
|
|
6
7
|
const source_schema_1 = require("./source_schema");
|
|
@@ -1407,15 +1408,26 @@ class AgentInboxService {
|
|
|
1407
1408
|
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
1408
1409
|
if (!state) {
|
|
1409
1410
|
const inbox = this.ensureInboxForAgent(buffer.agentId);
|
|
1411
|
+
const unackedItems = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
|
|
1412
|
+
itemId: item.itemId,
|
|
1413
|
+
sourceId: item.sourceId,
|
|
1414
|
+
sourceNativeId: item.sourceNativeId,
|
|
1415
|
+
eventVariant: item.eventVariant,
|
|
1416
|
+
inboxId: item.inboxId,
|
|
1417
|
+
occurredAt: item.occurredAt,
|
|
1418
|
+
metadata: item.metadata,
|
|
1419
|
+
rawPayload: item.rawPayload,
|
|
1420
|
+
deliveryHandle: item.deliveryHandle,
|
|
1421
|
+
}));
|
|
1410
1422
|
const dispatched = await this.dispatchActivationTarget({
|
|
1411
1423
|
agentId: buffer.agentId,
|
|
1412
1424
|
targetId: buffer.targetId,
|
|
1413
1425
|
newItemCount: entries.length,
|
|
1414
|
-
totalUnackedCount:
|
|
1415
|
-
summary: entries
|
|
1426
|
+
totalUnackedCount: unackedItems.length,
|
|
1427
|
+
summary: latestSummary(entries),
|
|
1416
1428
|
subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1417
1429
|
sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1418
|
-
items:
|
|
1430
|
+
items: unackedItems,
|
|
1419
1431
|
});
|
|
1420
1432
|
if (dispatched === "retryable_failure") {
|
|
1421
1433
|
this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
|
|
@@ -1427,8 +1439,9 @@ class AgentInboxService {
|
|
|
1427
1439
|
targetId: buffer.targetId,
|
|
1428
1440
|
status: "dirty",
|
|
1429
1441
|
leaseExpiresAt: state.leaseExpiresAt,
|
|
1442
|
+
lastNotifiedFingerprint: state.lastNotifiedFingerprint,
|
|
1430
1443
|
pendingNewItemCount: state.pendingNewItemCount + entries.length,
|
|
1431
|
-
pendingSummary:
|
|
1444
|
+
pendingSummary: latestSummary(entries) ?? state.pendingSummary,
|
|
1432
1445
|
pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
1433
1446
|
pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
1434
1447
|
updatedAt: (0, util_1.nowIso)(),
|
|
@@ -1477,6 +1490,20 @@ class AgentInboxService {
|
|
|
1477
1490
|
if (reason === "ack" && state.status !== "dirty") {
|
|
1478
1491
|
return;
|
|
1479
1492
|
}
|
|
1493
|
+
if (reason === "lease" && state.status === "notified") {
|
|
1494
|
+
const target = this.getActivationTarget(targetId);
|
|
1495
|
+
if (target.kind !== "terminal") {
|
|
1496
|
+
// Webhook targets keep the existing lease-based behavior.
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
this.store.upsertActivationDispatchState({
|
|
1500
|
+
...state,
|
|
1501
|
+
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1502
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1503
|
+
});
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1480
1507
|
const dispatched = await this.dispatchActivationTarget({
|
|
1481
1508
|
agentId,
|
|
1482
1509
|
targetId,
|
|
@@ -1515,9 +1542,11 @@ class AgentInboxService {
|
|
|
1515
1542
|
if (target.status === "offline") {
|
|
1516
1543
|
return "offline";
|
|
1517
1544
|
}
|
|
1545
|
+
const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
|
|
1518
1546
|
const inbox = this.ensureInboxForAgent(input.agentId);
|
|
1519
1547
|
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1520
1548
|
const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
|
|
1549
|
+
let terminalPrompt = null;
|
|
1521
1550
|
try {
|
|
1522
1551
|
if (target.kind === "terminal") {
|
|
1523
1552
|
const gate = await this.activationGate.evaluate(target);
|
|
@@ -1532,6 +1561,7 @@ class AgentInboxService {
|
|
|
1532
1561
|
targetId: input.targetId,
|
|
1533
1562
|
status: "dirty",
|
|
1534
1563
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1564
|
+
lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
|
|
1535
1565
|
pendingNewItemCount: input.newItemCount,
|
|
1536
1566
|
pendingSummary: input.summary,
|
|
1537
1567
|
pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
|
|
@@ -1540,11 +1570,44 @@ class AgentInboxService {
|
|
|
1540
1570
|
});
|
|
1541
1571
|
return "deferred";
|
|
1542
1572
|
}
|
|
1573
|
+
let preview = null;
|
|
1574
|
+
if (totalUnackedCount === 1 && input.items.length === 1) {
|
|
1575
|
+
const singleItem = input.items[0];
|
|
1576
|
+
const source = this.store.getSource(singleItem.sourceId);
|
|
1577
|
+
if (source) {
|
|
1578
|
+
try {
|
|
1579
|
+
const sourcePreview = await this.adapters.deriveInlinePreview(source, singleItem);
|
|
1580
|
+
preview = typeof sourcePreview === "string" ? (0, terminal_1.normalizeInlinePreviewText)(sourcePreview) : null;
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
preview = null;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
preview ??= (0, terminal_1.deriveInlineItemPreview)(singleItem, input.summary);
|
|
1587
|
+
}
|
|
1543
1588
|
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1544
1589
|
inboxId: inbox.inboxId,
|
|
1545
1590
|
totalUnackedCount,
|
|
1546
1591
|
summary: input.summary,
|
|
1592
|
+
preview,
|
|
1547
1593
|
});
|
|
1594
|
+
terminalPrompt = prompt;
|
|
1595
|
+
const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
|
|
1596
|
+
if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
|
|
1597
|
+
this.store.upsertActivationDispatchState({
|
|
1598
|
+
agentId: input.agentId,
|
|
1599
|
+
targetId: input.targetId,
|
|
1600
|
+
status: "notified",
|
|
1601
|
+
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1602
|
+
lastNotifiedFingerprint: promptFingerprint,
|
|
1603
|
+
pendingNewItemCount: 0,
|
|
1604
|
+
pendingSummary: null,
|
|
1605
|
+
pendingSubscriptionIds: [],
|
|
1606
|
+
pendingSourceIds: [],
|
|
1607
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1608
|
+
});
|
|
1609
|
+
return "suppressed";
|
|
1610
|
+
}
|
|
1548
1611
|
await this.terminalDispatcher.dispatch(target, prompt);
|
|
1549
1612
|
}
|
|
1550
1613
|
else {
|
|
@@ -1573,6 +1636,9 @@ class AgentInboxService {
|
|
|
1573
1636
|
targetId: input.targetId,
|
|
1574
1637
|
status: "notified",
|
|
1575
1638
|
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1639
|
+
lastNotifiedFingerprint: target.kind === "terminal"
|
|
1640
|
+
? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
|
|
1641
|
+
: state?.lastNotifiedFingerprint ?? null,
|
|
1576
1642
|
pendingNewItemCount: 0,
|
|
1577
1643
|
pendingSummary: null,
|
|
1578
1644
|
pendingSubscriptionIds: [],
|
|
@@ -1602,8 +1668,9 @@ class AgentInboxService {
|
|
|
1602
1668
|
targetId,
|
|
1603
1669
|
status: "dirty",
|
|
1604
1670
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1671
|
+
lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
|
|
1605
1672
|
pendingNewItemCount: entries.length,
|
|
1606
|
-
pendingSummary: entries
|
|
1673
|
+
pendingSummary: latestSummary(entries),
|
|
1607
1674
|
pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1608
1675
|
pendingSourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1609
1676
|
updatedAt: (0, util_1.nowIso)(),
|
|
@@ -2288,6 +2355,24 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
|
|
|
2288
2355
|
}
|
|
2289
2356
|
return `${newItemCount} new ${itemWord} in ${inboxId}`;
|
|
2290
2357
|
}
|
|
2358
|
+
function latestSummary(entries) {
|
|
2359
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
2360
|
+
const summary = entries[index]?.summary;
|
|
2361
|
+
if (summary) {
|
|
2362
|
+
return summary;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
function terminalReminderFingerprint(prompt, items) {
|
|
2368
|
+
const payload = JSON.stringify({
|
|
2369
|
+
prompt,
|
|
2370
|
+
itemIds: items
|
|
2371
|
+
.map((item) => item.itemId)
|
|
2372
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
2373
|
+
});
|
|
2374
|
+
return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
|
|
2375
|
+
}
|
|
2291
2376
|
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
2292
2377
|
if (sourceType === "github_repo_ci") {
|
|
2293
2378
|
const parts = eventVariant.split(".");
|
|
@@ -191,6 +191,16 @@ class RemoteSourceRuntime {
|
|
|
191
191
|
source: profileInputSource(source),
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
async deriveInlinePreview(source, item) {
|
|
195
|
+
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const profile = await this.profileRegistry.resolve(source, this.homeDir);
|
|
199
|
+
if (typeof profile.deriveInlinePreview !== "function") {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return profile.deriveInlinePreview(item, profileInputSource(source));
|
|
203
|
+
}
|
|
194
204
|
async syncAll() {
|
|
195
205
|
const sources = this.store
|
|
196
206
|
.listSources()
|
|
@@ -95,6 +95,7 @@ function validateProfileContract(profile, sourcePath) {
|
|
|
95
95
|
validateOptionalHook(profile.expandSubscriptionShortcut, "expandSubscriptionShortcut", sourcePath);
|
|
96
96
|
validateOptionalHook(profile.deriveTrackedResource, "deriveTrackedResource", sourcePath);
|
|
97
97
|
validateOptionalHook(profile.projectLifecycleSignal, "projectLifecycleSignal", sourcePath);
|
|
98
|
+
validateOptionalHook(profile.deriveInlinePreview, "deriveInlinePreview", sourcePath);
|
|
98
99
|
}
|
|
99
100
|
function validateOptionalHook(value, name, sourcePath) {
|
|
100
101
|
if (value !== undefined && typeof value !== "function") {
|
|
@@ -267,6 +268,36 @@ const GITHUB_REPO_CI_PROFILE = {
|
|
|
267
268
|
eventVariantExamples: ["workflow_run.ci.completed.failure", "workflow_run.nightly_checks.observed"],
|
|
268
269
|
};
|
|
269
270
|
},
|
|
271
|
+
deriveInlinePreview(item) {
|
|
272
|
+
const repoFullName = asNonEmptyString(item.metadata.repoFullName);
|
|
273
|
+
const workflowName = asNonEmptyString(item.metadata.name)
|
|
274
|
+
?? asNonEmptyString(item.metadata.displayTitle)
|
|
275
|
+
?? asNonEmptyString(item.rawPayload.name)
|
|
276
|
+
?? asNonEmptyString(item.rawPayload.display_title);
|
|
277
|
+
const headBranch = asNonEmptyString(item.metadata.headBranch) ?? asNonEmptyString(item.rawPayload.head_branch);
|
|
278
|
+
const conclusion = asNonEmptyString(item.metadata.conclusion) ?? asNonEmptyString(item.rawPayload.conclusion);
|
|
279
|
+
const status = asNonEmptyString(item.metadata.status) ?? asNonEmptyString(item.rawPayload.status);
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (workflowName) {
|
|
282
|
+
parts.push(workflowName);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
parts.push("Workflow run");
|
|
286
|
+
}
|
|
287
|
+
if (conclusion) {
|
|
288
|
+
parts.push(`completed ${conclusion}`);
|
|
289
|
+
}
|
|
290
|
+
else if (status) {
|
|
291
|
+
parts.push(status);
|
|
292
|
+
}
|
|
293
|
+
if (repoFullName) {
|
|
294
|
+
parts.push(`for ${repoFullName}`);
|
|
295
|
+
}
|
|
296
|
+
if (headBranch) {
|
|
297
|
+
parts.push(`on ${headBranch}`);
|
|
298
|
+
}
|
|
299
|
+
return parts.join(" ");
|
|
300
|
+
},
|
|
270
301
|
validateConfig(source) {
|
|
271
302
|
(0, github_ci_1.parseGithubCiSourceConfig)(source);
|
|
272
303
|
},
|
package/dist/src/store.js
CHANGED
|
@@ -729,12 +729,13 @@ class AgentInboxStore {
|
|
|
729
729
|
upsertActivationDispatchState(state) {
|
|
730
730
|
this.db.run(`
|
|
731
731
|
insert into activation_dispatch_states (
|
|
732
|
-
agent_id, target_id, status, lease_expires_at, pending_new_item_count, pending_summary,
|
|
732
|
+
agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, pending_new_item_count, pending_summary,
|
|
733
733
|
pending_subscription_ids_json, pending_source_ids_json, updated_at
|
|
734
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
734
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
735
735
|
on conflict(agent_id, target_id) do update set
|
|
736
736
|
status = excluded.status,
|
|
737
737
|
lease_expires_at = excluded.lease_expires_at,
|
|
738
|
+
last_notified_fingerprint = excluded.last_notified_fingerprint,
|
|
738
739
|
pending_new_item_count = excluded.pending_new_item_count,
|
|
739
740
|
pending_summary = excluded.pending_summary,
|
|
740
741
|
pending_subscription_ids_json = excluded.pending_subscription_ids_json,
|
|
@@ -745,6 +746,7 @@ class AgentInboxStore {
|
|
|
745
746
|
state.targetId,
|
|
746
747
|
state.status,
|
|
747
748
|
state.leaseExpiresAt ?? null,
|
|
749
|
+
state.lastNotifiedFingerprint ?? null,
|
|
748
750
|
state.pendingNewItemCount,
|
|
749
751
|
state.pendingSummary ?? null,
|
|
750
752
|
JSON.stringify(state.pendingSubscriptionIds),
|
|
@@ -778,6 +780,12 @@ class AgentInboxStore {
|
|
|
778
780
|
]);
|
|
779
781
|
const inserted = this.changes() > before;
|
|
780
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]);
|
|
781
789
|
this.persist();
|
|
782
790
|
}
|
|
783
791
|
return inserted;
|
|
@@ -790,14 +798,14 @@ class AgentInboxStore {
|
|
|
790
798
|
filters.push("acked_at is null");
|
|
791
799
|
}
|
|
792
800
|
if (options?.afterItemId) {
|
|
793
|
-
const anchor = this.getOne("select
|
|
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]);
|
|
794
802
|
if (!anchor) {
|
|
795
803
|
throw new Error(`unknown inbox item: ${options.afterItemId}`);
|
|
796
804
|
}
|
|
797
|
-
filters.push("(
|
|
798
|
-
params.push(
|
|
805
|
+
filters.push("coalesce(inbox_sequence, rowid) > ?");
|
|
806
|
+
params.push(Number(anchor.inbox_sequence));
|
|
799
807
|
}
|
|
800
|
-
const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by
|
|
808
|
+
const rows = this.getAll(`select * from inbox_items where ${filters.join(" and ")} order by coalesce(inbox_sequence, rowid) asc`, params);
|
|
801
809
|
return rows.map((row) => this.mapInboxItem(row));
|
|
802
810
|
}
|
|
803
811
|
ackItems(inboxId, itemIds, ackedAt) {
|
|
@@ -816,7 +824,7 @@ class AgentInboxStore {
|
|
|
816
824
|
return changes;
|
|
817
825
|
}
|
|
818
826
|
ackItemsThrough(inboxId, itemId, ackedAt) {
|
|
819
|
-
const anchor = this.getOne("select
|
|
827
|
+
const anchor = this.getOne("select coalesce(inbox_sequence, rowid) as inbox_sequence from inbox_items where inbox_id = ? and item_id = ?", [inboxId, itemId]);
|
|
820
828
|
if (!anchor) {
|
|
821
829
|
throw new Error(`unknown inbox item: ${itemId}`);
|
|
822
830
|
}
|
|
@@ -825,8 +833,8 @@ class AgentInboxStore {
|
|
|
825
833
|
set acked_at = ?
|
|
826
834
|
where inbox_id = ?
|
|
827
835
|
and acked_at is null
|
|
828
|
-
and (
|
|
829
|
-
`, [ackedAt, inboxId,
|
|
836
|
+
and coalesce(inbox_sequence, rowid) <= ?
|
|
837
|
+
`, [ackedAt, inboxId, Number(anchor.inbox_sequence)]);
|
|
830
838
|
const changes = this.changes();
|
|
831
839
|
if (changes > 0) {
|
|
832
840
|
this.persist();
|
|
@@ -1251,6 +1259,7 @@ class AgentInboxStore {
|
|
|
1251
1259
|
targetId: String(row.target_id),
|
|
1252
1260
|
status: row.status,
|
|
1253
1261
|
leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
|
|
1262
|
+
lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
|
|
1254
1263
|
pendingNewItemCount: Number(row.pending_new_item_count),
|
|
1255
1264
|
pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
|
|
1256
1265
|
pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
|
package/dist/src/terminal.js
CHANGED
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.TerminalDispatcher = void 0;
|
|
7
7
|
exports.detectTerminalContext = detectTerminalContext;
|
|
8
8
|
exports.renderAgentPrompt = renderAgentPrompt;
|
|
9
|
+
exports.deriveInlineItemPreview = deriveInlineItemPreview;
|
|
10
|
+
exports.normalizeInlinePreviewText = normalizeInlinePreviewText;
|
|
9
11
|
exports.assignedAgentIdFromContext = assignedAgentIdFromContext;
|
|
10
12
|
const node_child_process_1 = require("node:child_process");
|
|
11
13
|
const node_util_1 = require("node:util");
|
|
@@ -49,11 +51,44 @@ function renderAgentPrompt(input) {
|
|
|
49
51
|
const totalUnackedCount = input.totalUnackedCount ?? input.newItemCount ?? 0;
|
|
50
52
|
const itemWord = totalUnackedCount === 1 ? "item" : "items";
|
|
51
53
|
const base = `AgentInbox: ${totalUnackedCount} unacked ${itemWord} in inbox ${input.inboxId}.`;
|
|
54
|
+
if (totalUnackedCount === 1 && input.preview) {
|
|
55
|
+
const suffix = /(?:[.!?…]|\.{3})$/.test(input.preview) ? "" : ".";
|
|
56
|
+
return `${base} Preview: ${input.preview}${suffix} Read the inbox for full details if needed.`;
|
|
57
|
+
}
|
|
52
58
|
if (input.summary) {
|
|
53
59
|
return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
|
|
54
60
|
}
|
|
55
61
|
return `${base} Please read the inbox, process them, and ack when finished.`;
|
|
56
62
|
}
|
|
63
|
+
const MAX_INLINE_PREVIEW_INPUT_CHARS = 240;
|
|
64
|
+
const MAX_INLINE_PREVIEW_CHARS = 96;
|
|
65
|
+
const INLINE_PREVIEW_KEYS = ["message", "text", "body", "title"];
|
|
66
|
+
function deriveInlineItemPreview(item, summary) {
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (const key of INLINE_PREVIEW_KEYS) {
|
|
69
|
+
const rawPayloadValue = item.rawPayload[key];
|
|
70
|
+
if (typeof rawPayloadValue === "string") {
|
|
71
|
+
candidates.push(rawPayloadValue);
|
|
72
|
+
}
|
|
73
|
+
const metadataValue = item.metadata[key];
|
|
74
|
+
if (typeof metadataValue === "string") {
|
|
75
|
+
candidates.push(metadataValue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (isPreviewFriendlySummary(summary)) {
|
|
79
|
+
candidates.push(summary);
|
|
80
|
+
}
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
const preview = normalizeInlinePreviewText(candidate);
|
|
83
|
+
if (preview) {
|
|
84
|
+
return preview;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function normalizeInlinePreviewText(candidate) {
|
|
90
|
+
return normalizeInlinePreview(candidate);
|
|
91
|
+
}
|
|
57
92
|
function assignedAgentIdFromContext(input) {
|
|
58
93
|
const primary = normalizeOptionalString(input.runtimeSessionId)
|
|
59
94
|
?? normalizeOptionalString(input.tmuxPaneId)
|
|
@@ -176,6 +211,44 @@ function normalizeItermSessionId(raw) {
|
|
|
176
211
|
const parts = value.split(":");
|
|
177
212
|
return parts[parts.length - 1] || null;
|
|
178
213
|
}
|
|
214
|
+
function normalizeInlinePreview(value) {
|
|
215
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
216
|
+
if (!normalized) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (normalized.length > MAX_INLINE_PREVIEW_INPUT_CHARS) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (looksStructured(normalized)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (normalized.length <= MAX_INLINE_PREVIEW_CHARS) {
|
|
226
|
+
return normalized;
|
|
227
|
+
}
|
|
228
|
+
return `${normalized.slice(0, MAX_INLINE_PREVIEW_CHARS - 3).trimEnd()}...`;
|
|
229
|
+
}
|
|
230
|
+
function isPreviewFriendlySummary(summary) {
|
|
231
|
+
if (typeof summary !== "string") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const normalized = summary.trim();
|
|
235
|
+
if (!normalized) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (!normalized.includes(" ")) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (/^[a-z0-9_.-]+:/i.test(normalized)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function looksStructured(value) {
|
|
247
|
+
if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith("[") && value.endsWith("]"))) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
179
252
|
function detectRuntimeContext(env) {
|
|
180
253
|
const codexThreadId = normalizeOptionalString(env.CODEX_THREAD_ID);
|
|
181
254
|
if (codexThreadId) {
|
|
@@ -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
|
@@ -122,6 +122,7 @@ export const activationDispatchStates = sqliteTable("activation_dispatch_states"
|
|
|
122
122
|
targetId: text("target_id").notNull(),
|
|
123
123
|
status: text("status").notNull(),
|
|
124
124
|
leaseExpiresAt: text("lease_expires_at"),
|
|
125
|
+
lastNotifiedFingerprint: text("last_notified_fingerprint"),
|
|
125
126
|
pendingNewItemCount: integer("pending_new_item_count").notNull(),
|
|
126
127
|
pendingSummary: text("pending_summary"),
|
|
127
128
|
pendingSubscriptionIdsJson: text("pending_subscription_ids_json").notNull(),
|
|
@@ -138,6 +139,7 @@ export const inboxItems = sqliteTable("inbox_items", {
|
|
|
138
139
|
sourceNativeId: text("source_native_id").notNull(),
|
|
139
140
|
eventVariant: text("event_variant").notNull(),
|
|
140
141
|
inboxId: text("inbox_id").notNull(),
|
|
142
|
+
inboxSequence: integer("inbox_sequence"),
|
|
141
143
|
occurredAt: text("occurred_at").notNull(),
|
|
142
144
|
metadataJson: text("metadata_json").notNull(),
|
|
143
145
|
rawPayloadJson: text("raw_payload_json").notNull(),
|
|
@@ -146,8 +148,10 @@ export const inboxItems = sqliteTable("inbox_items", {
|
|
|
146
148
|
}, (table) => ({
|
|
147
149
|
sourceNativeEventInboxUnique: uniqueIndex("idx_inbox_items_source_native_event_inbox")
|
|
148
150
|
.on(table.sourceId, table.sourceNativeId, table.eventVariant, table.inboxId),
|
|
151
|
+
inboxSequenceUnique: uniqueIndex("idx_inbox_items_inbox_sequence").on(table.inboxSequence),
|
|
149
152
|
ackedAtIdx: index("idx_inbox_items_acked_at").on(table.ackedAt),
|
|
150
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),
|
|
151
155
|
sourceOccurredAtIdx: index("idx_inbox_items_source_occurred_at").on(table.sourceId, table.occurredAt),
|
|
152
156
|
}));
|
|
153
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holon-run/agentinbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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"
|
|
@@ -49,8 +50,8 @@
|
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "^24.5.2",
|
|
51
52
|
"@types/sql.js": "^1.4.9",
|
|
52
|
-
"drizzle-kit": "
|
|
53
|
-
"drizzle-orm": "^0.
|
|
53
|
+
"drizzle-kit": "1.0.0-beta.21",
|
|
54
|
+
"drizzle-orm": "^0.45.2",
|
|
54
55
|
"mdorigin": "^0.5.0",
|
|
55
56
|
"ts-node": "^10.9.2",
|
|
56
57
|
"typescript": "^5.9.2"
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
|
|
12
|
+
def probe_session(connection, session_id):
|
|
13
|
+
"""Probe iTerm2 session state and return cursor position with buffer contents."""
|
|
14
|
+
try:
|
|
15
|
+
import iterm2
|
|
16
|
+
|
|
17
|
+
async def get_session_state():
|
|
18
|
+
try:
|
|
19
|
+
app = await iterm2.async_get_app(connection)
|
|
20
|
+
session = app.get_session_by_id(session_id)
|
|
21
|
+
|
|
22
|
+
if not session:
|
|
23
|
+
return {"status": "gone"}
|
|
24
|
+
|
|
25
|
+
screen = await session.async_get_screen_contents()
|
|
26
|
+
cursor = screen.cursor_coord
|
|
27
|
+
|
|
28
|
+
# Get the last few lines of content
|
|
29
|
+
lines = []
|
|
30
|
+
start_line = max(0, screen.number_of_lines - 5)
|
|
31
|
+
for i in range(start_line, screen.number_of_lines):
|
|
32
|
+
try:
|
|
33
|
+
line = screen.line(i)
|
|
34
|
+
line_str = line.string if hasattr(line, 'string') else str(line)
|
|
35
|
+
lines.append(line_str)
|
|
36
|
+
except Exception:
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"status": "available",
|
|
41
|
+
"cursor": {"x": cursor.x, "y": cursor.y},
|
|
42
|
+
"screen_height": screen.number_of_lines,
|
|
43
|
+
"start_line": start_line,
|
|
44
|
+
"lines": lines
|
|
45
|
+
}
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return {"status": "error", "error": str(e)}
|
|
48
|
+
|
|
49
|
+
return get_session_state()
|
|
50
|
+
|
|
51
|
+
except ImportError:
|
|
52
|
+
return {"status": "error", "error": "iterm2 module not installed"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
if len(sys.argv) < 2:
|
|
57
|
+
result = {"status": "error", "error": "missing session_id argument"}
|
|
58
|
+
print(json.dumps(result))
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
session_id = sys.argv[1]
|
|
62
|
+
|
|
63
|
+
# Try to import iterm2 and run the probe
|
|
64
|
+
try:
|
|
65
|
+
import iterm2
|
|
66
|
+
result = iterm2.run_until_complete(probe_session, session_id)
|
|
67
|
+
print(json.dumps(result))
|
|
68
|
+
|
|
69
|
+
# Exit with error code if status is not "available" or "gone"
|
|
70
|
+
if result.get("status") not in ["available", "gone"]:
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
else:
|
|
73
|
+
sys.exit(0)
|
|
74
|
+
|
|
75
|
+
except ImportError:
|
|
76
|
+
result = {"status": "error", "error": "iterm2 module not installed"}
|
|
77
|
+
print(json.dumps(result))
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
result = {"status": "error", "error": str(e)}
|
|
81
|
+
print(json.dumps(result))
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|