agent-office-cli 0.1.4 → 0.1.6
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/package.json +1 -1
- package/src/core/providers/codex.js +54 -24
- package/src/core/providers/codex.test.js +30 -0
- package/src/index.js +8 -0
- package/src/runtime/index.js +2 -0
- package/src/runtime/pty-manager.js +13 -4
- package/src/runtime/sleep-inhibitor.js +31 -0
- package/src/runtime/sleep-inhibitor.test.js +63 -0
package/package.json
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
|
+
const { displayZoneFor } = require("../state");
|
|
1
2
|
const { GenericProvider } = require("./generic");
|
|
2
3
|
const { findManagedCodexSessionFile, summarizeCodexSession } = require("./codex-transcript");
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const APPROVAL_LINE_PATTERNS = [
|
|
6
|
+
/^approval requested:/i,
|
|
7
|
+
/^approval requested by /i,
|
|
8
|
+
/^tool call needs your approval$/i,
|
|
9
|
+
/^requires approval by policy$/i,
|
|
10
|
+
/^requires approval:/i
|
|
10
11
|
];
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"something went wrong? hit /feedback to"
|
|
13
|
+
const IDLE_LINE_PATTERNS = [
|
|
14
|
+
/^conversation interrupted - tell the model what to do differently/i,
|
|
15
|
+
/^something went wrong\? hit `?\/feedback`? to/i
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
18
|
+
const STATUS_LINE_CONTINUATION = String.raw`(?:$|[\s:.,;(])`;
|
|
19
|
+
|
|
20
|
+
const ATTENTION_LINE_PATTERNS = [
|
|
21
|
+
new RegExp(`^stream disconnected before completion${STATUS_LINE_CONTINUATION}`, "i"),
|
|
22
|
+
new RegExp(`^error sending request for url${STATUS_LINE_CONTINUATION}`, "i"),
|
|
23
|
+
new RegExp(`^network error${STATUS_LINE_CONTINUATION}`, "i"),
|
|
24
|
+
new RegExp(`^connection timeout${STATUS_LINE_CONTINUATION}`, "i"),
|
|
25
|
+
new RegExp(`^timed out${STATUS_LINE_CONTINUATION}`, "i"),
|
|
26
|
+
new RegExp(`^failed to send request${STATUS_LINE_CONTINUATION}`, "i"),
|
|
27
|
+
new RegExp(`^failed to submit${STATUS_LINE_CONTINUATION}`, "i"),
|
|
28
|
+
new RegExp(`^panic${STATUS_LINE_CONTINUATION}`, "i")
|
|
27
29
|
];
|
|
28
30
|
|
|
31
|
+
function matchesAnyLine(text, patterns) {
|
|
32
|
+
return String(text)
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.some((line) => patterns.some((pattern) => pattern.test(line)));
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
function activeOverlayPatch(session, nextLifecycleState) {
|
|
30
40
|
if (!session || !["approval", "attention"].includes(session.displayState)) {
|
|
31
41
|
return null;
|
|
@@ -72,23 +82,43 @@ class CodexProvider extends GenericProvider {
|
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
classifyOutput(chunk) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (IDLE_PATTERNS.some((pattern) => text.includes(pattern))) {
|
|
85
|
+
if (matchesAnyLine(chunk, IDLE_LINE_PATTERNS)) {
|
|
78
86
|
return "idle";
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
if (
|
|
89
|
+
if (matchesAnyLine(chunk, APPROVAL_LINE_PATTERNS)) {
|
|
82
90
|
return "approval";
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
if (
|
|
93
|
+
if (matchesAnyLine(chunk, ATTENTION_LINE_PATTERNS)) {
|
|
86
94
|
return "attention";
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
return null;
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
getOverlayDisplayPatch(session, overlayState) {
|
|
101
|
+
if (!overlayState) {
|
|
102
|
+
if (session && session.displayState === "attention") {
|
|
103
|
+
const nextDisplayState = session.state || "working";
|
|
104
|
+
return {
|
|
105
|
+
displayState: nextDisplayState,
|
|
106
|
+
displayZone: displayZoneFor(nextDisplayState)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (session && overlayState === session.displayState) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
displayState: overlayState,
|
|
118
|
+
displayZone: displayZoneFor(overlayState)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
reconcileSession(session, context = {}) {
|
|
93
123
|
if (session.status === "exited") {
|
|
94
124
|
return null;
|
|
@@ -91,6 +91,19 @@ test("classifyOutput treats stream disconnects as attention", () => {
|
|
|
91
91
|
assert.equal(nextState, "attention");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
test("classifyOutput ignores diagnostic text that only mentions attention patterns", () => {
|
|
95
|
+
const provider = new CodexProvider();
|
|
96
|
+
const nextState = provider.classifyOutput(
|
|
97
|
+
[
|
|
98
|
+
'rg -n -i "conversation interrupted|error sending request for url|network error|timed out|failed to send request|failed to submit|panic|fetch failed"',
|
|
99
|
+
'const nextState = provider.classifyOutput("network error: connection timed out", { meta: { codexSessionPath: "/tmp/mock-codex.jsonl" } });',
|
|
100
|
+
'The changelog says stream disconnected before completion should surface as attention.'
|
|
101
|
+
].join("\n")
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
assert.equal(nextState, null);
|
|
105
|
+
});
|
|
106
|
+
|
|
94
107
|
test("classifyOutput does not treat plain explanatory approval text as a real approval prompt", () => {
|
|
95
108
|
const provider = new CodexProvider();
|
|
96
109
|
const nextState = provider.classifyOutput(
|
|
@@ -108,3 +121,20 @@ test("classifyOutput recognizes real Codex approval prompts", () => {
|
|
|
108
121
|
|
|
109
122
|
assert.equal(nextState, "approval");
|
|
110
123
|
});
|
|
124
|
+
|
|
125
|
+
test("getOverlayDisplayPatch clears stale attention overlays back to the lifecycle state", () => {
|
|
126
|
+
const provider = new CodexProvider();
|
|
127
|
+
const patch = provider.getOverlayDisplayPatch(
|
|
128
|
+
{
|
|
129
|
+
state: "working",
|
|
130
|
+
displayState: "attention",
|
|
131
|
+
displayZone: "attention-zone"
|
|
132
|
+
},
|
|
133
|
+
null
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
assert.deepEqual(patch, {
|
|
137
|
+
displayState: "working",
|
|
138
|
+
displayZone: "working-zone"
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
createPtyManager,
|
|
20
20
|
defaultTransportForProvider,
|
|
21
21
|
ensureNodePtySpawnHelper,
|
|
22
|
+
startSleepInhibitor,
|
|
22
23
|
listSessionRecords,
|
|
23
24
|
removeSessionRecord,
|
|
24
25
|
applyClaudeHookConfig,
|
|
@@ -172,6 +173,13 @@ async function main() {
|
|
|
172
173
|
throw new Error("tmux is required for AgentOffice local sessions. Install it first, for example with `brew install tmux`.");
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
const sleepInhibitor = startSleepInhibitor({ commandExists });
|
|
177
|
+
if (sleepInhibitor.started) {
|
|
178
|
+
console.log("- sleep guard: caffeinate active");
|
|
179
|
+
} else if (process.platform === "darwin") {
|
|
180
|
+
console.log("- sleep guard: skipped (caffeinate unavailable)");
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
const store = createSessionStore();
|
|
176
184
|
const ptyManager = createPtyManager({ store });
|
|
177
185
|
const restored = ptyManager.restoreManagedSessions();
|
package/src/runtime/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
removeSessionRecord
|
|
19
19
|
} = require("./session-registry");
|
|
20
20
|
const { ensureNodePtySpawnHelper } = require("./ensure-node-pty");
|
|
21
|
+
const { startSleepInhibitor } = require("./sleep-inhibitor");
|
|
21
22
|
const {
|
|
22
23
|
applyClaudeHookConfig,
|
|
23
24
|
claudeSettingsPath,
|
|
@@ -45,6 +46,7 @@ module.exports = {
|
|
|
45
46
|
persistSessionRecord,
|
|
46
47
|
removeSessionRecord,
|
|
47
48
|
ensureNodePtySpawnHelper,
|
|
49
|
+
startSleepInhibitor,
|
|
48
50
|
applyClaudeHookConfig,
|
|
49
51
|
claudeSettingsPath,
|
|
50
52
|
commandExists,
|
|
@@ -353,12 +353,21 @@ function createPtyManager({ store }) {
|
|
|
353
353
|
const screen = await capturePane(runtime.tmuxSession);
|
|
354
354
|
const latestSession = store.getSession(session.sessionId) || session;
|
|
355
355
|
const overlayState = runtime.provider.classifyOutput(screen, latestSession);
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
const overlayPatch = typeof runtime.provider.getOverlayDisplayPatch === "function"
|
|
357
|
+
? runtime.provider.getOverlayDisplayPatch(latestSession, overlayState)
|
|
358
|
+
: (
|
|
359
|
+
overlayState && overlayState !== latestSession.displayState
|
|
360
|
+
? {
|
|
361
|
+
displayState: overlayState,
|
|
362
|
+
displayZone: displayZoneFor(overlayState)
|
|
363
|
+
}
|
|
364
|
+
: null
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (overlayPatch) {
|
|
358
368
|
store.setSessionState(session.sessionId, latestSession.state || "working", {
|
|
359
369
|
status: "running",
|
|
360
|
-
|
|
361
|
-
displayZone: displayZoneFor(overlayState)
|
|
370
|
+
...overlayPatch
|
|
362
371
|
});
|
|
363
372
|
}
|
|
364
373
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
|
|
3
|
+
function startSleepInhibitor({
|
|
4
|
+
pid = process.pid,
|
|
5
|
+
platform = process.platform,
|
|
6
|
+
commandExists = () => true,
|
|
7
|
+
spawn: spawnProcess = spawn
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (platform !== "darwin") {
|
|
10
|
+
return { started: false, reason: "unsupported_platform" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!commandExists("caffeinate")) {
|
|
14
|
+
return { started: false, reason: "missing_command" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawnProcess("caffeinate", ["-dimsu", "-w", String(pid)], {
|
|
18
|
+
stdio: "ignore"
|
|
19
|
+
});
|
|
20
|
+
child.unref?.();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
started: true,
|
|
24
|
+
reason: "started",
|
|
25
|
+
child
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
startSleepInhibitor
|
|
31
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
|
|
4
|
+
const { startSleepInhibitor } = require("./sleep-inhibitor");
|
|
5
|
+
|
|
6
|
+
test("startSleepInhibitor starts caffeinate by default on macOS", () => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
const child = {
|
|
9
|
+
unrefCalled: false,
|
|
10
|
+
unref() {
|
|
11
|
+
this.unrefCalled = true;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const result = startSleepInhibitor({
|
|
16
|
+
pid: 4321,
|
|
17
|
+
platform: "darwin",
|
|
18
|
+
commandExists: (command) => command === "caffeinate",
|
|
19
|
+
spawn: (command, args, options) => {
|
|
20
|
+
calls.push({ command, args, options });
|
|
21
|
+
return child;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(calls, [
|
|
26
|
+
{
|
|
27
|
+
command: "caffeinate",
|
|
28
|
+
args: ["-dimsu", "-w", "4321"],
|
|
29
|
+
options: { stdio: "ignore" }
|
|
30
|
+
}
|
|
31
|
+
]);
|
|
32
|
+
assert.equal(child.unrefCalled, true);
|
|
33
|
+
assert.equal(result.started, true);
|
|
34
|
+
assert.equal(result.reason, "started");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("startSleepInhibitor skips non-macOS platforms", () => {
|
|
38
|
+
const result = startSleepInhibitor({
|
|
39
|
+
pid: 4321,
|
|
40
|
+
platform: "linux",
|
|
41
|
+
commandExists: () => true,
|
|
42
|
+
spawn: () => {
|
|
43
|
+
throw new Error("spawn should not be called");
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(result.started, false);
|
|
48
|
+
assert.equal(result.reason, "unsupported_platform");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("startSleepInhibitor skips when caffeinate is unavailable", () => {
|
|
52
|
+
const result = startSleepInhibitor({
|
|
53
|
+
pid: 4321,
|
|
54
|
+
platform: "darwin",
|
|
55
|
+
commandExists: () => false,
|
|
56
|
+
spawn: () => {
|
|
57
|
+
throw new Error("spawn should not be called");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
assert.equal(result.started, false);
|
|
62
|
+
assert.equal(result.reason, "missing_command");
|
|
63
|
+
});
|