larkin 0.5.4 → 0.5.5

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +1 -100
  4. package/dist/agent/context-prompt.cjs +16 -17
  5. package/dist/agent/context-prompt.mjs +15 -16
  6. package/dist/runtime/pi-state-dir.cjs +105 -0
  7. package/dist/runtime/pi-state-dir.mjs +94 -0
  8. package/dist/runtime/pi-tmux-extension.mjs +137 -0
  9. package/dist/runtime/pi-tmux-injection.mjs +49 -0
  10. package/dist/runtime/pi-tmux.bundle.js +600 -0
  11. package/dist/runtime/pi-tmux.mjs +408 -0
  12. package/dist/runtime/runtime-adapters.mjs +26 -113
  13. package/dist/runtime/runtime-host.cjs +3 -413
  14. package/dist/runtime/runtime-host.mjs +2 -412
  15. package/package.json +2 -8
  16. package/dist/runtime/pi-bash-timeout-extension.mjs +0 -60
  17. package/dist/runtime/pi-bash-timeout-injection.mjs +0 -54
  18. package/dist/runtime/pi-bash-timeout.bundle.js +0 -43
  19. package/dist/runtime/pi-extension-api.mjs +0 -1
  20. package/dist/runtime/pi-subagent-injection.mjs +0 -290
  21. package/dist/runtime/pi-subagent-ledger.cjs +0 -632
  22. package/dist/runtime/pi-subagent-ledger.mjs +0 -594
  23. package/dist/runtime/pi-subagent-record-watchdog-injection.mjs +0 -43
  24. package/dist/runtime/pi-subagent-record-watchdog.bundle.js +0 -183
  25. package/dist/runtime/pi-subagent-record-watchdog.mjs +0 -46
  26. package/dist/runtime/pi-subagents-notification.mjs +0 -175
  27. package/dist/runtime/pi-subagents.bundle.js +0 -9110
  28. package/dist/runtime/pi-supervised-command-extension.mjs +0 -72
  29. package/dist/runtime/pi-supervised-command.bundle.js +0 -4735
  30. package/dist/runtime/pi-supervised-command.mjs +0 -420
  31. package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +0 -945
