@holon-run/agentinbox 0.5.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 +15 -3
- package/dist/src/sources/remote.js +10 -0
- package/dist/src/sources/remote_profiles.js +31 -0
- package/dist/src/store.js +13 -7
- package/dist/src/terminal.js +5 -1
- package/drizzle/migrations/0008_inbox_item_sequence.sql +21 -0
- package/drizzle/schema.ts +3 -0
- package/package.json +2 -1
- 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
|
@@ -1570,9 +1570,21 @@ class AgentInboxService {
|
|
|
1570
1570
|
});
|
|
1571
1571
|
return "deferred";
|
|
1572
1572
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
+
}
|
|
1576
1588
|
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1577
1589
|
inboxId: inbox.inboxId,
|
|
1578
1590
|
totalUnackedCount,
|
|
@@ -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
|
@@ -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
|
|
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("(
|
|
800
|
-
params.push(
|
|
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
|
|
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
|
|
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 (
|
|
831
|
-
`, [ackedAt, inboxId,
|
|
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();
|
package/dist/src/terminal.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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"
|
|
@@ -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()
|