@holon-run/agentinbox 0.3.0 → 0.5.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/README.md +38 -0
- package/dist/src/cli.js +18 -0
- package/dist/src/daemon.js +69 -0
- package/dist/src/http.js +37 -0
- package/dist/src/runtime_gate.js +199 -18
- package/dist/src/service.js +173 -5
- package/dist/src/store.js +5 -2
- package/dist/src/terminal.js +92 -8
- package/drizzle/migrations/0007_activation_dispatch_last_notified_fingerprint.sql +2 -0
- package/drizzle/schema.ts +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -136,12 +136,40 @@ Create a local source and publish an event:
|
|
|
136
136
|
agentinbox source add local_event local-demo
|
|
137
137
|
agentinbox subscription add <source_id>
|
|
138
138
|
agentinbox subscription add <source_id> --agent-id <agent_id>
|
|
139
|
+
agentinbox source schema <source_id>
|
|
140
|
+
agentinbox subscription add <source_id> --shortcut pr --shortcut-args-json '{"number":373}'
|
|
139
141
|
agentinbox subscription add <source_id> --tracked-resource-ref pr:373 --cleanup-policy-json '{"mode":"manual"}'
|
|
140
142
|
agentinbox subscription add <source_id> --filter-file ./filter.json
|
|
141
143
|
cat filter.json | agentinbox subscription add <source_id> --filter-stdin
|
|
142
144
|
agentinbox source event <source_id> --native-id demo-1 --event local.demo
|
|
143
145
|
agentinbox inbox read
|
|
144
146
|
agentinbox inbox read --agent-id <agent_id>
|
|
147
|
+
agentinbox inbox ack --agent-id <agent_id> --through <last_item_id>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
When acking a reviewed batch, prefer `--through <last_item_id>` over `--all`.
|
|
151
|
+
That preserves the boundary of the items you actually inspected. Use
|
|
152
|
+
`ack --all` only when you have intentionally verified that every current
|
|
153
|
+
unacked item should be cleared.
|
|
154
|
+
|
|
155
|
+
Send a direct text message into an agent inbox:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
agentinbox inbox send --agent-id <agent_id> --message "Please review PR #87"
|
|
159
|
+
agentinbox inbox send --agent-id <agent_id> --message "CI failed on main" --sender operator
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Schedule reminder messages with timers:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
agentinbox timer add --agent-id <agent_id> --at <RFC3339_TIMESTAMP> --message "Check the morning build"
|
|
166
|
+
agentinbox timer add --agent-id <agent_id> --every 24h --message "Review today's open PRs"
|
|
167
|
+
agentinbox timer add --agent-id <agent_id> --cron "0 8 * * *" --timezone Asia/Shanghai --message "Daily triage"
|
|
168
|
+
agentinbox timer list
|
|
169
|
+
agentinbox timer list --agent-id <agent_id>
|
|
170
|
+
agentinbox timer pause <schedule_id>
|
|
171
|
+
agentinbox timer resume <schedule_id>
|
|
172
|
+
agentinbox timer remove <schedule_id>
|
|
145
173
|
```
|
|
146
174
|
|
|
147
175
|
Update an existing source in place:
|
|
@@ -164,6 +192,16 @@ Remove a task-specific subscription without deleting the whole agent:
|
|
|
164
192
|
agentinbox subscription remove <subscription_id>
|
|
165
193
|
```
|
|
166
194
|
|
|
195
|
+
For implementation-backed sources, inspect resolved subscription capabilities
|
|
196
|
+
before adding task-scoped subscriptions:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
agentinbox source schema <source_id>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
If the source advertises subscription shortcuts, prefer them over manually
|
|
203
|
+
repeating the equivalent filter, tracked resource ref, and cleanup policy.
|
|
204
|
+
|
|
167
205
|
## Docs
|
|
168
206
|
|
|
169
207
|
Public docs live in the mdorigin site under [`docs/site`](./docs/site).
|
package/dist/src/cli.js
CHANGED
|
@@ -213,6 +213,14 @@ async function main() {
|
|
|
213
213
|
await printRemote(client, `/agents/${encodeURIComponent(agentId)}`, undefined, "DELETE");
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
|
+
if (command === "agent" && normalized[1] === "resume") {
|
|
217
|
+
const agentId = normalized[2];
|
|
218
|
+
if (!agentId) {
|
|
219
|
+
throw new Error("usage: agentinbox agent resume <agentId>");
|
|
220
|
+
}
|
|
221
|
+
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/resume`, {});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
216
224
|
if (command === "agent" && normalized[1] === "target" && normalized[2] === "add" && normalized[3] === "webhook") {
|
|
217
225
|
const agentId = normalized[4];
|
|
218
226
|
const url = takeFlagValue(normalized, "--url");
|
|
@@ -243,6 +251,14 @@ async function main() {
|
|
|
243
251
|
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}`, undefined, "DELETE");
|
|
244
252
|
return;
|
|
245
253
|
}
|
|
254
|
+
if (command === "agent" && normalized[1] === "target" && normalized[2] === "resume") {
|
|
255
|
+
const [agentId, targetId] = normalized.slice(3, 5);
|
|
256
|
+
if (!agentId || !targetId) {
|
|
257
|
+
throw new Error("usage: agentinbox agent target resume <agentId> <targetId>");
|
|
258
|
+
}
|
|
259
|
+
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}/resume`, {});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
246
262
|
if (command === "subscription" && normalized[1] === "add") {
|
|
247
263
|
const args = normalized.slice(2);
|
|
248
264
|
const positionals = positionalArgs(args, [
|
|
@@ -905,9 +921,11 @@ Usage:
|
|
|
905
921
|
agentinbox agent current
|
|
906
922
|
agentinbox agent show <agentId>
|
|
907
923
|
agentinbox agent remove <agentId>
|
|
924
|
+
agentinbox agent resume <agentId>
|
|
908
925
|
agentinbox agent target add webhook <agentId> --url URL [--activation-mode MODE] [--notify-lease-ms N]
|
|
909
926
|
agentinbox agent target list <agentId>
|
|
910
927
|
agentinbox agent target remove <agentId> <targetId>
|
|
928
|
+
agentinbox agent target resume <agentId> <targetId>
|
|
911
929
|
`,
|
|
912
930
|
timer: `agentinbox timer
|
|
913
931
|
|
package/dist/src/daemon.js
CHANGED
|
@@ -15,6 +15,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
15
15
|
const client_1 = require("./client");
|
|
16
16
|
const paths_1 = require("./paths");
|
|
17
17
|
const START_TIMEOUT_MS = 15_000;
|
|
18
|
+
const PACKAGE_VERSION = readOwnPackageVersion();
|
|
18
19
|
async function ensureDaemonForClient(options = {}) {
|
|
19
20
|
const transport = resolveDaemonClientTransport(options);
|
|
20
21
|
if (options.noAutoStart || transport.kind !== "socket") {
|
|
@@ -95,14 +96,23 @@ async function daemonStatus(options = {}) {
|
|
|
95
96
|
return {
|
|
96
97
|
running: false,
|
|
97
98
|
pid: null,
|
|
99
|
+
version: null,
|
|
100
|
+
startedAt: null,
|
|
101
|
+
command: null,
|
|
102
|
+
nodeVersion: null,
|
|
98
103
|
pidPath,
|
|
99
104
|
logPath,
|
|
100
105
|
transport,
|
|
101
106
|
};
|
|
102
107
|
}
|
|
108
|
+
const processInfo = pid == null ? null : readProcessMetadata(pid);
|
|
103
109
|
return {
|
|
104
110
|
running: await canReachHealthz(transport),
|
|
105
111
|
pid,
|
|
112
|
+
version: pid == null ? null : PACKAGE_VERSION,
|
|
113
|
+
startedAt: processInfo?.startedAt ?? null,
|
|
114
|
+
command: processInfo?.command ?? null,
|
|
115
|
+
nodeVersion: processInfo?.nodeVersion ?? null,
|
|
106
116
|
pidPath,
|
|
107
117
|
logPath,
|
|
108
118
|
transport,
|
|
@@ -179,6 +189,24 @@ async function canReachHealthz(transport) {
|
|
|
179
189
|
return false;
|
|
180
190
|
}
|
|
181
191
|
}
|
|
192
|
+
function readOwnPackageVersion() {
|
|
193
|
+
const candidatePaths = [
|
|
194
|
+
node_path_1.default.join(__dirname, "..", "package.json"),
|
|
195
|
+
node_path_1.default.join(__dirname, "..", "..", "package.json"),
|
|
196
|
+
];
|
|
197
|
+
for (const candidate of candidatePaths) {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(candidate, "utf8"));
|
|
200
|
+
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
201
|
+
return parsed.version;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
182
210
|
async function waitForHealthz(transport, timeoutMs) {
|
|
183
211
|
const startedAt = Date.now();
|
|
184
212
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -222,6 +250,47 @@ function readPidFile(pidPath) {
|
|
|
222
250
|
return null;
|
|
223
251
|
}
|
|
224
252
|
}
|
|
253
|
+
function readProcessMetadata(pid) {
|
|
254
|
+
try {
|
|
255
|
+
const output = (0, node_child_process_1.execFileSync)("ps", ["-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
256
|
+
encoding: "utf8",
|
|
257
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
258
|
+
}).trimEnd();
|
|
259
|
+
if (!output) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const firstNonSpace = output.search(/\S/);
|
|
263
|
+
if (firstNonSpace < 0) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const trimmed = output.slice(firstNonSpace);
|
|
267
|
+
const match = trimmed.match(/^([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d\d:\d\d:\d\d\s+\d{4})\s+(.*)$/);
|
|
268
|
+
if (!match) {
|
|
269
|
+
return {
|
|
270
|
+
startedAt: null,
|
|
271
|
+
command: trimmed,
|
|
272
|
+
nodeVersion: inferNodeVersionFromCommand(trimmed),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const [, startedAtRaw, command] = match;
|
|
276
|
+
const startedAtMs = Date.parse(startedAtRaw);
|
|
277
|
+
return {
|
|
278
|
+
startedAt: Number.isNaN(startedAtMs) ? null : new Date(startedAtMs).toISOString(),
|
|
279
|
+
command: command || null,
|
|
280
|
+
nodeVersion: inferNodeVersionFromCommand(command),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function inferNodeVersionFromCommand(command) {
|
|
288
|
+
if (!command) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const match = command.match(/node\/(v\d+\.\d+\.\d+)\//);
|
|
292
|
+
return match ? match[1] : null;
|
|
293
|
+
}
|
|
225
294
|
function isProcessAlive(pid) {
|
|
226
295
|
try {
|
|
227
296
|
process.kill(pid, 0);
|
package/dist/src/http.js
CHANGED
|
@@ -487,6 +487,24 @@ function buildFastifyServer(service) {
|
|
|
487
487
|
const params = request.params;
|
|
488
488
|
return service.removeAgent(decodeURIComponent(params.agentId));
|
|
489
489
|
});
|
|
490
|
+
app.post("/agents/:agentId/resume", {
|
|
491
|
+
schema: {
|
|
492
|
+
tags: ["agents"],
|
|
493
|
+
params: {
|
|
494
|
+
type: "object",
|
|
495
|
+
required: ["agentId"],
|
|
496
|
+
properties: {
|
|
497
|
+
agentId: { type: "string", minLength: 1 },
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
response: {
|
|
501
|
+
200: jsonObjectSchema,
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
}, async (request) => {
|
|
505
|
+
const params = request.params;
|
|
506
|
+
return service.resumeAgent(decodeURIComponent(params.agentId));
|
|
507
|
+
});
|
|
490
508
|
app.get("/agents/:agentId/targets", {
|
|
491
509
|
schema: {
|
|
492
510
|
tags: ["agents"],
|
|
@@ -565,6 +583,25 @@ function buildFastifyServer(service) {
|
|
|
565
583
|
const params = request.params;
|
|
566
584
|
return service.removeActivationTarget(decodeURIComponent(params.agentId), decodeURIComponent(params.targetId));
|
|
567
585
|
});
|
|
586
|
+
app.post("/agents/:agentId/targets/:targetId/resume", {
|
|
587
|
+
schema: {
|
|
588
|
+
tags: ["agents"],
|
|
589
|
+
params: {
|
|
590
|
+
type: "object",
|
|
591
|
+
required: ["agentId", "targetId"],
|
|
592
|
+
properties: {
|
|
593
|
+
agentId: { type: "string", minLength: 1 },
|
|
594
|
+
targetId: { type: "string", minLength: 1 },
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
response: {
|
|
598
|
+
200: jsonObjectSchema,
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
}, async (request) => {
|
|
602
|
+
const params = request.params;
|
|
603
|
+
return service.resumeActivationTarget(decodeURIComponent(params.agentId), decodeURIComponent(params.targetId));
|
|
604
|
+
});
|
|
568
605
|
app.get("/timers", {
|
|
569
606
|
schema: {
|
|
570
607
|
tags: ["timers"],
|
package/dist/src/runtime_gate.js
CHANGED
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ClaudeCodeTerminalStateProbe = exports.ClaudeCodeRuntimePresenceProbe = exports.TmuxTerminalStateProbe = exports.Iterm2TerminalStateProbe = exports.CodexRuntimePresenceProbe = exports.DefaultActivationGate = void 0;
|
|
6
|
+
exports.ClaudeCodeTerminalStateProbe = exports.ClaudeCodeRuntimePresenceProbe = exports.TmuxTerminalStateProbe = exports.Iterm2TerminalStateProbe = exports.CodexTerminalStateProbe = exports.CodexRuntimePresenceProbe = exports.DefaultActivationGate = void 0;
|
|
7
|
+
exports.readFirstLine = readFirstLine;
|
|
7
8
|
const node_child_process_1 = require("node:child_process");
|
|
8
9
|
const node_util_1 = require("node:util");
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -28,6 +29,7 @@ class DefaultActivationGate {
|
|
|
28
29
|
new CodexRuntimePresenceProbe(),
|
|
29
30
|
new ClaudeCodeRuntimePresenceProbe(),
|
|
30
31
|
], terminalProbes = [
|
|
32
|
+
new CodexTerminalStateProbe(),
|
|
31
33
|
new Iterm2TerminalStateProbe(),
|
|
32
34
|
new ClaudeCodeTerminalStateProbe(),
|
|
33
35
|
new TmuxTerminalStateProbe(),
|
|
@@ -38,27 +40,32 @@ class DefaultActivationGate {
|
|
|
38
40
|
async evaluate(target) {
|
|
39
41
|
const runtimeProbe = this.runtimeProbes.find((probe) => probe.supports(target));
|
|
40
42
|
const runtimePresence = runtimeProbe ? await runtimeProbe.check(target) : "unknown";
|
|
43
|
+
if (runtimePresence === "gone") {
|
|
44
|
+
return { outcome: "offline", reason: "runtime_gone" };
|
|
45
|
+
}
|
|
41
46
|
const terminalProbe = this.terminalProbes.find((probe) => probe.supports(target));
|
|
42
47
|
const terminalState = terminalProbe
|
|
43
48
|
? await terminalProbe.check(target)
|
|
44
49
|
: { presence: "unknown", busy: "unknown" };
|
|
45
|
-
if (runtimePresence === "gone") {
|
|
46
|
-
return { outcome: "offline", reason: "runtime_gone" };
|
|
47
|
-
}
|
|
48
50
|
if (terminalState.presence === "gone") {
|
|
49
51
|
return { outcome: "offline", reason: "terminal_gone" };
|
|
50
52
|
}
|
|
51
53
|
if (terminalState.busy === "busy") {
|
|
52
54
|
return { outcome: "defer", reason: "terminal_busy" };
|
|
53
55
|
}
|
|
56
|
+
if (target.runtimeKind === "codex" && terminalState.busy === "idle") {
|
|
57
|
+
return { outcome: "defer", reason: "terminal_recently_active" };
|
|
58
|
+
}
|
|
54
59
|
return { outcome: "inject", reason: "gate_unknown_or_idle" };
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
exports.DefaultActivationGate = DefaultActivationGate;
|
|
58
63
|
class CodexRuntimePresenceProbe {
|
|
59
64
|
killFn;
|
|
60
|
-
|
|
65
|
+
sessionFileReader;
|
|
66
|
+
constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader) {
|
|
61
67
|
this.killFn = killFn;
|
|
68
|
+
this.sessionFileReader = sessionFileReader;
|
|
62
69
|
}
|
|
63
70
|
supports(target) {
|
|
64
71
|
return target.runtimeKind === "codex" && Number.isInteger(target.runtimePid);
|
|
@@ -69,6 +76,16 @@ class CodexRuntimePresenceProbe {
|
|
|
69
76
|
}
|
|
70
77
|
try {
|
|
71
78
|
this.killFn(target.runtimePid, 0);
|
|
79
|
+
if (!target.runtimeSessionId) {
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
|
|
83
|
+
if (sessionInfo === "unknown") {
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
if (!sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
|
|
87
|
+
return "gone";
|
|
88
|
+
}
|
|
72
89
|
return "alive";
|
|
73
90
|
}
|
|
74
91
|
catch (error) {
|
|
@@ -81,6 +98,58 @@ class CodexRuntimePresenceProbe {
|
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
100
|
exports.CodexRuntimePresenceProbe = CodexRuntimePresenceProbe;
|
|
101
|
+
class CodexTerminalStateProbe {
|
|
102
|
+
iTermProbe;
|
|
103
|
+
tmuxProbe;
|
|
104
|
+
sessionFileReader;
|
|
105
|
+
statReader;
|
|
106
|
+
nowFn;
|
|
107
|
+
constructor(iTermProbe = new Iterm2TerminalStateProbe(), tmuxProbe = new TmuxTerminalStateProbe(), sessionFileReader, statReader, nowFn = () => Date.now()) {
|
|
108
|
+
this.iTermProbe = iTermProbe;
|
|
109
|
+
this.tmuxProbe = tmuxProbe;
|
|
110
|
+
this.sessionFileReader = sessionFileReader;
|
|
111
|
+
this.statReader = statReader;
|
|
112
|
+
this.nowFn = nowFn;
|
|
113
|
+
}
|
|
114
|
+
supports(target) {
|
|
115
|
+
return target.runtimeKind === "codex";
|
|
116
|
+
}
|
|
117
|
+
async check(target) {
|
|
118
|
+
const terminalState = await this.readTerminalState(target);
|
|
119
|
+
if (!target.runtimeSessionId) {
|
|
120
|
+
return terminalState;
|
|
121
|
+
}
|
|
122
|
+
const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
|
|
123
|
+
if (sessionInfo === "unknown" || !sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
|
|
124
|
+
return terminalState;
|
|
125
|
+
}
|
|
126
|
+
const stat = await (this.statReader ?? defaultFileStatReader)(sessionInfo.filePath);
|
|
127
|
+
if (!stat) {
|
|
128
|
+
return terminalState;
|
|
129
|
+
}
|
|
130
|
+
if (terminalState.busy === "busy" || terminalState.presence === "gone") {
|
|
131
|
+
return terminalState;
|
|
132
|
+
}
|
|
133
|
+
const ageMs = this.nowFn() - stat.mtimeMs;
|
|
134
|
+
if (ageMs < 5000) {
|
|
135
|
+
return { presence: terminalState.presence, busy: "busy" };
|
|
136
|
+
}
|
|
137
|
+
if (ageMs < 30000) {
|
|
138
|
+
return { presence: terminalState.presence, busy: "idle" };
|
|
139
|
+
}
|
|
140
|
+
return terminalState;
|
|
141
|
+
}
|
|
142
|
+
async readTerminalState(target) {
|
|
143
|
+
if (this.iTermProbe.supports(target)) {
|
|
144
|
+
return this.iTermProbe.check(target);
|
|
145
|
+
}
|
|
146
|
+
if (this.tmuxProbe.supports(target)) {
|
|
147
|
+
return this.tmuxProbe.check(target);
|
|
148
|
+
}
|
|
149
|
+
return { presence: "unknown", busy: "unknown" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.CodexTerminalStateProbe = CodexTerminalStateProbe;
|
|
84
153
|
class Iterm2TerminalStateProbe {
|
|
85
154
|
execAsync;
|
|
86
155
|
options;
|
|
@@ -128,16 +197,16 @@ class Iterm2TerminalStateProbe {
|
|
|
128
197
|
if (containsActiveBufferMarker(second)) {
|
|
129
198
|
return { presence: "available", busy: "busy" };
|
|
130
199
|
}
|
|
200
|
+
if (hasVisibleTypingPrompt(second)) {
|
|
201
|
+
return { presence: "available", busy: "busy" };
|
|
202
|
+
}
|
|
131
203
|
return { presence: "available", busy: "unknown" };
|
|
132
204
|
}
|
|
133
205
|
async checkSessionPresence(it2api, sessionId) {
|
|
134
206
|
try {
|
|
135
207
|
const result = await this.execAsync(it2api, ["list-sessions"]);
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
.map((line) => line.trim())
|
|
139
|
-
.filter((line) => line.length > 0);
|
|
140
|
-
return sessions.includes(sessionId) ? "available" : "gone";
|
|
208
|
+
const sessionIds = parseIterm2SessionIds(result.stdout);
|
|
209
|
+
return sessionIds.includes(sessionId) ? "available" : "gone";
|
|
141
210
|
}
|
|
142
211
|
catch {
|
|
143
212
|
return "unknown";
|
|
@@ -284,6 +353,114 @@ function resolveIterm2ApiPath(override) {
|
|
|
284
353
|
async function sleep(ms) {
|
|
285
354
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
355
|
}
|
|
356
|
+
async function defaultFileStatReader(filePath) {
|
|
357
|
+
try {
|
|
358
|
+
const stats = await node_fs_1.default.promises.stat(filePath);
|
|
359
|
+
return { mtimeMs: stats.mtimeMs };
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function defaultCodexSessionFileReader(sessionId) {
|
|
366
|
+
const sessionsRoot = node_path_1.default.join(node_os_1.default.homedir(), ".codex", "sessions");
|
|
367
|
+
const filePath = await findCodexSessionFile(sessionsRoot, sessionId).catch(() => "unknown");
|
|
368
|
+
if (filePath === "unknown") {
|
|
369
|
+
return "unknown";
|
|
370
|
+
}
|
|
371
|
+
if (!filePath) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const firstLine = await readFirstLine(filePath);
|
|
376
|
+
if (!firstLine) {
|
|
377
|
+
return "unknown";
|
|
378
|
+
}
|
|
379
|
+
const record = JSON.parse(firstLine);
|
|
380
|
+
const parsedSessionId = typeof record.payload?.id === "string" ? record.payload.id : null;
|
|
381
|
+
if (record.type !== "session_meta" || parsedSessionId !== sessionId) {
|
|
382
|
+
return "unknown";
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
sessionId: parsedSessionId,
|
|
386
|
+
filePath,
|
|
387
|
+
cwd: typeof record.payload?.cwd === "string" ? record.payload.cwd : null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return "unknown";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function findCodexSessionFile(root, sessionId) {
|
|
395
|
+
let years;
|
|
396
|
+
try {
|
|
397
|
+
years = await node_fs_1.default.promises.readdir(root, { withFileTypes: true });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
for (const year of years) {
|
|
403
|
+
if (!year.isDirectory()) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const yearPath = node_path_1.default.join(root, year.name);
|
|
407
|
+
const match = await walkCodexSessionLevel(yearPath, sessionId, 2);
|
|
408
|
+
if (match) {
|
|
409
|
+
return match;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
async function walkCodexSessionLevel(dir, sessionId, depth) {
|
|
415
|
+
let entries;
|
|
416
|
+
try {
|
|
417
|
+
entries = await node_fs_1.default.promises.readdir(dir, { withFileTypes: true });
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
const entryPath = node_path_1.default.join(dir, entry.name);
|
|
424
|
+
if (entry.isFile() && entry.name.endsWith(`${sessionId}.jsonl`)) {
|
|
425
|
+
return entryPath;
|
|
426
|
+
}
|
|
427
|
+
if (entry.isDirectory() && depth > 0) {
|
|
428
|
+
const match = await walkCodexSessionLevel(entryPath, sessionId, depth - 1);
|
|
429
|
+
if (match) {
|
|
430
|
+
return match;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
async function readFirstLine(filePath) {
|
|
437
|
+
const handle = await node_fs_1.default.promises.open(filePath, "r");
|
|
438
|
+
try {
|
|
439
|
+
const chunkSize = 4096;
|
|
440
|
+
const chunks = [];
|
|
441
|
+
let position = 0;
|
|
442
|
+
while (true) {
|
|
443
|
+
const buffer = Buffer.alloc(chunkSize);
|
|
444
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
445
|
+
if (bytesRead <= 0) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
const chunk = buffer.toString("utf8", 0, bytesRead);
|
|
449
|
+
const newline = chunk.indexOf("\n");
|
|
450
|
+
if (newline >= 0) {
|
|
451
|
+
chunks.push(chunk.slice(0, newline));
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
chunks.push(chunk);
|
|
455
|
+
position += bytesRead;
|
|
456
|
+
}
|
|
457
|
+
const firstLine = chunks.join("").trim();
|
|
458
|
+
return firstLine.length > 0 ? firstLine : null;
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
await handle.close();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
287
464
|
function isTmuxMissingPaneError(error) {
|
|
288
465
|
if (!(error instanceof Error)) {
|
|
289
466
|
return false;
|
|
@@ -446,14 +623,7 @@ class ClaudeCodeTerminalStateProbe {
|
|
|
446
623
|
try {
|
|
447
624
|
const it2api = resolveIterm2ApiPath();
|
|
448
625
|
const result = await execFileAsync(it2api, ["list-sessions"]);
|
|
449
|
-
const
|
|
450
|
-
// Parse session IDs from iTerm2 output format: "Session \"...\" id=<UUID> ..."
|
|
451
|
-
const sessionIds = lines
|
|
452
|
-
.map((line) => {
|
|
453
|
-
const match = line.match(/id=([A-F0-9-]+)/);
|
|
454
|
-
return match ? match[1] : null;
|
|
455
|
-
})
|
|
456
|
-
.filter((id) => id !== null);
|
|
626
|
+
const sessionIds = parseIterm2SessionIds(result.stdout);
|
|
457
627
|
return sessionIds.includes(itermSessionId) ? "available" : "gone";
|
|
458
628
|
}
|
|
459
629
|
catch {
|
|
@@ -462,3 +632,14 @@ class ClaudeCodeTerminalStateProbe {
|
|
|
462
632
|
}
|
|
463
633
|
}
|
|
464
634
|
exports.ClaudeCodeTerminalStateProbe = ClaudeCodeTerminalStateProbe;
|
|
635
|
+
function parseIterm2SessionIds(stdout) {
|
|
636
|
+
return stdout
|
|
637
|
+
.split(/\r?\n/)
|
|
638
|
+
.map((line) => line.trim())
|
|
639
|
+
.filter((line) => line.length > 0)
|
|
640
|
+
.map((line) => {
|
|
641
|
+
const match = line.match(/\bid=([A-F0-9-]+)/i);
|
|
642
|
+
return match ? match[1] : null;
|
|
643
|
+
})
|
|
644
|
+
.filter((id) => id !== null);
|
|
645
|
+
}
|
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");
|
|
@@ -531,6 +532,57 @@ class AgentInboxService {
|
|
|
531
532
|
this.reconcileAgentStatus(agentId);
|
|
532
533
|
return { removed: true };
|
|
533
534
|
}
|
|
535
|
+
async resumeAgent(agentId) {
|
|
536
|
+
this.getAgent(agentId);
|
|
537
|
+
const targets = this.store.listActivationTargetsForAgent(agentId);
|
|
538
|
+
const results = [];
|
|
539
|
+
for (const target of targets) {
|
|
540
|
+
results.push(await this.resumeActivationTarget(agentId, target.targetId));
|
|
541
|
+
}
|
|
542
|
+
this.reconcileAgentStatus(agentId);
|
|
543
|
+
return {
|
|
544
|
+
resumed: results.some((result) => result.resumed),
|
|
545
|
+
agent: this.getAgent(agentId),
|
|
546
|
+
targets: results,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async resumeActivationTarget(agentId, targetId) {
|
|
550
|
+
const target = this.getActivationTarget(targetId);
|
|
551
|
+
if (target.agentId !== agentId) {
|
|
552
|
+
throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
|
|
553
|
+
}
|
|
554
|
+
if (target.status === "active") {
|
|
555
|
+
return {
|
|
556
|
+
targetId: target.targetId,
|
|
557
|
+
resumed: false,
|
|
558
|
+
status: target.status,
|
|
559
|
+
reason: "already_active",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
let result;
|
|
563
|
+
if (target.kind === "terminal") {
|
|
564
|
+
result = await this.resumeTerminalActivationTarget(target);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
const now = (0, util_1.nowIso)();
|
|
568
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
569
|
+
status: "active",
|
|
570
|
+
offlineSince: null,
|
|
571
|
+
consecutiveFailures: 0,
|
|
572
|
+
lastError: null,
|
|
573
|
+
updatedAt: now,
|
|
574
|
+
lastSeenAt: now,
|
|
575
|
+
});
|
|
576
|
+
result = {
|
|
577
|
+
targetId: target.targetId,
|
|
578
|
+
resumed: true,
|
|
579
|
+
status: "active",
|
|
580
|
+
reason: "webhook_resumed",
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
this.reconcileAgentStatus(agentId);
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
534
586
|
async registerSubscription(input) {
|
|
535
587
|
if (input.shortcut) {
|
|
536
588
|
const source = this.store.getSource(input.sourceId);
|
|
@@ -1355,14 +1407,27 @@ class AgentInboxService {
|
|
|
1355
1407
|
try {
|
|
1356
1408
|
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
1357
1409
|
if (!state) {
|
|
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
|
+
}));
|
|
1358
1422
|
const dispatched = await this.dispatchActivationTarget({
|
|
1359
1423
|
agentId: buffer.agentId,
|
|
1360
1424
|
targetId: buffer.targetId,
|
|
1361
1425
|
newItemCount: entries.length,
|
|
1362
|
-
|
|
1426
|
+
totalUnackedCount: unackedItems.length,
|
|
1427
|
+
summary: latestSummary(entries),
|
|
1363
1428
|
subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1364
1429
|
sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1365
|
-
items:
|
|
1430
|
+
items: unackedItems,
|
|
1366
1431
|
});
|
|
1367
1432
|
if (dispatched === "retryable_failure") {
|
|
1368
1433
|
this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
|
|
@@ -1374,8 +1439,9 @@ class AgentInboxService {
|
|
|
1374
1439
|
targetId: buffer.targetId,
|
|
1375
1440
|
status: "dirty",
|
|
1376
1441
|
leaseExpiresAt: state.leaseExpiresAt,
|
|
1442
|
+
lastNotifiedFingerprint: state.lastNotifiedFingerprint,
|
|
1377
1443
|
pendingNewItemCount: state.pendingNewItemCount + entries.length,
|
|
1378
|
-
pendingSummary:
|
|
1444
|
+
pendingSummary: latestSummary(entries) ?? state.pendingSummary,
|
|
1379
1445
|
pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
1380
1446
|
pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
1381
1447
|
updatedAt: (0, util_1.nowIso)(),
|
|
@@ -1424,10 +1490,25 @@ class AgentInboxService {
|
|
|
1424
1490
|
if (reason === "ack" && state.status !== "dirty") {
|
|
1425
1491
|
return;
|
|
1426
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
|
+
}
|
|
1427
1507
|
const dispatched = await this.dispatchActivationTarget({
|
|
1428
1508
|
agentId,
|
|
1429
1509
|
targetId,
|
|
1430
1510
|
newItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
|
|
1511
|
+
totalUnackedCount: unacked,
|
|
1431
1512
|
summary: state.pendingSummary,
|
|
1432
1513
|
subscriptionIds: state.pendingSubscriptionIds,
|
|
1433
1514
|
sourceIds: state.pendingSourceIds,
|
|
@@ -1461,8 +1542,11 @@ class AgentInboxService {
|
|
|
1461
1542
|
if (target.status === "offline") {
|
|
1462
1543
|
return "offline";
|
|
1463
1544
|
}
|
|
1545
|
+
const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
|
|
1464
1546
|
const inbox = this.ensureInboxForAgent(input.agentId);
|
|
1465
1547
|
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1548
|
+
const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
|
|
1549
|
+
let terminalPrompt = null;
|
|
1466
1550
|
try {
|
|
1467
1551
|
if (target.kind === "terminal") {
|
|
1468
1552
|
const gate = await this.activationGate.evaluate(target);
|
|
@@ -1477,6 +1561,7 @@ class AgentInboxService {
|
|
|
1477
1561
|
targetId: input.targetId,
|
|
1478
1562
|
status: "dirty",
|
|
1479
1563
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1564
|
+
lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
|
|
1480
1565
|
pendingNewItemCount: input.newItemCount,
|
|
1481
1566
|
pendingSummary: input.summary,
|
|
1482
1567
|
pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
|
|
@@ -1485,11 +1570,32 @@ class AgentInboxService {
|
|
|
1485
1570
|
});
|
|
1486
1571
|
return "deferred";
|
|
1487
1572
|
}
|
|
1573
|
+
const preview = totalUnackedCount === 1 && input.items.length === 1
|
|
1574
|
+
? (0, terminal_1.deriveInlineItemPreview)(input.items[0], input.summary)
|
|
1575
|
+
: null;
|
|
1488
1576
|
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1489
1577
|
inboxId: inbox.inboxId,
|
|
1490
|
-
|
|
1578
|
+
totalUnackedCount,
|
|
1491
1579
|
summary: input.summary,
|
|
1580
|
+
preview,
|
|
1492
1581
|
});
|
|
1582
|
+
terminalPrompt = prompt;
|
|
1583
|
+
const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
|
|
1584
|
+
if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
|
|
1585
|
+
this.store.upsertActivationDispatchState({
|
|
1586
|
+
agentId: input.agentId,
|
|
1587
|
+
targetId: input.targetId,
|
|
1588
|
+
status: "notified",
|
|
1589
|
+
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1590
|
+
lastNotifiedFingerprint: promptFingerprint,
|
|
1591
|
+
pendingNewItemCount: 0,
|
|
1592
|
+
pendingSummary: null,
|
|
1593
|
+
pendingSubscriptionIds: [],
|
|
1594
|
+
pendingSourceIds: [],
|
|
1595
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1596
|
+
});
|
|
1597
|
+
return "suppressed";
|
|
1598
|
+
}
|
|
1493
1599
|
await this.terminalDispatcher.dispatch(target, prompt);
|
|
1494
1600
|
}
|
|
1495
1601
|
else {
|
|
@@ -1518,6 +1624,9 @@ class AgentInboxService {
|
|
|
1518
1624
|
targetId: input.targetId,
|
|
1519
1625
|
status: "notified",
|
|
1520
1626
|
leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
|
|
1627
|
+
lastNotifiedFingerprint: target.kind === "terminal"
|
|
1628
|
+
? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
|
|
1629
|
+
: state?.lastNotifiedFingerprint ?? null,
|
|
1521
1630
|
pendingNewItemCount: 0,
|
|
1522
1631
|
pendingSummary: null,
|
|
1523
1632
|
pendingSubscriptionIds: [],
|
|
@@ -1547,8 +1656,9 @@ class AgentInboxService {
|
|
|
1547
1656
|
targetId,
|
|
1548
1657
|
status: "dirty",
|
|
1549
1658
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1659
|
+
lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
|
|
1550
1660
|
pendingNewItemCount: entries.length,
|
|
1551
|
-
pendingSummary: entries
|
|
1661
|
+
pendingSummary: latestSummary(entries),
|
|
1552
1662
|
pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1553
1663
|
pendingSourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1554
1664
|
updatedAt: (0, util_1.nowIso)(),
|
|
@@ -1644,6 +1754,46 @@ class AgentInboxService {
|
|
|
1644
1754
|
lastSeenAt: agent.lastSeenAt,
|
|
1645
1755
|
});
|
|
1646
1756
|
}
|
|
1757
|
+
async resumeTerminalActivationTarget(target) {
|
|
1758
|
+
const probeStatus = await this.terminalDispatcher.probeStatus(target);
|
|
1759
|
+
const now = (0, util_1.nowIso)();
|
|
1760
|
+
if (probeStatus === "available") {
|
|
1761
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
1762
|
+
status: "active",
|
|
1763
|
+
offlineSince: null,
|
|
1764
|
+
consecutiveFailures: 0,
|
|
1765
|
+
lastError: null,
|
|
1766
|
+
updatedAt: now,
|
|
1767
|
+
lastSeenAt: now,
|
|
1768
|
+
});
|
|
1769
|
+
return {
|
|
1770
|
+
targetId: target.targetId,
|
|
1771
|
+
resumed: true,
|
|
1772
|
+
status: "active",
|
|
1773
|
+
reason: "terminal_available",
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
if (probeStatus === "gone") {
|
|
1777
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
1778
|
+
status: "offline",
|
|
1779
|
+
offlineSince: target.offlineSince ?? now,
|
|
1780
|
+
updatedAt: now,
|
|
1781
|
+
lastError: target.lastError ?? "terminal gone",
|
|
1782
|
+
});
|
|
1783
|
+
return {
|
|
1784
|
+
targetId: target.targetId,
|
|
1785
|
+
resumed: false,
|
|
1786
|
+
status: "offline",
|
|
1787
|
+
reason: "terminal_gone",
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
targetId: target.targetId,
|
|
1792
|
+
resumed: false,
|
|
1793
|
+
status: "offline",
|
|
1794
|
+
reason: "probe_unknown",
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1647
1797
|
runLifecycleCleanupPass(nowMs) {
|
|
1648
1798
|
this.lastLifecycleCleanupAt = nowMs;
|
|
1649
1799
|
const removed = new Set();
|
|
@@ -2193,6 +2343,24 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
|
|
|
2193
2343
|
}
|
|
2194
2344
|
return `${newItemCount} new ${itemWord} in ${inboxId}`;
|
|
2195
2345
|
}
|
|
2346
|
+
function latestSummary(entries) {
|
|
2347
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
2348
|
+
const summary = entries[index]?.summary;
|
|
2349
|
+
if (summary) {
|
|
2350
|
+
return summary;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
function terminalReminderFingerprint(prompt, items) {
|
|
2356
|
+
const payload = JSON.stringify({
|
|
2357
|
+
prompt,
|
|
2358
|
+
itemIds: items
|
|
2359
|
+
.map((item) => item.itemId)
|
|
2360
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
2361
|
+
});
|
|
2362
|
+
return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
|
|
2363
|
+
}
|
|
2196
2364
|
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
2197
2365
|
if (sourceType === "github_repo_ci") {
|
|
2198
2366
|
const parts = eventVariant.split(".");
|
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),
|
|
@@ -1251,6 +1253,7 @@ class AgentInboxStore {
|
|
|
1251
1253
|
targetId: String(row.target_id),
|
|
1252
1254
|
status: row.status,
|
|
1253
1255
|
leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
|
|
1256
|
+
lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
|
|
1254
1257
|
pendingNewItemCount: Number(row.pending_new_item_count),
|
|
1255
1258
|
pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
|
|
1256
1259
|
pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
|
package/dist/src/terminal.js
CHANGED
|
@@ -6,6 +6,7 @@ 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;
|
|
9
10
|
exports.assignedAgentIdFromContext = assignedAgentIdFromContext;
|
|
10
11
|
const node_child_process_1 = require("node:child_process");
|
|
11
12
|
const node_util_1 = require("node:util");
|
|
@@ -46,12 +47,44 @@ function detectTerminalContext(env = process.env) {
|
|
|
46
47
|
throw new Error("unable to detect a supported terminal context");
|
|
47
48
|
}
|
|
48
49
|
function renderAgentPrompt(input) {
|
|
49
|
-
const
|
|
50
|
+
const totalUnackedCount = input.totalUnackedCount ?? input.newItemCount ?? 0;
|
|
51
|
+
const itemWord = totalUnackedCount === 1 ? "item" : "items";
|
|
52
|
+
const base = `AgentInbox: ${totalUnackedCount} unacked ${itemWord} in inbox ${input.inboxId}.`;
|
|
53
|
+
if (totalUnackedCount === 1 && input.preview) {
|
|
54
|
+
const suffix = /(?:[.!?…]|\.{3})$/.test(input.preview) ? "" : ".";
|
|
55
|
+
return `${base} Preview: ${input.preview}${suffix} Read the inbox for full details if needed.`;
|
|
56
|
+
}
|
|
50
57
|
if (input.summary) {
|
|
51
58
|
return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
|
|
52
59
|
}
|
|
53
60
|
return `${base} Please read the inbox, process them, and ack when finished.`;
|
|
54
61
|
}
|
|
62
|
+
const MAX_INLINE_PREVIEW_INPUT_CHARS = 240;
|
|
63
|
+
const MAX_INLINE_PREVIEW_CHARS = 96;
|
|
64
|
+
const INLINE_PREVIEW_KEYS = ["message", "text", "body", "title"];
|
|
65
|
+
function deriveInlineItemPreview(item, summary) {
|
|
66
|
+
const candidates = [];
|
|
67
|
+
for (const key of INLINE_PREVIEW_KEYS) {
|
|
68
|
+
const rawPayloadValue = item.rawPayload[key];
|
|
69
|
+
if (typeof rawPayloadValue === "string") {
|
|
70
|
+
candidates.push(rawPayloadValue);
|
|
71
|
+
}
|
|
72
|
+
const metadataValue = item.metadata[key];
|
|
73
|
+
if (typeof metadataValue === "string") {
|
|
74
|
+
candidates.push(metadataValue);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (isPreviewFriendlySummary(summary)) {
|
|
78
|
+
candidates.push(summary);
|
|
79
|
+
}
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
const preview = normalizeInlinePreview(candidate);
|
|
82
|
+
if (preview) {
|
|
83
|
+
return preview;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
55
88
|
function assignedAgentIdFromContext(input) {
|
|
56
89
|
const primary = normalizeOptionalString(input.runtimeSessionId)
|
|
57
90
|
?? normalizeOptionalString(input.tmuxPaneId)
|
|
@@ -90,31 +123,44 @@ class TerminalDispatcher {
|
|
|
90
123
|
throw new Error(`unsupported terminal backend: ${String(target.backend)}`);
|
|
91
124
|
}
|
|
92
125
|
async probe(target) {
|
|
126
|
+
return (await this.probeStatus(target)) === "available";
|
|
127
|
+
}
|
|
128
|
+
async probeStatus(target) {
|
|
93
129
|
try {
|
|
94
130
|
if (target.backend === "tmux") {
|
|
95
131
|
if (!target.tmuxPaneId) {
|
|
96
|
-
return
|
|
132
|
+
return "unknown";
|
|
97
133
|
}
|
|
98
134
|
const result = await this.execAsync("tmux", ["display-message", "-p", "-t", target.tmuxPaneId, "#{pane_id}"]);
|
|
99
|
-
return result.stdout.trim() === target.tmuxPaneId;
|
|
135
|
+
return result.stdout.trim() === target.tmuxPaneId ? "available" : "gone";
|
|
100
136
|
}
|
|
101
137
|
if (target.backend === "iterm2") {
|
|
102
138
|
const sessionId = normalizeOptionalString(target.itermSessionId);
|
|
103
139
|
if (!sessionId) {
|
|
104
|
-
return
|
|
140
|
+
return "unknown";
|
|
105
141
|
}
|
|
106
142
|
const it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
|
|
107
143
|
const result = await this.execAsync(it2api, ["list-sessions"]);
|
|
108
|
-
return result.stdout.includes(sessionId);
|
|
144
|
+
return result.stdout.includes(sessionId) ? "available" : "gone";
|
|
109
145
|
}
|
|
110
146
|
}
|
|
111
|
-
catch {
|
|
112
|
-
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (target.backend === "tmux" && isTmuxMissingPaneError(error)) {
|
|
149
|
+
return "gone";
|
|
150
|
+
}
|
|
151
|
+
return "unknown";
|
|
113
152
|
}
|
|
114
|
-
return
|
|
153
|
+
return "unknown";
|
|
115
154
|
}
|
|
116
155
|
}
|
|
117
156
|
exports.TerminalDispatcher = TerminalDispatcher;
|
|
157
|
+
function isTmuxMissingPaneError(error) {
|
|
158
|
+
if (!(error instanceof Error)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const candidate = `${error.message} ${"stderr" in error ? String(error.stderr ?? "") : ""}`;
|
|
162
|
+
return candidate.includes("can't find pane");
|
|
163
|
+
}
|
|
118
164
|
function detectTty(env) {
|
|
119
165
|
const direct = tryReadCurrentTty();
|
|
120
166
|
if (direct) {
|
|
@@ -161,6 +207,44 @@ function normalizeItermSessionId(raw) {
|
|
|
161
207
|
const parts = value.split(":");
|
|
162
208
|
return parts[parts.length - 1] || null;
|
|
163
209
|
}
|
|
210
|
+
function normalizeInlinePreview(value) {
|
|
211
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
212
|
+
if (!normalized) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (normalized.length > MAX_INLINE_PREVIEW_INPUT_CHARS) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (looksStructured(normalized)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (normalized.length <= MAX_INLINE_PREVIEW_CHARS) {
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
return `${normalized.slice(0, MAX_INLINE_PREVIEW_CHARS - 3).trimEnd()}...`;
|
|
225
|
+
}
|
|
226
|
+
function isPreviewFriendlySummary(summary) {
|
|
227
|
+
if (typeof summary !== "string") {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const normalized = summary.trim();
|
|
231
|
+
if (!normalized) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (!normalized.includes(" ")) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (/^[a-z0-9_.-]+:/i.test(normalized)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
function looksStructured(value) {
|
|
243
|
+
if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith("[") && value.endsWith("]"))) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
164
248
|
function detectRuntimeContext(env) {
|
|
165
249
|
const codexThreadId = normalizeOptionalString(env.CODEX_THREAD_ID);
|
|
166
250
|
if (codexThreadId) {
|
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(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holon-run/agentinbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local event subscription and delivery service for agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^24.5.2",
|
|
51
51
|
"@types/sql.js": "^1.4.9",
|
|
52
|
-
"drizzle-kit": "
|
|
53
|
-
"drizzle-orm": "^0.
|
|
52
|
+
"drizzle-kit": "1.0.0-beta.21",
|
|
53
|
+
"drizzle-orm": "^0.45.2",
|
|
54
54
|
"mdorigin": "^0.5.0",
|
|
55
55
|
"ts-node": "^10.9.2",
|
|
56
56
|
"typescript": "^5.9.2"
|