@holon-run/agentinbox 0.3.0 → 0.4.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 +96 -1
- package/dist/src/terminal.js +23 -8
- package/package.json +1 -1
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
|
@@ -531,6 +531,57 @@ class AgentInboxService {
|
|
|
531
531
|
this.reconcileAgentStatus(agentId);
|
|
532
532
|
return { removed: true };
|
|
533
533
|
}
|
|
534
|
+
async resumeAgent(agentId) {
|
|
535
|
+
this.getAgent(agentId);
|
|
536
|
+
const targets = this.store.listActivationTargetsForAgent(agentId);
|
|
537
|
+
const results = [];
|
|
538
|
+
for (const target of targets) {
|
|
539
|
+
results.push(await this.resumeActivationTarget(agentId, target.targetId));
|
|
540
|
+
}
|
|
541
|
+
this.reconcileAgentStatus(agentId);
|
|
542
|
+
return {
|
|
543
|
+
resumed: results.some((result) => result.resumed),
|
|
544
|
+
agent: this.getAgent(agentId),
|
|
545
|
+
targets: results,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async resumeActivationTarget(agentId, targetId) {
|
|
549
|
+
const target = this.getActivationTarget(targetId);
|
|
550
|
+
if (target.agentId !== agentId) {
|
|
551
|
+
throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
|
|
552
|
+
}
|
|
553
|
+
if (target.status === "active") {
|
|
554
|
+
return {
|
|
555
|
+
targetId: target.targetId,
|
|
556
|
+
resumed: false,
|
|
557
|
+
status: target.status,
|
|
558
|
+
reason: "already_active",
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
let result;
|
|
562
|
+
if (target.kind === "terminal") {
|
|
563
|
+
result = await this.resumeTerminalActivationTarget(target);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
const now = (0, util_1.nowIso)();
|
|
567
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
568
|
+
status: "active",
|
|
569
|
+
offlineSince: null,
|
|
570
|
+
consecutiveFailures: 0,
|
|
571
|
+
lastError: null,
|
|
572
|
+
updatedAt: now,
|
|
573
|
+
lastSeenAt: now,
|
|
574
|
+
});
|
|
575
|
+
result = {
|
|
576
|
+
targetId: target.targetId,
|
|
577
|
+
resumed: true,
|
|
578
|
+
status: "active",
|
|
579
|
+
reason: "webhook_resumed",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
this.reconcileAgentStatus(agentId);
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
534
585
|
async registerSubscription(input) {
|
|
535
586
|
if (input.shortcut) {
|
|
536
587
|
const source = this.store.getSource(input.sourceId);
|
|
@@ -1355,10 +1406,12 @@ class AgentInboxService {
|
|
|
1355
1406
|
try {
|
|
1356
1407
|
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
1357
1408
|
if (!state) {
|
|
1409
|
+
const inbox = this.ensureInboxForAgent(buffer.agentId);
|
|
1358
1410
|
const dispatched = await this.dispatchActivationTarget({
|
|
1359
1411
|
agentId: buffer.agentId,
|
|
1360
1412
|
targetId: buffer.targetId,
|
|
1361
1413
|
newItemCount: entries.length,
|
|
1414
|
+
totalUnackedCount: this.store.countInboxItems(inbox.inboxId, false),
|
|
1362
1415
|
summary: entries[0]?.summary ?? null,
|
|
1363
1416
|
subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1364
1417
|
sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
@@ -1428,6 +1481,7 @@ class AgentInboxService {
|
|
|
1428
1481
|
agentId,
|
|
1429
1482
|
targetId,
|
|
1430
1483
|
newItemCount: state.pendingNewItemCount > 0 ? state.pendingNewItemCount : unacked,
|
|
1484
|
+
totalUnackedCount: unacked,
|
|
1431
1485
|
summary: state.pendingSummary,
|
|
1432
1486
|
subscriptionIds: state.pendingSubscriptionIds,
|
|
1433
1487
|
sourceIds: state.pendingSourceIds,
|
|
@@ -1463,6 +1517,7 @@ class AgentInboxService {
|
|
|
1463
1517
|
}
|
|
1464
1518
|
const inbox = this.ensureInboxForAgent(input.agentId);
|
|
1465
1519
|
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1520
|
+
const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
|
|
1466
1521
|
try {
|
|
1467
1522
|
if (target.kind === "terminal") {
|
|
1468
1523
|
const gate = await this.activationGate.evaluate(target);
|
|
@@ -1487,7 +1542,7 @@ class AgentInboxService {
|
|
|
1487
1542
|
}
|
|
1488
1543
|
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1489
1544
|
inboxId: inbox.inboxId,
|
|
1490
|
-
|
|
1545
|
+
totalUnackedCount,
|
|
1491
1546
|
summary: input.summary,
|
|
1492
1547
|
});
|
|
1493
1548
|
await this.terminalDispatcher.dispatch(target, prompt);
|
|
@@ -1644,6 +1699,46 @@ class AgentInboxService {
|
|
|
1644
1699
|
lastSeenAt: agent.lastSeenAt,
|
|
1645
1700
|
});
|
|
1646
1701
|
}
|
|
1702
|
+
async resumeTerminalActivationTarget(target) {
|
|
1703
|
+
const probeStatus = await this.terminalDispatcher.probeStatus(target);
|
|
1704
|
+
const now = (0, util_1.nowIso)();
|
|
1705
|
+
if (probeStatus === "available") {
|
|
1706
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
1707
|
+
status: "active",
|
|
1708
|
+
offlineSince: null,
|
|
1709
|
+
consecutiveFailures: 0,
|
|
1710
|
+
lastError: null,
|
|
1711
|
+
updatedAt: now,
|
|
1712
|
+
lastSeenAt: now,
|
|
1713
|
+
});
|
|
1714
|
+
return {
|
|
1715
|
+
targetId: target.targetId,
|
|
1716
|
+
resumed: true,
|
|
1717
|
+
status: "active",
|
|
1718
|
+
reason: "terminal_available",
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (probeStatus === "gone") {
|
|
1722
|
+
this.store.updateActivationTargetRuntime(target.targetId, {
|
|
1723
|
+
status: "offline",
|
|
1724
|
+
offlineSince: target.offlineSince ?? now,
|
|
1725
|
+
updatedAt: now,
|
|
1726
|
+
lastError: target.lastError ?? "terminal gone",
|
|
1727
|
+
});
|
|
1728
|
+
return {
|
|
1729
|
+
targetId: target.targetId,
|
|
1730
|
+
resumed: false,
|
|
1731
|
+
status: "offline",
|
|
1732
|
+
reason: "terminal_gone",
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
return {
|
|
1736
|
+
targetId: target.targetId,
|
|
1737
|
+
resumed: false,
|
|
1738
|
+
status: "offline",
|
|
1739
|
+
reason: "probe_unknown",
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1647
1742
|
runLifecycleCleanupPass(nowMs) {
|
|
1648
1743
|
this.lastLifecycleCleanupAt = nowMs;
|
|
1649
1744
|
const removed = new Set();
|
package/dist/src/terminal.js
CHANGED
|
@@ -46,7 +46,9 @@ function detectTerminalContext(env = process.env) {
|
|
|
46
46
|
throw new Error("unable to detect a supported terminal context");
|
|
47
47
|
}
|
|
48
48
|
function renderAgentPrompt(input) {
|
|
49
|
-
const
|
|
49
|
+
const totalUnackedCount = input.totalUnackedCount ?? input.newItemCount ?? 0;
|
|
50
|
+
const itemWord = totalUnackedCount === 1 ? "item" : "items";
|
|
51
|
+
const base = `AgentInbox: ${totalUnackedCount} unacked ${itemWord} in inbox ${input.inboxId}.`;
|
|
50
52
|
if (input.summary) {
|
|
51
53
|
return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
|
|
52
54
|
}
|
|
@@ -90,31 +92,44 @@ class TerminalDispatcher {
|
|
|
90
92
|
throw new Error(`unsupported terminal backend: ${String(target.backend)}`);
|
|
91
93
|
}
|
|
92
94
|
async probe(target) {
|
|
95
|
+
return (await this.probeStatus(target)) === "available";
|
|
96
|
+
}
|
|
97
|
+
async probeStatus(target) {
|
|
93
98
|
try {
|
|
94
99
|
if (target.backend === "tmux") {
|
|
95
100
|
if (!target.tmuxPaneId) {
|
|
96
|
-
return
|
|
101
|
+
return "unknown";
|
|
97
102
|
}
|
|
98
103
|
const result = await this.execAsync("tmux", ["display-message", "-p", "-t", target.tmuxPaneId, "#{pane_id}"]);
|
|
99
|
-
return result.stdout.trim() === target.tmuxPaneId;
|
|
104
|
+
return result.stdout.trim() === target.tmuxPaneId ? "available" : "gone";
|
|
100
105
|
}
|
|
101
106
|
if (target.backend === "iterm2") {
|
|
102
107
|
const sessionId = normalizeOptionalString(target.itermSessionId);
|
|
103
108
|
if (!sessionId) {
|
|
104
|
-
return
|
|
109
|
+
return "unknown";
|
|
105
110
|
}
|
|
106
111
|
const it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
|
|
107
112
|
const result = await this.execAsync(it2api, ["list-sessions"]);
|
|
108
|
-
return result.stdout.includes(sessionId);
|
|
113
|
+
return result.stdout.includes(sessionId) ? "available" : "gone";
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
|
-
catch {
|
|
112
|
-
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (target.backend === "tmux" && isTmuxMissingPaneError(error)) {
|
|
118
|
+
return "gone";
|
|
119
|
+
}
|
|
120
|
+
return "unknown";
|
|
113
121
|
}
|
|
114
|
-
return
|
|
122
|
+
return "unknown";
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
exports.TerminalDispatcher = TerminalDispatcher;
|
|
126
|
+
function isTmuxMissingPaneError(error) {
|
|
127
|
+
if (!(error instanceof Error)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const candidate = `${error.message} ${"stderr" in error ? String(error.stderr ?? "") : ""}`;
|
|
131
|
+
return candidate.includes("can't find pane");
|
|
132
|
+
}
|
|
118
133
|
function detectTty(env) {
|
|
119
134
|
const direct = tryReadCurrentTty();
|
|
120
135
|
if (direct) {
|