khala-cli 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2U4E7AM7.mjs → chunk-25AX7UMF.mjs} +294 -101
- package/dist/{chunk-AUJEUKVM.mjs → chunk-3OFKVYOX.mjs} +1 -1
- package/dist/{chunk-AWBKVTLV.mjs → chunk-4LSCOWWN.mjs} +13 -14
- package/dist/chunk-5RGEO5HN.mjs +142 -0
- package/dist/{chunk-B2MOLCJM.mjs → chunk-6YEYXZZB.mjs} +8 -9
- package/dist/{chunk-2XTYRBYX.mjs → chunk-7V2YQIRI.mjs} +4 -4
- package/dist/{chunk-55KBLEX5.mjs → chunk-APIWKRKG.mjs} +1 -1
- package/dist/{chunk-PY2ZNVNY.mjs → chunk-C5SMOUTO.mjs} +34 -5
- package/dist/{chunk-JG5J3SHW.mjs → chunk-IG2NU5J6.mjs} +4 -4
- package/dist/chunk-JEI32DEA.mjs +634 -0
- package/dist/{chunk-4X35OUAN.mjs → chunk-K6EJSZYO.mjs} +92 -95
- package/dist/{chunk-FH2CZX5T.mjs → chunk-L7L6UNXN.mjs} +83 -36
- package/dist/{chunk-P5CP32X2.mjs → chunk-LNG3ZZBK.mjs} +1 -1
- package/dist/{chunk-CWHGNVZV.mjs → chunk-OAFYPBCP.mjs} +1 -1
- package/dist/{chunk-OECJ45NM.mjs → chunk-ODTSGEID.mjs} +24 -8
- package/dist/{chunk-XUJIJ4KB.mjs → chunk-OP2HZ7EM.mjs} +1 -1
- package/dist/{chunk-6E3UCO22.mjs → chunk-S3FX7VCY.mjs} +8 -11
- package/dist/{chunk-PFDUXY3N.mjs → chunk-SU6362YU.mjs} +1 -1
- package/dist/{chunk-Z4EDYB6Y.mjs → chunk-TOBQUAPD.mjs} +59 -43
- package/dist/{chunk-SN3STAO2.mjs → chunk-U3MKSPRO.mjs} +40 -3
- package/dist/{chunk-SZIEG3RS.mjs → chunk-W5HS2PQO.mjs} +12 -14
- package/dist/{chunk-OW3DSTEZ.mjs → chunk-WNOIHCCC.mjs} +5 -5
- package/dist/{chunk-RRBIVI77.mjs → chunk-YIHRHD7W.mjs} +1 -1
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-BQl2KrE1.js +9 -0
- package/dist/web/assets/local-OwDq47O4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-DPW43YNC.mjs +0 -36
- package/dist/chunk-IULQYQJZ.mjs +0 -144
- package/dist/chunk-WHONG7UF.mjs +0 -267
- package/dist/chunk-ZHMGB3PP.mjs +0 -82
- package/dist/web/assets/local-K8EcTVxW.js +0 -9
- package/dist/web/assets/local-R2y0Gl4T.css +0 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
advanceCursor,
|
|
4
|
+
listChannels,
|
|
5
|
+
readCursor,
|
|
6
|
+
unread
|
|
7
|
+
} from "./chunk-OP2HZ7EM.mjs";
|
|
8
|
+
import {
|
|
9
|
+
SESSION_ID_PATTERN,
|
|
10
|
+
StateError,
|
|
11
|
+
readJson,
|
|
12
|
+
stateRoot,
|
|
13
|
+
writeJsonAtomic
|
|
14
|
+
} from "./chunk-WNOIHCCC.mjs";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_LISTENING_MODE,
|
|
17
|
+
decodeListeningMode
|
|
18
|
+
} from "./chunk-3OFKVYOX.mjs";
|
|
19
|
+
import {
|
|
20
|
+
HARNESS_REGISTRY
|
|
21
|
+
} from "./chunk-U3MKSPRO.mjs";
|
|
22
|
+
|
|
23
|
+
// src/mode.ts
|
|
24
|
+
async function readListeningMode(files) {
|
|
25
|
+
const content = await readJson(files.mode);
|
|
26
|
+
const decoded = decodeListeningMode(typeof content === "object" && content !== null ? content.mode : void 0);
|
|
27
|
+
return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
|
|
28
|
+
}
|
|
29
|
+
async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */ new Date()) {
|
|
30
|
+
const previous = await readListeningMode(files);
|
|
31
|
+
if (previous === "async" && next !== "async") {
|
|
32
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
33
|
+
const { cursor: cursor2, entries } = await unread(files);
|
|
34
|
+
if (await advanceCursor(files, cursor2, entries) !== "conflict") break;
|
|
35
|
+
if (attempt === 1) throw new StateError("storage_failed");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await writeJsonAtomic(files.mode, { mode: next, ...meta, updatedAt: now().toISOString() });
|
|
39
|
+
return { previous, next };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/harness/codecs/claude-style.ts
|
|
43
|
+
var events = { SessionStart: "start", UserPromptSubmit: "prompt", PostToolUse: "tool", Stop: "stop" };
|
|
44
|
+
var names = { prompt: "UserPromptSubmit", tool: "PostToolUse", stop: "Stop" };
|
|
45
|
+
var claudeStyleCodec = {
|
|
46
|
+
promptDeliversWithoutWake: true,
|
|
47
|
+
promptAcceptsContext: true,
|
|
48
|
+
suppressOutputErrors: false,
|
|
49
|
+
parse(stdin) {
|
|
50
|
+
try {
|
|
51
|
+
const input = JSON.parse(stdin);
|
|
52
|
+
if (!input || typeof input.session_id !== "string" || typeof input.hook_event_name !== "string" || !Object.hasOwn(events, input.hook_event_name) || input.stop_hook_active !== void 0 && typeof input.stop_hook_active !== "boolean") return null;
|
|
53
|
+
return {
|
|
54
|
+
sessionId: input.session_id,
|
|
55
|
+
event: events[input.hook_event_name],
|
|
56
|
+
continuation: input.stop_hook_active === true,
|
|
57
|
+
...typeof input.cwd === "string" ? { workspace: input.cwd } : {},
|
|
58
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
noop: () => "",
|
|
65
|
+
render(kind, frame) {
|
|
66
|
+
return JSON.stringify(kind === "stop" ? { decision: "block", reason: frame } : { hookSpecificOutput: { hookEventName: names[kind], additionalContext: frame } }) + "\n";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/harness/claude.ts
|
|
71
|
+
var claude = {
|
|
72
|
+
id: "claude",
|
|
73
|
+
sessionSources: [{ kind: "env", resolve: (_meta, env) => env.CLAUDE_CODE_SESSION_ID, rejoinable: () => true }],
|
|
74
|
+
codec: claudeStyleCodec,
|
|
75
|
+
restoreAtStartup: true,
|
|
76
|
+
watcherStatus: true
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/wake/shared/rules.ts
|
|
80
|
+
import { randomBytes } from "node:crypto";
|
|
81
|
+
var CODEX_IDLE_WAKE_NOTICE = "Khala: channel messages are waiting. Continue.";
|
|
82
|
+
function newNonce() {
|
|
83
|
+
return randomBytes(4).toString("hex");
|
|
84
|
+
}
|
|
85
|
+
function wakeLine(nonce) {
|
|
86
|
+
if (!/^[a-f0-9]{8}$/.test(nonce)) throw new Error("invalid_wake_nonce");
|
|
87
|
+
return `${CODEX_IDLE_WAKE_NOTICE} (k-${nonce})`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/wake/idle-wake.ts
|
|
91
|
+
function codexIdleWakeArgv(sessionId) {
|
|
92
|
+
return ["queue", "--thread", sessionId, "--message", CODEX_IDLE_WAKE_NOTICE];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/wake/idle-wake-process.ts
|
|
96
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
97
|
+
var CODEX_QUEUE_ENV_ALLOWLIST = [
|
|
98
|
+
"PATH",
|
|
99
|
+
"HOME",
|
|
100
|
+
"USER",
|
|
101
|
+
"LOGNAME",
|
|
102
|
+
"SHELL",
|
|
103
|
+
"LANG",
|
|
104
|
+
"LC_ALL",
|
|
105
|
+
"TMPDIR",
|
|
106
|
+
"CODEX_HOME",
|
|
107
|
+
"XDG_CONFIG_HOME",
|
|
108
|
+
"XDG_DATA_HOME",
|
|
109
|
+
"XDG_STATE_HOME",
|
|
110
|
+
"XDG_RUNTIME_DIR"
|
|
111
|
+
];
|
|
112
|
+
function scrubbedQueueEnv(source) {
|
|
113
|
+
const env = {};
|
|
114
|
+
for (const name of CODEX_QUEUE_ENV_ALLOWLIST) {
|
|
115
|
+
const value = source[name];
|
|
116
|
+
if (value !== void 0) env[name] = value;
|
|
117
|
+
}
|
|
118
|
+
return env;
|
|
119
|
+
}
|
|
120
|
+
function createCodexQueueProcessPort(deps) {
|
|
121
|
+
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
122
|
+
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
123
|
+
return {
|
|
124
|
+
run(argv, signal) {
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
if (signal.aborted) return resolve({ status: "not_started" });
|
|
127
|
+
let child;
|
|
128
|
+
try {
|
|
129
|
+
child = spawn(deps.command, argv, { shell: false, stdio: "ignore", env: scrubbedQueueEnv(deps.env) });
|
|
130
|
+
} catch {
|
|
131
|
+
return resolve({ status: "not_started" });
|
|
132
|
+
}
|
|
133
|
+
let timedOut = false;
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let force = null;
|
|
136
|
+
const stop = () => {
|
|
137
|
+
if (stopped) return;
|
|
138
|
+
stopped = true;
|
|
139
|
+
child.kill("SIGTERM");
|
|
140
|
+
force = setTimeout(() => {
|
|
141
|
+
child.kill("SIGKILL");
|
|
142
|
+
}, 500);
|
|
143
|
+
};
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
stop();
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
149
|
+
const settle = (outcome) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
if (force !== null) clearTimeout(force);
|
|
152
|
+
signal.removeEventListener("abort", stop);
|
|
153
|
+
resolve(outcome);
|
|
154
|
+
};
|
|
155
|
+
child.on("error", () => settle({ status: "not_started" }));
|
|
156
|
+
child.on("close", (code) => settle(timedOut ? { status: "lost", cause: "timeout" } : code === 0 ? { status: "queued" } : { status: "exited", code: code ?? -1 }));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/wake/ladder.ts
|
|
163
|
+
import path4 from "node:path";
|
|
164
|
+
|
|
165
|
+
// src/activity.ts
|
|
166
|
+
import path from "node:path";
|
|
167
|
+
async function readActivity(files) {
|
|
168
|
+
const activity = await readJson(path.join(files.dir, "activity.json"));
|
|
169
|
+
return activity && ["idle", "busy"].includes(activity.state) && typeof activity.updatedAt === "string" && Number.isFinite(Date.parse(activity.updatedAt)) ? activity : { state: "busy", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
170
|
+
}
|
|
171
|
+
async function writeActivity(files, state, now = () => /* @__PURE__ */ new Date()) {
|
|
172
|
+
const activity = { state, updatedAt: now().toISOString() };
|
|
173
|
+
await writeJsonAtomic(path.join(files.dir, "activity.json"), activity);
|
|
174
|
+
return activity;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/wake/shared/settings.ts
|
|
178
|
+
import * as fs from "node:fs/promises";
|
|
179
|
+
import path2 from "node:path";
|
|
180
|
+
async function readWakeSettings(root) {
|
|
181
|
+
try {
|
|
182
|
+
const value = JSON.parse(await fs.readFile(path2.join(root, "wake-settings.json"), "utf8"));
|
|
183
|
+
const entries = (input) => {
|
|
184
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
185
|
+
return Object.fromEntries(Object.entries(input).filter(([, entry]) => entry && typeof entry === "object" && typeof entry.at === "string"));
|
|
186
|
+
};
|
|
187
|
+
return { consent: entries(value?.consent), off: entries(value?.off) };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error.code === "ENOENT") return { consent: {}, off: {} };
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function driverAllowed(root, harness, driver, optIn) {
|
|
194
|
+
const settings = await readWakeSettings(root);
|
|
195
|
+
const key = `${harness}/${driver}`;
|
|
196
|
+
return !Object.hasOwn(settings.off, key) && (!optIn || Object.hasOwn(settings.consent, key));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/wake/shared/nonce.ts
|
|
200
|
+
import * as fs2 from "node:fs/promises";
|
|
201
|
+
import path3 from "node:path";
|
|
202
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
203
|
+
async function readJson2(file, fallback) {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(await fs2.readFile(file, "utf8"));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error.code === "ENOENT") return fallback;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function atomic(file, value) {
|
|
212
|
+
const temp = `${file}.${randomBytes2(8).toString("hex")}.tmp`;
|
|
213
|
+
try {
|
|
214
|
+
await fs2.writeFile(temp, JSON.stringify(value), { mode: 384 });
|
|
215
|
+
await fs2.rename(temp, file);
|
|
216
|
+
} finally {
|
|
217
|
+
await fs2.rm(temp, { force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function clearAbandonedLock(lock) {
|
|
221
|
+
let entries;
|
|
222
|
+
try {
|
|
223
|
+
entries = await fs2.readdir(lock);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error.code === "ENOENT") return;
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const owner = /^owner-(\d+)-[a-f0-9]{16}$/.exec(entry);
|
|
230
|
+
if (!owner) continue;
|
|
231
|
+
try {
|
|
232
|
+
process.kill(Number(owner[1]), 0);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error.code !== "ESRCH") continue;
|
|
235
|
+
await fs2.rm(path3.join(lock, entry), { force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await fs2.rmdir(lock);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function locked(dir, work) {
|
|
245
|
+
await fs2.mkdir(dir, { recursive: true, mode: 448 });
|
|
246
|
+
const lock = path3.join(dir, "wake.lock");
|
|
247
|
+
const owner = `owner-${process.pid}-${randomBytes2(8).toString("hex")}`;
|
|
248
|
+
const candidate = path3.join(dir, `.${owner}`);
|
|
249
|
+
await fs2.mkdir(candidate, { mode: 448 });
|
|
250
|
+
await fs2.writeFile(path3.join(candidate, owner), "", { mode: 384 });
|
|
251
|
+
let acquired = false;
|
|
252
|
+
try {
|
|
253
|
+
for (let tries = 0; tries < 200; tries++) {
|
|
254
|
+
try {
|
|
255
|
+
await fs2.rename(candidate, lock);
|
|
256
|
+
acquired = true;
|
|
257
|
+
break;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code ?? "")) throw error;
|
|
260
|
+
}
|
|
261
|
+
await clearAbandonedLock(lock);
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
263
|
+
}
|
|
264
|
+
} finally {
|
|
265
|
+
if (!acquired) await fs2.rm(candidate, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
if (!acquired) throw new Error("wake_state_locked");
|
|
268
|
+
try {
|
|
269
|
+
const data = await readJson2(path3.join(dir, "wake-journal.json"), { attempts: [], state: {} });
|
|
270
|
+
const result = work(data);
|
|
271
|
+
await atomic(path3.join(dir, "wake-journal.json"), data);
|
|
272
|
+
await atomic(path3.join(dir, "wake-state.json"), data.state);
|
|
273
|
+
return result;
|
|
274
|
+
} finally {
|
|
275
|
+
await fs2.rm(path3.join(lock, owner), { force: true });
|
|
276
|
+
try {
|
|
277
|
+
await fs2.rmdir(lock);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function readWakeState(dir) {
|
|
284
|
+
return (await readJson2(path3.join(dir, "wake-journal.json"), { attempts: [], state: {} })).state;
|
|
285
|
+
}
|
|
286
|
+
async function recordAttempt(dir, attempt) {
|
|
287
|
+
wakeLine(attempt.nonce);
|
|
288
|
+
if (!attempt.driver || ["__proto__", "constructor", "prototype"].includes(attempt.driver) || !Number.isFinite(attempt.at) || !Number.isFinite(attempt.deadline) || attempt.deadline <= attempt.at) throw new Error("invalid_wake_attempt");
|
|
289
|
+
await locked(dir, (data) => {
|
|
290
|
+
if (data.attempts.some((item) => item.nonce === attempt.nonce)) throw new Error("duplicate_wake_nonce");
|
|
291
|
+
data.attempts.push(attempt);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async function settleAttempts(dir, input) {
|
|
295
|
+
const snapshot = await readJson2(path3.join(dir, "wake-journal.json"), { attempts: [], state: {} });
|
|
296
|
+
if (!snapshot.attempts.length) return [];
|
|
297
|
+
return locked(dir, (data) => {
|
|
298
|
+
const results = [];
|
|
299
|
+
const remaining = [];
|
|
300
|
+
const activityAt = typeof input.activity?.updatedAt === "number" ? input.activity.updatedAt : Date.parse(input.activity?.updatedAt ?? "");
|
|
301
|
+
for (const attempt of data.attempts) {
|
|
302
|
+
let status;
|
|
303
|
+
const baselineAt = typeof attempt.activityUpdatedAt === "number" ? attempt.activityUpdatedAt : attempt.activityUpdatedAt === void 0 ? attempt.at : Date.parse(attempt.activityUpdatedAt);
|
|
304
|
+
const verified = input.promptText?.includes(`(k-${attempt.nonce})`);
|
|
305
|
+
const activityChanged = input.activity && (input.activity.state !== "idle" || activityAt > baselineAt);
|
|
306
|
+
if (activityChanged) status = "void";
|
|
307
|
+
else if (verified && input.now <= attempt.deadline) status = "success";
|
|
308
|
+
else if (input.now >= attempt.deadline && input.activity?.state === "idle") status = "failure";
|
|
309
|
+
else if (input.promptText !== void 0) status = "void";
|
|
310
|
+
if (!status) {
|
|
311
|
+
remaining.push(attempt);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
results.push({ nonce: attempt.nonce, driver: attempt.driver, status });
|
|
315
|
+
if (status === "void") continue;
|
|
316
|
+
const prior = data.state[attempt.driver] ?? { failures: 0 };
|
|
317
|
+
if (status === "success") data.state[attempt.driver] = { failures: 0 };
|
|
318
|
+
else {
|
|
319
|
+
const failures = prior.failures + 1;
|
|
320
|
+
data.state[attempt.driver] = failures >= 2 ? { failures, disabled: true, reason: "nonce_timeout", at: new Date(input.now).toISOString() } : { failures };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
data.attempts = remaining;
|
|
324
|
+
return results;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/wake/ladder.ts
|
|
329
|
+
function createWakeLadder(deps) {
|
|
330
|
+
if (!SESSION_ID_PATTERN.test(deps.sessionId)) throw new TypeError("invalid_session_id");
|
|
331
|
+
const now = deps.now ?? Date.now;
|
|
332
|
+
const stderr = deps.stderr ?? ((line) => {
|
|
333
|
+
process.stderr.write(line);
|
|
334
|
+
});
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
let pending;
|
|
337
|
+
const wakesAtCount = /* @__PURE__ */ new Map();
|
|
338
|
+
const identities = /* @__PURE__ */ new Map();
|
|
339
|
+
const disabledDrivers = /* @__PURE__ */ new Set();
|
|
340
|
+
let stopped = false;
|
|
341
|
+
let again = false;
|
|
342
|
+
let inFlight;
|
|
343
|
+
const warn = (warning, status) => {
|
|
344
|
+
try {
|
|
345
|
+
stderr(JSON.stringify({ ok: false, warning, ...status ? { status } : {} }) + "\n");
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const evaluate = async () => {
|
|
350
|
+
try {
|
|
351
|
+
if (deps.drivers.some((driver2) => driver2.verification !== "none")) await settleAttempts(deps.files.dir, { now: now(), activity: await readActivity(deps.files) });
|
|
352
|
+
const driverStates = await readWakeState(deps.files.dir);
|
|
353
|
+
for (const [id, state] of Object.entries(driverStates)) {
|
|
354
|
+
if (state.disabled && !disabledDrivers.has(id)) {
|
|
355
|
+
disabledDrivers.add(id);
|
|
356
|
+
wakesAtCount.clear();
|
|
357
|
+
pending = void 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const channels = await listChannels(deps.files);
|
|
361
|
+
const eligible = [];
|
|
362
|
+
const currentIdentities = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const channel of channels) {
|
|
364
|
+
const session = await readJson(channel.files.session);
|
|
365
|
+
const metadata = channel.legacy ? null : await readJson(path4.join(channel.files.dir, "channel.json"));
|
|
366
|
+
currentIdentities.set(channel.key, JSON.stringify([session?.roomId ?? channel.roomId, session?.userId, metadata?.joinedAt]));
|
|
367
|
+
if (await readListeningMode(channel.files) === "async") continue;
|
|
368
|
+
eligible.push({
|
|
369
|
+
channel,
|
|
370
|
+
counts: { messages: (await unread(channel.files)).entries.filter((entry) => entry.kind === "message" && entry.sender !== session?.userId).length },
|
|
371
|
+
cursor: await readCursor(channel.files)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const keys = new Set(currentIdentities.keys());
|
|
375
|
+
const changed = /* @__PURE__ */ new Set();
|
|
376
|
+
for (const [key, identity] of currentIdentities) {
|
|
377
|
+
if (identities.get(key) !== identity) changed.add(key);
|
|
378
|
+
}
|
|
379
|
+
for (const key of identities.keys()) if (!keys.has(key)) changed.add(key);
|
|
380
|
+
if (changed.size) {
|
|
381
|
+
for (const [key, wakes2] of [...wakesAtCount]) {
|
|
382
|
+
const pairs2 = JSON.parse(key);
|
|
383
|
+
if (!pairs2.some(([id]) => changed.has(id))) continue;
|
|
384
|
+
wakesAtCount.delete(key);
|
|
385
|
+
const remainingPairs = pairs2.filter(([id]) => !changed.has(id));
|
|
386
|
+
if (!remainingPairs.length) continue;
|
|
387
|
+
const remaining = JSON.stringify(remainingPairs);
|
|
388
|
+
wakesAtCount.set(remaining, Math.max(wakes2, wakesAtCount.get(remaining) ?? 0));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const key of changed) identities.delete(key);
|
|
392
|
+
for (const [key, identity] of currentIdentities) identities.set(key, identity);
|
|
393
|
+
const pairs = eligible.map(({ channel, cursor: cursor2 }) => [channel.key, cursor2.deliveredCount]).sort(([a], [b]) => a.localeCompare(b));
|
|
394
|
+
const budgetKey = JSON.stringify(pairs);
|
|
395
|
+
if (changed.size) {
|
|
396
|
+
const inheritedKey = JSON.stringify(pairs.filter(([key]) => !changed.has(key)));
|
|
397
|
+
const newUnread = eligible.some((item) => changed.has(item.channel.key) && item.counts.messages > 0);
|
|
398
|
+
wakesAtCount.set(budgetKey, newUnread ? 0 : wakesAtCount.get(inheritedKey) ?? 0);
|
|
399
|
+
if (newUnread || !eligible.some((item) => !changed.has(item.channel.key) && item.counts.messages > 0)) pending = void 0;
|
|
400
|
+
}
|
|
401
|
+
if (!eligible.some((item) => item.counts.messages > 0)) {
|
|
402
|
+
pending = void 0;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const activity = await readActivity(deps.files);
|
|
406
|
+
if (pending && Date.parse(activity.updatedAt) > pending.at) pending = void 0;
|
|
407
|
+
const at = now();
|
|
408
|
+
if (pending && at - pending.at < (deps.retryAfterMs ?? 6e4)) return;
|
|
409
|
+
if (stopped || activity.state !== "idle") return;
|
|
410
|
+
const wakes = wakesAtCount.get(budgetKey) ?? 0;
|
|
411
|
+
if (wakes >= 2) return;
|
|
412
|
+
for (const { channel } of eligible) {
|
|
413
|
+
if (await readListeningMode(channel.files) === "async") {
|
|
414
|
+
pending = void 0;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (stopped) return;
|
|
419
|
+
const ctx = { files: deps.files, harness: deps.harness, sessionId: deps.sessionId, env: deps.env ?? process.env, signal: controller.signal, now: at };
|
|
420
|
+
const driver = await chooseWakeDriver(deps.drivers, ctx, activity);
|
|
421
|
+
if (!driver || stopped) return;
|
|
422
|
+
const stillEligible = async () => {
|
|
423
|
+
if (stopped) return false;
|
|
424
|
+
if (!await driverAllowed(stateRoot(ctx.env), ctx.harness, driver.id, driver.optIn)) return false;
|
|
425
|
+
if ((await readWakeState(deps.files.dir))[driver.id]?.disabled) return false;
|
|
426
|
+
for (const { channel } of eligible) {
|
|
427
|
+
if (await readListeningMode(channel.files) === "async") return false;
|
|
428
|
+
}
|
|
429
|
+
const current = await readActivity(deps.files);
|
|
430
|
+
return !stopped && current.state === "idle" && current.updatedAt === activity.updatedAt;
|
|
431
|
+
};
|
|
432
|
+
if (!await stillEligible()) return;
|
|
433
|
+
const attemptAt = now();
|
|
434
|
+
const nonce = newNonce();
|
|
435
|
+
if (driver.verification !== "none") {
|
|
436
|
+
await recordAttempt(deps.files.dir, { nonce, driver: driver.id, at: attemptAt, deadline: attemptAt + driver.deadlineMs, activityUpdatedAt: activity.updatedAt });
|
|
437
|
+
if (!await stillEligible()) {
|
|
438
|
+
await settleAttempts(deps.files.dir, { now: now(), activity: await readActivity(deps.files), promptText: "" });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (stopped) return;
|
|
443
|
+
pending = { at: attemptAt };
|
|
444
|
+
wakesAtCount.set(budgetKey, wakes + 1);
|
|
445
|
+
await driver.wake({ ...ctx, now: attemptAt }, wakeLine(nonce));
|
|
446
|
+
} catch {
|
|
447
|
+
warn(`${deps.warningPrefix ?? "wake"}_waker_error`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const notify = () => {
|
|
451
|
+
if (stopped) return;
|
|
452
|
+
if (inFlight) {
|
|
453
|
+
again = true;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
inFlight = (async () => {
|
|
457
|
+
do {
|
|
458
|
+
again = false;
|
|
459
|
+
await evaluate();
|
|
460
|
+
} while (again && !stopped);
|
|
461
|
+
})().finally(() => {
|
|
462
|
+
inFlight = void 0;
|
|
463
|
+
if (again && !stopped) notify();
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
const timer = setInterval(notify, deps.pollMs ?? 1e3);
|
|
467
|
+
timer.unref();
|
|
468
|
+
return {
|
|
469
|
+
notify,
|
|
470
|
+
async stop() {
|
|
471
|
+
stopped = true;
|
|
472
|
+
again = false;
|
|
473
|
+
clearInterval(timer);
|
|
474
|
+
controller.abort();
|
|
475
|
+
await inFlight;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function chooseWakeDriver(drivers, ctx, activity) {
|
|
480
|
+
if (activity.state !== "idle") return void 0;
|
|
481
|
+
const states = await readWakeState(ctx.files.dir);
|
|
482
|
+
const override = ctx.env.KHALA_WAKE_TEST_IDLE_MS;
|
|
483
|
+
const idleOverride = override !== void 0 && /^\d+$/.test(override) ? Number(override) : void 0;
|
|
484
|
+
for (const driver of [...drivers].sort((a, b) => a.rung - b.rung)) {
|
|
485
|
+
if (states[driver.id]?.disabled) continue;
|
|
486
|
+
if (!await driverAllowed(stateRoot(ctx.env), ctx.harness, driver.id, driver.optIn)) continue;
|
|
487
|
+
if (!await driver.available(ctx)) continue;
|
|
488
|
+
if (ctx.now - Date.parse(activity.updatedAt) < (idleOverride ?? driver.minIdleMs)) return void 0;
|
|
489
|
+
return driver;
|
|
490
|
+
}
|
|
491
|
+
return void 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/wake/codex.ts
|
|
495
|
+
function createCodexWakeDriver(deps = {}) {
|
|
496
|
+
return {
|
|
497
|
+
id: "queue",
|
|
498
|
+
rung: 1,
|
|
499
|
+
optIn: false,
|
|
500
|
+
minIdleMs: 0,
|
|
501
|
+
deadlineMs: 3e4,
|
|
502
|
+
// U13 introduces the nonce-bearing argv. Preserve U2 queue goldens until then.
|
|
503
|
+
verification: "none",
|
|
504
|
+
available: () => true,
|
|
505
|
+
async wake(ctx) {
|
|
506
|
+
const port = deps.port ?? createCodexQueueProcessPort({ command: "codex", env: ctx.env });
|
|
507
|
+
const outcome = await port.run(codexIdleWakeArgv(ctx.sessionId), ctx.signal);
|
|
508
|
+
if (outcome.status !== "queued") {
|
|
509
|
+
const line = JSON.stringify({ ok: false, warning: "codex_queue_failed", status: outcome.status }) + "\n";
|
|
510
|
+
try {
|
|
511
|
+
(deps.stderr ?? ((value) => {
|
|
512
|
+
process.stderr.write(value);
|
|
513
|
+
}))(line);
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
var codexWakeDriver = createCodexWakeDriver();
|
|
521
|
+
|
|
522
|
+
// src/harness/codex.ts
|
|
523
|
+
var install = async (flags, deps) => (await import("./chunk-S3FX7VCY.mjs")).runCodexInstall(flags, deps);
|
|
524
|
+
var codex = {
|
|
525
|
+
id: "codex",
|
|
526
|
+
// A string metadata id wins even when empty or invalid; validation happens after selection.
|
|
527
|
+
sessionSources: [
|
|
528
|
+
{ kind: "meta", resolve: (meta) => typeof meta?.threadId === "string" ? meta.threadId : void 0, rejoinable: () => true },
|
|
529
|
+
{ kind: "env", resolve: (_meta, env) => env.CODEX_THREAD_ID, rejoinable: () => true }
|
|
530
|
+
],
|
|
531
|
+
codec: claudeStyleCodec,
|
|
532
|
+
restoreAtStartup: true,
|
|
533
|
+
install,
|
|
534
|
+
uninstall: (flags, deps) => install([...flags, "--uninstall"], deps),
|
|
535
|
+
wakeLadder: [codexWakeDriver],
|
|
536
|
+
wakeWarningName: "codex"
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/cursor.ts
|
|
540
|
+
import { createHash } from "node:crypto";
|
|
541
|
+
var CURSOR_WORKSPACE_ENV = "KHALA_CURSOR_WORKSPACE";
|
|
542
|
+
var CURSOR_DEFAULT_SESSION = "cursor-default";
|
|
543
|
+
function normalizeWorkspace(root) {
|
|
544
|
+
let value = root.trim();
|
|
545
|
+
if (!value || value.includes("${")) return null;
|
|
546
|
+
if (/^file:\/\//iu.test(value)) {
|
|
547
|
+
try {
|
|
548
|
+
value = decodeURIComponent(new URL(value).pathname);
|
|
549
|
+
} catch {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
value = value.replaceAll("\\", "/");
|
|
554
|
+
if (/^\/[A-Za-z]:(?:\/|$)/u.test(value)) value = value.slice(1);
|
|
555
|
+
if (/^[A-Za-z]:(?:\/|$)/u.test(value)) value = value.toLowerCase();
|
|
556
|
+
value = value.replace(/\/+$/u, "");
|
|
557
|
+
if (/^[a-z]:$/u.test(value)) value += "/";
|
|
558
|
+
return value || null;
|
|
559
|
+
}
|
|
560
|
+
function cursorSessionId(root) {
|
|
561
|
+
const normalized = typeof root === "string" ? normalizeWorkspace(root) : null;
|
|
562
|
+
if (normalized === null) return CURSOR_DEFAULT_SESSION;
|
|
563
|
+
return `ws-${createHash("sha256").update(normalized).digest("hex").slice(0, 32)}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/harness/codecs/cursor.ts
|
|
567
|
+
var events2 = { beforeSubmitPrompt: "prompt", postToolUse: "tool", stop: "stop" };
|
|
568
|
+
var cursorCodec = {
|
|
569
|
+
promptDeliversWithoutWake: false,
|
|
570
|
+
promptAcceptsContext: false,
|
|
571
|
+
suppressOutputErrors: true,
|
|
572
|
+
fallbackSession: CURSOR_DEFAULT_SESSION,
|
|
573
|
+
parse(stdin) {
|
|
574
|
+
try {
|
|
575
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
576
|
+
if (!input || typeof input.hook_event_name !== "string" || !Object.hasOwn(events2, input.hook_event_name)) return null;
|
|
577
|
+
const workspace = Array.isArray(input.workspace_roots) && typeof input.workspace_roots[0] === "string" ? input.workspace_roots[0] : void 0;
|
|
578
|
+
return {
|
|
579
|
+
sessionId: cursorSessionId(workspace),
|
|
580
|
+
event: events2[input.hook_event_name],
|
|
581
|
+
continuation: typeof input.loop_count === "number" && input.loop_count > 0,
|
|
582
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {},
|
|
583
|
+
...workspace !== void 0 ? { workspace } : {}
|
|
584
|
+
};
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
noop: (kind) => JSON.stringify(kind === "prompt" ? { continue: true } : {}) + "\n",
|
|
590
|
+
render(kind, frame) {
|
|
591
|
+
if (kind === "prompt") return cursorCodec.noop(kind);
|
|
592
|
+
return JSON.stringify(kind === "stop" ? { followup_message: frame } : { additional_context: frame }) + "\n";
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/harness/cursor.ts
|
|
597
|
+
var install2 = async (flags, deps) => (await import("./chunk-S3FX7VCY.mjs")).runCursorInstall(flags, deps);
|
|
598
|
+
var cursor = {
|
|
599
|
+
id: "cursor",
|
|
600
|
+
sessionSources: [{
|
|
601
|
+
kind: "workspace",
|
|
602
|
+
resolve: (_meta, env) => cursorSessionId(env[CURSOR_WORKSPACE_ENV]),
|
|
603
|
+
rejoinable: (sessionId) => sessionId !== CURSOR_DEFAULT_SESSION
|
|
604
|
+
}],
|
|
605
|
+
codec: cursorCodec,
|
|
606
|
+
restoreAtStartup: false,
|
|
607
|
+
install: install2,
|
|
608
|
+
uninstall: (flags, deps) => install2([...flags, "--uninstall"], deps)
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// src/harness/index.ts
|
|
612
|
+
function createAdapterRegistry(adapters) {
|
|
613
|
+
const byId = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const adapter of adapters) {
|
|
615
|
+
if (byId.has(adapter.id)) throw new Error(`duplicate_adapter: ${adapter.id}`);
|
|
616
|
+
if (!HARNESS_REGISTRY.some((row) => row.id === adapter.id)) throw new Error(`unregistered_adapter: ${adapter.id}`);
|
|
617
|
+
byId.set(adapter.id, adapter);
|
|
618
|
+
}
|
|
619
|
+
return Object.freeze({ resolve: (id) => byId.get(id) });
|
|
620
|
+
}
|
|
621
|
+
var ADAPTERS = Object.freeze([claude, codex, cursor]);
|
|
622
|
+
var registry = createAdapterRegistry(ADAPTERS);
|
|
623
|
+
var adapterFor = registry.resolve;
|
|
624
|
+
|
|
625
|
+
export {
|
|
626
|
+
readListeningMode,
|
|
627
|
+
applyListeningMode,
|
|
628
|
+
readActivity,
|
|
629
|
+
writeActivity,
|
|
630
|
+
settleAttempts,
|
|
631
|
+
createWakeLadder,
|
|
632
|
+
CURSOR_WORKSPACE_ENV,
|
|
633
|
+
adapterFor
|
|
634
|
+
};
|