@@ -0,0 +1,408 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { ensureOwnedTree, mkdirPrivateExclusive, writePrivateExclusive } from "./pi-state-dir.mjs";
6
+ export const DEFAULT_BASH_WAIT_SECONDS = 30;
7
+ export const OUTPUT_TAIL_BYTES = 32 * 1024;
8
+ export const LARKIN_TMUX_COMPLETION_TYPE = "larkin-tmux-completion";
9
+ export class ForeignTmuxTaskError extends Error {
10
+ taskId;
11
+ constructor(taskId) {
12
+ super(`tmux task ${taskId} is not owned by this Larkin instance`);
13
+ this.name = "ForeignTmuxTaskError";
14
+ this.taskId = taskId;
15
+ }
16
+ }
17
+ export function tmuxAvailable(env = process.env, platform = process.platform) {
18
+ if (platform === "win32")
19
+ return false;
20
+ const result = spawnSync("tmux", ["-V"], { env: tmuxClientEnv(env), encoding: "utf8", timeout: 5_000 });
21
+ const version = /tmux\s+(\d+)\.(\d+)/i.exec(`${result.stdout || ""} ${result.stderr || ""}`);
22
+ return result.status === 0 && version !== null
23
+ && (Number(version[1]) > 3 || (Number(version[1]) === 3 && Number(version[2]) >= 2));
24
+ }
25
+ function posixQuote(value) {
26
+ return `'${value.replace(/'/g, `'\\''`)}'`;
27
+ }
28
+ function sanitizeName(value, fallback) {
29
+ const cleaned = value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);
30
+ return cleaned || fallback;
31
+ }
32
+ function taskRoot(stateDir, agentId, instanceId) {
33
+ return path.join(stateDir, "pi-tmux", sanitizeName(agentId, "agent"), sanitizeName(instanceId, "inst"));
34
+ }
35
+ function taskDir(stateDir, agentId, instanceId, taskId) {
36
+ return path.join(taskRoot(stateDir, agentId, instanceId), taskId);
37
+ }
38
+ function sessionName(agentId, instanceId, taskId) {
39
+ return `lkn-${sanitizeName(agentId, "agent")}-${sanitizeName(instanceId, "inst")}-${taskId}`;
40
+ }
41
+ function readText(file) {
42
+ try {
43
+ return fs.readFileSync(file, "utf8");
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function writePrivate(file, body) {
50
+ fs.writeFileSync(file, body, { mode: 0o600 });
51
+ fs.chmodSync(file, 0o600);
52
+ }
53
+ function isRegularFile(file) {
54
+ try {
55
+ const stat = fs.lstatSync(file);
56
+ return stat.isFile() && !stat.isSymbolicLink();
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function isInside(root, candidate) {
63
+ const relative = path.relative(root, candidate);
64
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
65
+ }
66
+ function tailBytes(file, maxBytes) {
67
+ try {
68
+ const fd = fs.openSync(file, "r");
69
+ try {
70
+ const size = fs.fstatSync(fd).size;
71
+ const start = size > maxBytes ? size - maxBytes : 0;
72
+ const buf = Buffer.alloc(size - start);
73
+ fs.readSync(fd, buf, 0, buf.length, start);
74
+ return buf.toString("utf8");
75
+ }
76
+ finally {
77
+ fs.closeSync(fd);
78
+ }
79
+ }
80
+ catch {
81
+ return "";
82
+ }
83
+ }
84
+ function writeEnvScript(file, env) {
85
+ const lines = ["#!/bin/bash", "set -eu"];
86
+ for (const [key, value] of Object.entries(env)) {
87
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
88
+ continue;
89
+ if (value === undefined)
90
+ continue;
91
+ // 不写入继承的 tmux 会话坐标;pane 会带上本次 new-session 的值。
92
+ if (key === "PWD" || key === "TMUX" || key === "TMUX_PANE")
93
+ continue;
94
+ lines.push(`export ${key}=${posixQuote(value)}`);
95
+ }
96
+ writePrivateExclusive(file, `${lines.join("\n")}\n`);
97
+ }
98
+ function tmuxClientEnv(env) {
99
+ const next = { ...env };
100
+ delete next.TMUX;
101
+ delete next.TMUX_PANE;
102
+ delete next.BASH_ENV;
103
+ return next;
104
+ }
105
+ function tmuxHasSession(name, env) {
106
+ return spawnSync("tmux", ["has-session", "-t", `=${name}`], { env: tmuxClientEnv(env), encoding: "utf8", timeout: 5_000 }).status === 0;
107
+ }
108
+ function killSession(name, env) {
109
+ spawnSync("tmux", ["kill-session", "-t", `=${name}`], { env: tmuxClientEnv(env), encoding: "utf8", timeout: 5_000 });
110
+ }
111
+ function parseExitCode(raw) {
112
+ if (raw === null)
113
+ return null;
114
+ const value = Number.parseInt(raw.trim(), 10);
115
+ return Number.isInteger(value) ? value : null;
116
+ }
117
+ function processTable() {
118
+ const listed = spawnSync("ps", ["-ax", "-o", "pid=,ppid=,pgid=,state="], { encoding: "utf8", timeout: 5_000 });
119
+ if (listed.status !== 0)
120
+ return [];
121
+ return listed.stdout.split("\n").flatMap((line) => {
122
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/);
123
+ if (!match)
124
+ return [];
125
+ return [{
126
+ pid: Number.parseInt(match[1], 10),
127
+ ppid: Number.parseInt(match[2], 10),
128
+ pgid: Number.parseInt(match[3], 10),
129
+ state: match[4],
130
+ }];
131
+ });
132
+ }
133
+ function liveOwnedPanePid(session, env) {
134
+ if (!tmuxHasSession(session, env))
135
+ return null;
136
+ const listed = spawnSync("tmux", ["list-panes", "-t", `=${session}`, "-F", "#{pane_pid}"], {
137
+ env: tmuxClientEnv(env), encoding: "utf8", timeout: 5_000,
138
+ });
139
+ if (listed.status !== 0)
140
+ return null;
141
+ const pid = Number.parseInt(listed.stdout.trim().split(/\s+/)[0] ?? "", 10);
142
+ if (!Number.isInteger(pid) || pid <= 1)
143
+ return null;
144
+ try {
145
+ process.kill(pid, 0);
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ return pid;
151
+ }
152
+ function ownedPids(panePid, rows) {
153
+ const pane = rows.find((row) => row.pid === panePid);
154
+ const owned = new Set([panePid]);
155
+ if (pane) {
156
+ for (const row of rows)
157
+ if (row.pgid === pane.pgid)
158
+ owned.add(row.pid);
159
+ }
160
+ let grew = true;
161
+ while (grew) {
162
+ grew = false;
163
+ for (const row of rows) {
164
+ if (owned.has(row.ppid) && !owned.has(row.pid) && !row.state.startsWith("Z")) {
165
+ owned.add(row.pid);
166
+ grew = true;
167
+ }
168
+ }
169
+ }
170
+ return [...owned].filter((pid) => pid > 1 && pid !== process.pid);
171
+ }
172
+ function pidAlive(pid) {
173
+ const listed = spawnSync("ps", ["-p", String(pid), "-o", "stat="], { encoding: "utf8", timeout: 5_000 });
174
+ const state = listed.stdout.trim();
175
+ return listed.status === 0 && Boolean(state) && !state.startsWith("Z");
176
+ }
177
+ function signalGroup(pgid, signal) {
178
+ try {
179
+ process.kill(-pgid, signal);
180
+ }
181
+ catch { /* 组已不存在或不是 leader */ }
182
+ }
183
+ /** 只杀当前 pane 进程组及其子孙;不读落盘 pid。SIGKILL 覆盖 trap 忽略 TERM/HUP。 */
184
+ function terminateOwnedPane(session, env) {
185
+ const panePid = liveOwnedPanePid(session, env);
186
+ const rows = processTable();
187
+ const pane = rows.find((row) => row.pid === panePid);
188
+ const pids = panePid ? ownedPids(panePid, rows) : [];
189
+ if (pane)
190
+ signalGroup(pane.pgid, "SIGTERM");
191
+ killSession(session, env);
192
+ if (pane)
193
+ signalGroup(pane.pgid, "SIGKILL");
194
+ for (const pid of pids) {
195
+ try {
196
+ process.kill(pid, "SIGKILL");
197
+ }
198
+ catch { /* already gone */ }
199
+ }
200
+ if (tmuxHasSession(session, env))
201
+ killSession(session, env);
202
+ if (tmuxHasSession(session, env))
203
+ throw new Error(`tmux session ${session} still present after kill`);
204
+ const deadline = Date.now() + 250;
205
+ while (Date.now() < deadline && pids.some(pidAlive)) {
206
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
207
+ }
208
+ if (pids.some(pidAlive))
209
+ throw new Error(`tmux pane descendants still present after kill`);
210
+ }
211
+ function snapshotFromDir(dir, taskId, env, session) {
212
+ // 先探活再读终态:exit_code 在 shell 退出前落盘。先读再 has-session 会把探测窗口里写完的快命令判成 failed+null。
213
+ const live = tmuxHasSession(session, env);
214
+ const readExit = () => parseExitCode(readText(path.join(dir, "exit_code")));
215
+ const cancelled = readText(path.join(dir, "cancelled")) !== null;
216
+ let exitCode = readExit();
217
+ if (!live && exitCode === null)
218
+ exitCode = readExit();
219
+ const startedAt = readText(path.join(dir, "started_at"))?.trim() || null;
220
+ const endedAt = readText(path.join(dir, "ended_at"))?.trim() || null;
221
+ const output = tailBytes(path.join(dir, "output"), OUTPUT_TAIL_BYTES);
222
+ let status;
223
+ if (exitCode === null) {
224
+ if (live)
225
+ status = cancelled ? "cancelled" : "running";
226
+ else
227
+ status = cancelled ? "cancelled" : "failed";
228
+ }
229
+ else if (cancelled)
230
+ status = "cancelled";
231
+ else
232
+ status = exitCode === 0 ? "completed" : "failed";
233
+ return { taskId, status, exitCode, output, startedAt, endedAt };
234
+ }
235
+ export function formatTmuxTaskText(snapshot) {
236
+ const exit = snapshot.exitCode === null ? "null" : String(snapshot.exitCode);
237
+ const body = snapshot.output ? `\n${snapshot.output}` : "";
238
+ return `taskId=${snapshot.taskId} status=${snapshot.status} exitCode=${exit}${body}`;
239
+ }
240
+ export function createLarkinTmux(input) {
241
+ const env = input.env ?? process.env;
242
+ const agentId = input.agentId;
243
+ const instanceId = input.instanceId || crypto.randomUUID();
244
+ const stateDir = path.resolve(input.stateDir);
245
+ const ownedParts = ["pi-tmux", sanitizeName(agentId, "agent"), sanitizeName(instanceId, "inst")];
246
+ const root = taskRoot(stateDir, agentId, instanceId);
247
+ const expectedSession = (taskId) => sessionName(agentId, instanceId, taskId);
248
+ const assertPrivateScripts = (dir) => {
249
+ for (const name of ["env.sh", "command.sh", "run.sh", "meta.json"]) {
250
+ const file = path.join(dir, name);
251
+ if (!fs.existsSync(file)) {
252
+ if (name === "meta.json")
253
+ throw new ForeignTmuxTaskError(path.basename(dir));
254
+ continue;
255
+ }
256
+ if (!isRegularFile(file))
257
+ throw new ForeignTmuxTaskError(path.basename(dir));
258
+ }
259
+ };
260
+ const requireOwned = (taskId) => {
261
+ if (!/^[A-Za-z0-9_-]+$/.test(taskId))
262
+ throw new ForeignTmuxTaskError(taskId);
263
+ let instanceRoot;
264
+ try {
265
+ instanceRoot = ensureOwnedTree(stateDir, ownedParts, false);
266
+ }
267
+ catch {
268
+ throw new ForeignTmuxTaskError(taskId);
269
+ }
270
+ const dir = taskDir(stateDir, agentId, instanceId, taskId);
271
+ try {
272
+ const stat = fs.lstatSync(dir);
273
+ if (stat.isSymbolicLink() || !stat.isDirectory() || !isInside(instanceRoot, dir))
274
+ throw new ForeignTmuxTaskError(taskId);
275
+ }
276
+ catch (error) {
277
+ if (error instanceof ForeignTmuxTaskError)
278
+ throw error;
279
+ throw new ForeignTmuxTaskError(taskId);
280
+ }
281
+ const metaFile = path.join(dir, "meta.json");
282
+ if (!isRegularFile(metaFile))
283
+ throw new ForeignTmuxTaskError(taskId);
284
+ let meta;
285
+ try {
286
+ meta = JSON.parse(fs.readFileSync(metaFile, "utf8"));
287
+ }
288
+ catch {
289
+ throw new ForeignTmuxTaskError(taskId);
290
+ }
291
+ if (meta.taskId !== taskId
292
+ || meta.agentId !== agentId
293
+ || meta.instanceId !== instanceId
294
+ || meta.session !== expectedSession(taskId)
295
+ || typeof meta.cwd !== "string")
296
+ throw new ForeignTmuxTaskError(taskId);
297
+ try {
298
+ assertPrivateScripts(dir);
299
+ }
300
+ catch (error) {
301
+ if (error instanceof ForeignTmuxTaskError)
302
+ throw error;
303
+ throw new ForeignTmuxTaskError(taskId);
304
+ }
305
+ return dir;
306
+ };
307
+ const start = (command, cwd) => {
308
+ const taskId = crypto.randomBytes(8).toString("hex");
309
+ const parent = ensureOwnedTree(stateDir, ownedParts);
310
+ const dir = path.join(parent, taskId);
311
+ mkdirPrivateExclusive(dir);
312
+ if (!tmuxAvailable(env))
313
+ throw new Error("tmux is not available");
314
+ const session = expectedSession(taskId);
315
+ writePrivateExclusive(path.join(dir, "meta.json"), `${JSON.stringify({
316
+ taskId, agentId, instanceId, session, cwd,
317
+ })}\n`);
318
+ writeEnvScript(path.join(dir, "env.sh"), env);
319
+ writePrivateExclusive(path.join(dir, "command.sh"), command.endsWith("\n") ? command : `${command}\n`);
320
+ const envFile = path.join(dir, "env.sh");
321
+ const commandFile = path.join(dir, "command.sh");
322
+ const outputFile = path.join(dir, "output");
323
+ const exitFile = path.join(dir, "exit_code");
324
+ const startedFile = path.join(dir, "started_at");
325
+ const endedFile = path.join(dir, "ended_at");
326
+ // 包装器用 /bin/bash;用户命令另启 bash -o pipefail,保留数组 / [[ / pipefail。
327
+ writePrivateExclusive(path.join(dir, "run.sh"), [
328
+ "#!/bin/bash",
329
+ `PANE_TMUX=\${TMUX-}`,
330
+ `PANE_TMUX_PANE=\${TMUX_PANE-}`,
331
+ `. ${posixQuote(envFile)}`,
332
+ "set +eu",
333
+ "unset TMUX TMUX_PANE",
334
+ `if [ -n "$PANE_TMUX" ]; then export TMUX="$PANE_TMUX"; fi`,
335
+ `if [ -n "$PANE_TMUX_PANE" ]; then export TMUX_PANE="$PANE_TMUX_PANE"; fi`,
336
+ `cd ${posixQuote(cwd)} || { date -u +%Y-%m-%dT%H:%M:%SZ > ${posixQuote(endedFile)}; printf '%s\\n' 127 > ${posixQuote(exitFile)}; exit 127; }`,
337
+ `date -u +%Y-%m-%dT%H:%M:%SZ > ${posixQuote(startedFile)}`,
338
+ `/bin/bash -o pipefail ${posixQuote(commandFile)} > ${posixQuote(outputFile)} 2>&1`,
339
+ `status=$?`,
340
+ `date -u +%Y-%m-%dT%H:%M:%SZ > ${posixQuote(endedFile)}`,
341
+ `printf '%s\\n' "$status" > ${posixQuote(exitFile)}`,
342
+ "",
343
+ ].join("\n"));
344
+ const created = spawnSync("tmux", ["new-session", "-d", "-s", session, "-n", "bash", "-e", "BASH_ENV=", "/usr/bin/env", "-u", "BASH_ENV", "/bin/bash", path.join(dir, "run.sh")], {
345
+ env: tmuxClientEnv(env),
346
+ encoding: "utf8",
347
+ timeout: 5_000,
348
+ });
349
+ if (created.status !== 0) {
350
+ throw new Error(`tmux new-session failed: ${(created.stderr || created.stdout || "").trim() || created.status}`);
351
+ }
352
+ return snapshotFromDir(dir, taskId, env, session);
353
+ };
354
+ const peek = (taskId) => {
355
+ const dir = requireOwned(taskId);
356
+ return snapshotFromDir(dir, taskId, env, expectedSession(taskId));
357
+ };
358
+ const list = () => {
359
+ let instanceRoot;
360
+ try {
361
+ instanceRoot = ensureOwnedTree(stateDir, ownedParts, false);
362
+ }
363
+ catch {
364
+ return [];
365
+ }
366
+ return fs.readdirSync(instanceRoot).sort().flatMap((taskId) => {
367
+ try {
368
+ return [peek(taskId)];
369
+ }
370
+ catch {
371
+ return [];
372
+ }
373
+ });
374
+ };
375
+ const wait = async (taskId, timeoutSeconds, signal) => {
376
+ const deadline = Date.now() + Math.max(0, timeoutSeconds) * 1000;
377
+ while (true) {
378
+ if (signal?.aborted) {
379
+ kill(taskId);
380
+ return peek(taskId);
381
+ }
382
+ const current = peek(taskId);
383
+ // 会话已消失但 exit 尚未落盘时继续等到 WAIT 窗口结束,避免快命令被误判 failed。
384
+ const flushing = current.status === "failed" && current.exitCode === null;
385
+ if (current.status !== "running" && !flushing)
386
+ return current;
387
+ if (Date.now() >= deadline)
388
+ return current;
389
+ await new Promise((resolve) => setTimeout(resolve, 50));
390
+ }
391
+ };
392
+ const kill = (taskId) => {
393
+ const dir = requireOwned(taskId);
394
+ const session = expectedSession(taskId);
395
+ if (parseExitCode(readText(path.join(dir, "exit_code"))) !== null)
396
+ return snapshotFromDir(dir, taskId, env, session);
397
+ const live = tmuxHasSession(session, env) || liveOwnedPanePid(session, env) !== null;
398
+ if (live) {
399
+ writePrivate(path.join(dir, "cancelled"), "1\n");
400
+ terminateOwnedPane(session, env);
401
+ if (!readText(path.join(dir, "ended_at"))) {
402
+ writePrivate(path.join(dir, "ended_at"), `${new Date().toISOString().slice(0, 19)}Z\n`);
403
+ }
404
+ }
405
+ return snapshotFromDir(dir, taskId, env, session);
406
+ };
407
+ return { start, peek, list, wait, kill, root, instanceId };
408
+ }
@@ -6,11 +6,8 @@ import { createInterface } from "node:readline";
6
6
  import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
7
7
  import { PiRpcClient } from "./pi-rpc-client.mjs";
8
8
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
9
- import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
10
- import { extractCanonicalPiSubagentNotification, ledgerStatusFromPiNotificationStatus, } from "./pi-subagents-notification.mjs";
11
- import { resolvePiSubagentRecordWatchdogExtensionArg } from "./pi-subagent-record-watchdog-injection.mjs";
12
- import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-subagent-ledger.mjs";
13
- import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
9
+ import { effectivePiStateDir, writePrivateAtomic } from "./pi-state-dir.mjs";
10
+ import { resolvePiTmuxExtensionArg } from "./pi-tmux-injection.mjs";
14
11
  import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
15
12
  import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, safeProviderDiagnostic, } from "./provider-error-classifier.mjs";
16
13
  import { assertEffectivePiCompactionSettings, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, projectPiSettingsFile, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
@@ -482,9 +479,6 @@ class PiSession extends EventSession {
482
479
  observedSubmitEpochs = new Set();
483
480
  observedAcceptedEpochs = new Set();
484
481
  observedCompletedEpochs = new Set();
485
- observedBackgroundCompletionKeys = new Set();
486
- pendingUnownedCompletionKeys = new Set();
487
- pendingUnownedCompletionStatuses = new Map();
488
482
  observedAgentEndEpochs = new Set();
489
483
  firstOutputObserved = false;
490
484
  toolCallOpen = false;
@@ -533,9 +527,9 @@ class PiSession extends EventSession {
533
527
  }
534
528
  async close(_reason) { this.unsubscribe?.(); await this.sdk.dispose?.(); }
535
529
  async enqueue(input, operation) {
536
- if (this.ownedInputIds.size === 0)
530
+ if (this.ownedInputIds.size === 0 && this.activeEpoch === null)
537
531
  this.requestEpoch += 1;
538
- const epoch = this.requestEpoch;
532
+ const epoch = this.activeEpoch ?? this.requestEpoch;
539
533
  this.ownedInputIds.add(input.inputId);
540
534
  this.inputEpochs.set(input.inputId, epoch);
541
535
  this.awaitingAcknowledgement.add(input.inputId);
@@ -598,9 +592,6 @@ class PiSession extends EventSession {
598
592
  this.observedSubmitEpochs.clear();
599
593
  this.observedAcceptedEpochs.clear();
600
594
  this.observedCompletedEpochs.clear();
601
- this.observedBackgroundCompletionKeys.clear();
602
- this.pendingUnownedCompletionKeys.clear();
603
- this.pendingUnownedCompletionStatuses.clear();
604
595
  this.observedAgentEndEpochs.clear();
605
596
  this.activeEpoch = null;
606
597
  this.settleArmedEpoch = null;
@@ -620,11 +611,19 @@ class PiSession extends EventSession {
620
611
  this.emitObservation("retry_progress");
621
612
  }
622
613
  else if (event?.type === "turn_start") {
623
- const epoch = this.oldestOwnedEpoch();
624
- if (epoch === null || this.activeEpoch !== null)
614
+ if (this.activeEpoch !== null)
625
615
  return;
626
- this.activeEpoch = epoch;
627
- this.settleArmedEpoch = null;
616
+ const owned = this.oldestOwnedEpoch();
617
+ if (owned === null) {
618
+ // Native Pi triggerTurn / extension follow-up: occupy busy without a host prompt.
619
+ this.requestEpoch += 1;
620
+ this.activeEpoch = this.requestEpoch;
621
+ this.settleArmedEpoch = this.activeEpoch;
622
+ }
623
+ else {
624
+ this.activeEpoch = owned;
625
+ this.settleArmedEpoch = null;
626
+ }
628
627
  this.firstOutputObserved = false;
629
628
  this.toolCallOpen = false;
630
629
  this.emitObservation("turn_start");
@@ -650,46 +649,13 @@ class PiSession extends EventSession {
650
649
  this.observedCompletedEpochs.add(this.activeEpoch);
651
650
  this.emitObservation("completed");
652
651
  }
653
- const completionNotification = extractCanonicalPiSubagentNotification(event.messages);
654
- if (completionNotification && !this.observedBackgroundCompletionKeys.has(completionNotification.key)) {
655
- const completionStatuses = Object.fromEntries(completionNotification.notifications.map((notification) => [
656
- notification.taskId,
657
- ledgerStatusFromPiNotificationStatus(notification.status),
658
- ]));
659
- const owningTurnFailed = event.willRetry === true
660
- || this.finalAssistantStopReason === "error"
661
- || this.finalAssistantStopReason === "aborted";
662
- if (this.activeEpoch === null || owningTurnFailed) {
663
- // Unowned agent_end still has an active Pi session, so prompting
664
- // before settle is rejected as "Agent is already processing".
665
- // A failed or retrying owned turn also did not process the
666
- // notification; do not emit handledInTurn so RuntimeHost can still
667
- // wake after input-error, retry, or restart.
668
- this.pendingUnownedCompletionKeys.add(completionNotification.key);
669
- this.pendingUnownedCompletionStatuses.set(completionNotification.key, completionStatuses);
670
- }
671
- else {
672
- // Already visible in the owned turn. Persist as acknowledged; do not
673
- // schedule another wake after the parent turn settles.
674
- this.observedBackgroundCompletionKeys.add(completionNotification.key);
675
- this.emitObservation("completed", {
676
- completionKey: completionNotification.key,
677
- completionStatuses,
678
- handledInTurn: true,
679
- });
680
- }
681
- }
682
652
  }
683
653
  else if (event?.type === "agent_settled") {
684
654
  const epoch = this.activeEpoch;
685
- if (epoch === null) {
686
- this.flushPendingUnownedCompletions();
655
+ if (epoch === null)
687
656
  return;
688
- }
689
- if (this.settleArmedEpoch !== epoch) {
690
- this.flushPendingUnownedCompletions();
657
+ if (this.settleArmedEpoch !== epoch)
691
658
  return;
692
- }
693
659
  this.activeEpoch = null;
694
660
  this.settleArmedEpoch = null;
695
661
  const error = this.finalAssistantError;
@@ -730,7 +696,6 @@ class PiSession extends EventSession {
730
696
  this.observedAcceptedEpochs.delete(epoch);
731
697
  this.observedCompletedEpochs.delete(epoch);
732
698
  this.observedAgentEndEpochs.delete(epoch);
733
- this.flushPendingUnownedCompletions();
734
699
  }
735
700
  else if (event?.type === "tool_execution_start") {
736
701
  if (!this.firstOutputObserved) {
@@ -748,13 +713,6 @@ class PiSession extends EventSession {
748
713
  this.toolCallOpen = false;
749
714
  this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_result" });
750
715
  }
751
- const dispatched = extractBackgroundPiSubagentDispatch(event);
752
- if (dispatched) {
753
- this.emitObservation("background_dispatched", {
754
- taskId: dispatched.taskId,
755
- ...(dispatched.outputFile ? { outputFile: dispatched.outputFile } : {}),
756
- });
757
- }
758
716
  }
759
717
  else if (event?.type === "message_update" && event.assistantMessageEvent?.delta) {
760
718
  if (!this.firstOutputObserved) {
@@ -765,20 +723,6 @@ class PiSession extends EventSession {
765
723
  this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
766
724
  }
767
725
  }
768
- flushPendingUnownedCompletions() {
769
- for (const completionKey of this.pendingUnownedCompletionKeys) {
770
- if (this.observedBackgroundCompletionKeys.has(completionKey))
771
- continue;
772
- this.observedBackgroundCompletionKeys.add(completionKey);
773
- const completionStatuses = this.pendingUnownedCompletionStatuses.get(completionKey);
774
- this.emitObservation("completed", {
775
- completionKey,
776
- ...(completionStatuses ? { completionStatuses } : {}),
777
- });
778
- }
779
- this.pendingUnownedCompletionKeys.clear();
780
- this.pendingUnownedCompletionStatuses.clear();
781
- }
782
726
  emitObservation(phase, fields = {}) {
783
727
  const inputId = this.oldestOwnedInput();
784
728
  const observation = { type: "runtime-observation", runtime: "pi",
@@ -788,8 +732,6 @@ class PiSession extends EventSession {
788
732
  Object.defineProperty(observation, "sessionId", { value: this.sessionId, enumerable: false });
789
733
  if (inputId)
790
734
  Object.defineProperty(observation, "inputId", { value: inputId, enumerable: false });
791
- if (fields.completionKey)
792
- Object.defineProperty(observation, "completionKey", { value: fields.completionKey, enumerable: false });
793
735
  this.emit(observation);
794
736
  }
795
737
  oldestOwnedInput() {
@@ -936,28 +878,6 @@ async function discoverEffectivePiContextWindow(input, command, commandPrefix, r
936
878
  await client.close();
937
879
  }
938
880
  }
939
- function writePrivateAtomic(file, content) {
940
- try {
941
- if (fs.lstatSync(file).isSymbolicLink())
942
- throw new Error("standing prompt must not be a symlink");
943
- }
944
- catch (error) {
945
- if (error.code !== "ENOENT")
946
- throw error;
947
- }
948
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
949
- try {
950
- fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
951
- fs.renameSync(temporary, file);
952
- fs.chmodSync(file, 0o600);
953
- }
954
- finally {
955
- try {
956
- fs.unlinkSync(temporary);
957
- }
958
- catch { /* renamed or absent */ }
959
- }
960
- }
961
881
  class PiRpcBackend {
962
882
  client;
963
883
  policy;
@@ -1012,27 +932,20 @@ class PiRpcBackend {
1012
932
  return () => { offEvent(); offFailure(); };
1013
933
  }
1014
934
  }
1015
- export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
1016
- const resolverInput = { distribution: "external", piCommand: input.piCommand, env: input.env };
1017
- const args = [];
1018
- // Watchdog must load before the subagent extension so session_shutdown still
1019
- // sees AgentManager.getRecord and can bridge consumed or terminal state.
1020
- const recordWatchdog = (resolvers.recordWatchdog ?? resolvePiSubagentRecordWatchdogExtensionArg)(resolverInput);
1021
- if (recordWatchdog)
1022
- args.push("-e", recordWatchdog);
1023
- const subagents = (resolvers.subagents ?? resolvePiSubagentExtensionArg)(resolverInput);
1024
- if (subagents)
1025
- args.push("-e", subagents);
1026
- const bashTimeout = (resolvers.bashTimeout ?? resolvePiBashTimeoutExtensionArg)(resolverInput);
1027
- if (bashTimeout)
1028
- args.push("-e", bashTimeout);
1029
- return args;
935
+ /** Inject only the Larkin-owned tmux extension. Skip native Windows so stock Pi bash remains. */
936
+ export function resolvePiProcessExtensionArgs(input) {
937
+ const bundle = resolvePiTmuxExtensionArg({ env: input.env, platform: input.platform });
938
+ return bundle ? ["-e", bundle] : [];
1030
939
  }
1031
940
  async function createPiRpcBackend(input, dependencies, spawn, productionSpawn = true) {
1032
941
  const stateRoot = effectivePiStateDir(input);
1033
942
  const mergedEnv = stripPiCodingAgentDir({ ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" });
1034
943
  if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
1035
944
  delete mergedEnv.LARKIN_PI_DISTRIBUTION;
945
+ mergedEnv.LARKIN_AGENT_ID = input.agentId;
946
+ mergedEnv.LARKIN_STATE_DIR = stateRoot;
947
+ if (!mergedEnv.LARKIN_TMUX_INSTANCE_ID)
948
+ mergedEnv.LARKIN_TMUX_INSTANCE_ID = crypto.randomUUID();
1036
949
  const projectSettings = projectPiSettingsFile(input.workspaceDir);
1037
950
  traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: projectSettings, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
1038
951
  const runtimeDir = path.join(stateRoot, "runtime");