@xl0/pi-lovely-agents 0.1.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +184 -0
  4. package/extensions/lovely-agents/agent.ts +1374 -0
  5. package/extensions/lovely-agents/bash.ts +599 -0
  6. package/extensions/lovely-agents/child-session.ts +296 -0
  7. package/extensions/lovely-agents/config.ts +221 -0
  8. package/extensions/lovely-agents/coordinator.ts +506 -0
  9. package/extensions/lovely-agents/definitions.ts +380 -0
  10. package/extensions/lovely-agents/index.ts +400 -0
  11. package/extensions/lovely-agents/lifecycle.ts +251 -0
  12. package/extensions/lovely-agents/management.ts +638 -0
  13. package/extensions/lovely-agents/notifications.ts +220 -0
  14. package/extensions/lovely-agents/provider-limits.ts +13 -0
  15. package/extensions/lovely-agents/rendering.ts +90 -0
  16. package/extensions/lovely-agents/state.ts +1179 -0
  17. package/extensions/lovely-agents/task-panel.ts +192 -0
  18. package/extensions/lovely-agents/tools.ts +635 -0
  19. package/extensions/lovely-agents/updates.ts +45 -0
  20. package/node_modules/@xl0/pi-lovely-config/CHANGELOG.md +79 -0
  21. package/node_modules/@xl0/pi-lovely-config/LICENSE +21 -0
  22. package/node_modules/@xl0/pi-lovely-config/README.md +200 -0
  23. package/node_modules/@xl0/pi-lovely-config/package.json +59 -0
  24. package/node_modules/@xl0/pi-lovely-config/src/config.ts +399 -0
  25. package/node_modules/@xl0/pi-lovely-config/src/index.ts +3 -0
  26. package/node_modules/@xl0/pi-lovely-config/src/ui.ts +786 -0
  27. package/package.json +68 -0
  28. package/skills/agent/SKILL.md +21 -0
  29. package/skills/agent-creator/SKILL.md +35 -0
@@ -0,0 +1,1179 @@
1
+ import { randomBytes } from "node:crypto"
2
+ import { watch } from "node:fs"
3
+ import { chmod, link, lstat, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises"
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
5
+ import { type Static, Type } from "typebox"
6
+ import { Value } from "typebox/value"
7
+ import { getAgentCoordinator, getBashCoordinator } from "./coordinator.js"
8
+ import { publishTaskUpdate } from "./updates.js"
9
+
10
+ export const TASK_METADATA_VERSION = 3
11
+ export const TASK_REFERENCE_PATTERN = /^[ab]_[0-9a-f]{8}$/
12
+ export const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/
13
+ export const STORAGE_GITIGNORE = "*\n"
14
+ export const MAX_AGENT_INPUT_BYTES = 64 * 1024
15
+ export const MAX_AGENT_LABEL_BYTES = 80
16
+ export const MAX_QUEUED_FOLLOWUPS = 32
17
+ export const MAX_TASK_NOTIFICATIONS = 128
18
+ export const MAX_NOTIFICATION_CONTENT_BYTES = 8 * 1024
19
+ export const PARENT_LEASE_VERSION = 1
20
+ export const RETAINED_OUTPUT_MAX_LINES = 2_000
21
+ export const RETAINED_OUTPUT_MAX_BYTES = 50 * 1024
22
+ export const RETAINED_OUTPUT_MAX_WAIT_MS = 10 * 60 * 1_000
23
+
24
+ const DIRECTORY_MODE = 0o700
25
+ const FILE_MODE = 0o600
26
+ const MAX_TASK_REFERENCE_ATTEMPTS = 100
27
+ const MAX_LEASE_ACQUIRE_ATTEMPTS = 10
28
+ const DEFINITION_NAME_PATTERN = "^[a-z0-9][a-z0-9_-]{0,63}$"
29
+ const LEASE_STATE_SYMBOL = Symbol.for("@xl0/pi-lovely-agents/parent-leases/v1")
30
+ const TASK_QUEUE_STATE_SYMBOL = Symbol.for("@xl0/pi-lovely-agents/task-queues/v1")
31
+
32
+ const RunId = Type.String({ pattern: "^r_[0-9a-f]{16}$" })
33
+ const Timestamp = Type.Integer({ minimum: 0 })
34
+ const TaskState = Type.Union([
35
+ Type.Literal("idle"),
36
+ Type.Literal("queued"),
37
+ Type.Literal("running"),
38
+ Type.Literal("suspended"),
39
+ Type.Literal("interrupted")
40
+ ])
41
+ const RunOutcome = Type.Union([Type.Literal("succeeded"), Type.Literal("failed"), Type.Literal("stopped"), Type.Literal("interrupted")])
42
+ const ThinkingLevel = Type.Union([
43
+ Type.Literal("off"),
44
+ Type.Literal("minimal"),
45
+ Type.Literal("low"),
46
+ Type.Literal("medium"),
47
+ Type.Literal("high"),
48
+ Type.Literal("xhigh"),
49
+ Type.Literal("max")
50
+ ])
51
+ const ActiveRun = Type.Object(
52
+ {
53
+ id: RunId,
54
+ sequence: Type.Integer({ minimum: 1 }),
55
+ acceptanceOrder: Type.Optional(Type.Integer({ minimum: 1 })),
56
+ // Acceptance policy, not current config. Missing means foreground.
57
+ background: Type.Optional(Type.Boolean()),
58
+ kind: Type.Union([Type.Literal("initial"), Type.Literal("followup")]),
59
+ state: Type.Union([Type.Literal("queued"), Type.Literal("running"), Type.Literal("suspended")]),
60
+ input: Type.String(),
61
+ acceptedAt: Timestamp,
62
+ startedAt: Type.Optional(Timestamp),
63
+ detachedAt: Type.Optional(Timestamp)
64
+ },
65
+ { additionalProperties: false }
66
+ )
67
+ const QueuedFollowUp = Type.Object(
68
+ {
69
+ id: RunId,
70
+ sequence: Type.Integer({ minimum: 1 }),
71
+ acceptanceOrder: Type.Optional(Type.Integer({ minimum: 1 })),
72
+ background: Type.Optional(Type.Boolean()),
73
+ content: Type.String(),
74
+ acceptedAt: Timestamp
75
+ },
76
+ { additionalProperties: false }
77
+ )
78
+ const Notification = Type.Object(
79
+ {
80
+ id: Type.String({ minLength: 1 }),
81
+ type: Type.Union([Type.Literal("completion"), Type.Literal("suspension"), Type.Literal("interruption")]),
82
+ runId: RunId,
83
+ content: Type.String({ minLength: 1 }),
84
+ createdAt: Timestamp,
85
+ deliveredAt: Type.Optional(Timestamp)
86
+ },
87
+ { additionalProperties: false }
88
+ )
89
+ const ParentLeaseFileSchema = Type.Object(
90
+ {
91
+ version: Type.Literal(PARENT_LEASE_VERSION),
92
+ pid: Type.Integer({ minimum: 1 }),
93
+ token: Type.String({ pattern: "^[0-9a-f]{32}$" }),
94
+ createdAt: Timestamp
95
+ },
96
+ { additionalProperties: false }
97
+ )
98
+
99
+ /** Durable current snapshot for one agent session. */
100
+ export const AgentTaskMetadataSchema = Type.Object(
101
+ {
102
+ version: Type.Literal(TASK_METADATA_VERSION),
103
+ kind: Type.Literal("agent"),
104
+ taskRef: Type.String({ pattern: "^a_[0-9a-f]{8}$" }),
105
+ parentSessionId: Type.String({ pattern: SESSION_ID_PATTERN.source }),
106
+ childSessionId: Type.String({ pattern: SESSION_ID_PATTERN.source }),
107
+ definitionName: Type.String({ pattern: DEFINITION_NAME_PATTERN }),
108
+ label: Type.String({ minLength: 1 }),
109
+ // Bounded current/last run input for human task panels; survives settlement.
110
+ inputPreview: Type.Optional(Type.String({ maxLength: 512 })),
111
+ model: Type.Object(
112
+ {
113
+ provider: Type.String({ minLength: 1 }),
114
+ id: Type.String({ minLength: 1 })
115
+ },
116
+ { additionalProperties: false }
117
+ ),
118
+ thinking: ThinkingLevel,
119
+ depth: Type.Integer({ minimum: 1 }),
120
+ allowAgents: Type.Boolean(),
121
+ sessionConfig: Type.Object(
122
+ {
123
+ systemPrompt: Type.String({ minLength: 1 }),
124
+ tools: Type.Union([Type.Array(Type.String({ minLength: 1 })), Type.Null()]),
125
+ excludeAgentsMd: Type.Boolean(),
126
+ scopedModels: Type.Array(
127
+ Type.Object(
128
+ {
129
+ provider: Type.String({ minLength: 1 }),
130
+ id: Type.String({ minLength: 1 }),
131
+ thinkingLevel: Type.Optional(ThinkingLevel)
132
+ },
133
+ { additionalProperties: false }
134
+ )
135
+ )
136
+ },
137
+ { additionalProperties: false }
138
+ ),
139
+ state: TaskState,
140
+ latestOutcome: Type.Union([RunOutcome, Type.Null()]),
141
+ latestReply: Type.Union([
142
+ Type.Object({ text: Type.String(), streaming: Type.Boolean() }, { additionalProperties: false }),
143
+ Type.Null()
144
+ ]),
145
+ lastActivity: Type.Optional(
146
+ Type.Object(
147
+ {
148
+ at: Timestamp,
149
+ action: Type.String({ minLength: 1, maxLength: 200 })
150
+ },
151
+ { additionalProperties: false }
152
+ )
153
+ ),
154
+ // Pi's composed prompt at agent start, distinct from the immutable Definition recipe.
155
+ effectiveSystemPrompt: Type.Optional(Type.String()),
156
+ lastRunSequence: Type.Integer({ minimum: 0 }),
157
+ activeRun: Type.Union([ActiveRun, Type.Null()]),
158
+ queuedFollowUps: Type.Array(QueuedFollowUp, { maxItems: MAX_QUEUED_FOLLOWUPS }),
159
+ notifications: Type.Array(Notification, { maxItems: MAX_TASK_NOTIFICATIONS }),
160
+ discardedAt: Type.Union([Timestamp, Type.Null()]),
161
+ createdAt: Timestamp,
162
+ updatedAt: Timestamp
163
+ },
164
+ { additionalProperties: false }
165
+ )
166
+
167
+ /** One shell invocation, without a Pi session or a reusable agent recipe. */
168
+ export const BashTaskMetadataSchema = Type.Object(
169
+ {
170
+ ...Type.Pick(AgentTaskMetadataSchema, [
171
+ "version",
172
+ "parentSessionId",
173
+ "label",
174
+ "inputPreview",
175
+ "state",
176
+ "latestOutcome",
177
+ "latestReply",
178
+ "lastActivity",
179
+ "lastRunSequence",
180
+ "activeRun",
181
+ "queuedFollowUps",
182
+ "notifications",
183
+ "discardedAt",
184
+ "createdAt",
185
+ "updatedAt"
186
+ ]).properties,
187
+ kind: Type.Literal("bash"),
188
+ latestReply: Type.Union([
189
+ Type.Object(
190
+ { text: Type.String(), streaming: Type.Boolean(), truncated: Type.Optional(Type.Boolean()) },
191
+ { additionalProperties: false }
192
+ ),
193
+ Type.Null()
194
+ ]),
195
+ taskRef: Type.String({ pattern: "^b_[0-9a-f]{8}$" }),
196
+ command: Type.String({ minLength: 1 }),
197
+ cwd: Type.String({ minLength: 1 }),
198
+ exitCode: Type.Union([Type.Integer(), Type.Null()]),
199
+ signal: Type.Union([Type.String({ minLength: 1 }), Type.Null()])
200
+ },
201
+ { additionalProperties: false }
202
+ )
203
+ export const TaskMetadataSchema = Type.Union([AgentTaskMetadataSchema, BashTaskMetadataSchema])
204
+ export type AgentTaskMetadata = Static<typeof AgentTaskMetadataSchema>
205
+ export type BashTaskMetadata = Static<typeof BashTaskMetadataSchema>
206
+ export type TaskMetadata = AgentTaskMetadata | BashTaskMetadata
207
+ type ParentLeaseFile = Static<typeof ParentLeaseFileSchema>
208
+
209
+ /** Filesystem locations owned by one parent Pi session. */
210
+ export type ParentStoragePaths = {
211
+ workspace: string
212
+ root: string
213
+ parentSessionId: string
214
+ parentDirectory: string
215
+ lease: string
216
+ }
217
+
218
+ /** Filesystem locations retained for one task. */
219
+ export type TaskStoragePaths = ParentStoragePaths & {
220
+ taskRef: string
221
+ taskDirectory: string
222
+ metadata: string
223
+ session: string
224
+ history: string
225
+ output: string
226
+ }
227
+
228
+ /** Process-global ownership proof for one parent partition. */
229
+ export type ParentLease = Readonly<ParentLeaseFile & { paths: Readonly<ParentStoragePaths> }>
230
+
231
+ /** Stable model-visible paths for retained task artifacts. */
232
+ export type RetainedPaths = {
233
+ history: string
234
+ session?: string
235
+ output?: string
236
+ }
237
+
238
+ /** Chronological inputs, replies, compact tool summaries, and run outcomes. */
239
+ export type HistoryEntry =
240
+ | { type: "run-start"; sequence: number; kind: "initial" | "followup"; timestamp: number }
241
+ | { type: "input"; delivery: "initial" | "followup" | "steer" | "stdin"; timestamp: number; content: string }
242
+ | { type: "assistant"; content: string }
243
+ | { type: "output"; content: string }
244
+ | { type: "stdin"; content: string; timestamp: number; eof?: boolean }
245
+ | { type: "run-end"; sequence: number; outcome: Static<typeof RunOutcome>; timestamp: number; summary?: string }
246
+ | { type: "tool"; tool: string; arguments: string; result: string; isError: boolean }
247
+
248
+ export type RetainedOutputReadOptions = {
249
+ waitMs?: number
250
+ signal?: AbortSignal
251
+ }
252
+
253
+ /** Latest agent reply or Bash output tail; run status is independent of streaming. */
254
+ export type RetainedOutputRead = {
255
+ text: string
256
+ totalLines: number
257
+ truncated: boolean
258
+ timedOut: boolean
259
+ state: Static<typeof TaskState>
260
+ latestOutcome: Static<typeof RunOutcome> | null
261
+ streaming: boolean
262
+ queuedFollowUps: number
263
+ lastActivity: NonNullable<TaskMetadata["lastActivity"]> | null
264
+ queueReason: "capacity" | "provider-limit" | "starting" | null
265
+ /** Held process-wide execution permits, not the number of tasks in running state. */
266
+ capacity: { active: number; limit: number }
267
+ exitCode?: number | null
268
+ signal?: string | null
269
+ paths: RetainedPaths
270
+ }
271
+
272
+ export type MetadataDiagnostic = {
273
+ code: "unreadable" | "invalid-json" | "unsupported-version" | "invalid-metadata"
274
+ message: string
275
+ path: string
276
+ }
277
+
278
+ export type MetadataLoadResult =
279
+ | { status: "ok"; metadata: TaskMetadata }
280
+ | { status: "missing" }
281
+ | { status: "invalid"; diagnostic: MetadataDiagnostic }
282
+
283
+ type ParentLeaseLoadResult = { status: "ok"; lease: ParentLeaseFile } | { status: "missing" } | { status: "invalid"; message: string }
284
+
285
+ type ParentLeaseState = {
286
+ version: typeof PARENT_LEASE_VERSION
287
+ leases: Map<string, ParentLease>
288
+ queues: Map<string, Promise<void>>
289
+ }
290
+
291
+ type TaskQueueState = {
292
+ version: 1
293
+ metadata: Map<string, Promise<void>>
294
+ logs: Map<string, Promise<void>>
295
+ }
296
+
297
+ export class InvalidTaskMetadataError extends Error {
298
+ constructor(message: string) {
299
+ super(message)
300
+ this.name = "InvalidTaskMetadataError"
301
+ }
302
+ }
303
+
304
+ export class ParentLeaseError extends Error {
305
+ readonly code: string = "LOVELY_AGENTS_PARENT_LEASE"
306
+
307
+ constructor(message: string) {
308
+ super(message)
309
+ this.name = "ParentLeaseError"
310
+ }
311
+ }
312
+
313
+ export class ParentLeaseConflictError extends ParentLeaseError {
314
+ override readonly code = "LOVELY_AGENTS_PARENT_LEASE_CONFLICT"
315
+ readonly ownerPid: number
316
+
317
+ constructor(path: string, ownerPid: number) {
318
+ super(`Lovely Agents parent partition is owned by live process ${ownerPid}: ${path}`)
319
+ this.name = "ParentLeaseConflictError"
320
+ this.ownerPid = ownerPid
321
+ }
322
+ }
323
+
324
+ export function parentStoragePaths(cwd: string, parentSessionId: string): ParentStoragePaths {
325
+ assertSessionId(parentSessionId)
326
+ const workspace = resolve(cwd)
327
+ const root = join(workspace, ".pi", "lovely-agents")
328
+ const parentDirectory = join(root, parentSessionId)
329
+ return { workspace, root, parentSessionId, parentDirectory, lease: join(parentDirectory, ".lease") }
330
+ }
331
+
332
+ export function taskStoragePaths(parent: ParentStoragePaths, taskRef: string): TaskStoragePaths {
333
+ assertTaskReference(taskRef)
334
+ const taskDirectory = join(parent.parentDirectory, taskRef)
335
+ return {
336
+ ...parent,
337
+ taskRef,
338
+ taskDirectory,
339
+ metadata: join(taskDirectory, "metadata.json"),
340
+ session: join(taskDirectory, "session.jsonl"),
341
+ history: join(taskDirectory, "history.md"),
342
+ output: join(taskDirectory, "output.log")
343
+ }
344
+ }
345
+
346
+ export function archivedTaskStoragePaths(paths: TaskStoragePaths): TaskStoragePaths {
347
+ return taskStoragePaths({ ...paths, parentDirectory: join(paths.root, "archive", paths.parentSessionId) }, paths.taskRef)
348
+ }
349
+
350
+ /** Only ownership is decoded across versions. Unsupported execution recipes stay unreadable. */
351
+ export async function readTaskIdentity(
352
+ paths: TaskStoragePaths
353
+ ): Promise<{ kind: "agent"; childSessionId: string } | { kind: "bash" } | undefined> {
354
+ try {
355
+ await assertRegularDirectory(dirname(paths.parentDirectory))
356
+ await assertRegularDirectory(paths.parentDirectory)
357
+ await assertRegularDirectory(paths.taskDirectory)
358
+ } catch (error) {
359
+ if (hasCode(error, "ENOENT")) return undefined
360
+ throw error
361
+ }
362
+ const stats = await lstat(paths.metadata)
363
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`Task metadata is not a regular file: ${paths.metadata}`)
364
+ const value: unknown = JSON.parse(await readFile(paths.metadata, "utf8"))
365
+ if (!isRecord(value) || property(value, "taskRef") !== paths.taskRef || property(value, "parentSessionId") !== paths.parentSessionId) {
366
+ throw new Error("Metadata identity does not match its parent/task path")
367
+ }
368
+ const kind = property(value, "kind")
369
+ if (kind === "bash" && paths.taskRef.startsWith("b_")) {
370
+ if (property(value, "childSessionId") !== undefined) throw new Error("Bash task cannot own a child session")
371
+ return { kind }
372
+ }
373
+ // Older agent identities did not require kind; decode ownership only, never their recipe.
374
+ const legacyAgent =
375
+ kind === undefined && typeof property(value, "version") === "number" && property(value, "version") !== TASK_METADATA_VERSION
376
+ if ((kind !== "agent" && !legacyAgent) || !paths.taskRef.startsWith("a_")) throw new Error("Task kind does not match its reference")
377
+ const childSessionId = property(value, "childSessionId")
378
+ if (typeof childSessionId !== "string" || !SESSION_ID_PATTERN.test(childSessionId) || childSessionId === paths.parentSessionId) {
379
+ throw new Error("Invalid child session identity")
380
+ }
381
+ return { kind: "agent", childSessionId }
382
+ }
383
+
384
+ /** Moves stopped work under archive without rewriting its retained metadata. */
385
+ export async function archiveTaskStorage(paths: TaskStoragePaths): Promise<void> {
386
+ await serializeMetadataMutation(paths.metadata, async () => {
387
+ const archived = archivedTaskStoragePaths(paths)
388
+ await assertRegularDirectory(paths.taskDirectory)
389
+ for (const directory of [dirname(archived.parentDirectory), archived.parentDirectory]) {
390
+ await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE })
391
+ await assertRegularDirectory(directory)
392
+ }
393
+ try {
394
+ await lstat(archived.taskDirectory)
395
+ } catch (error) {
396
+ if (!hasCode(error, "ENOENT")) throw error
397
+ await rename(paths.taskDirectory, archived.taskDirectory)
398
+ await Promise.all([syncDirectory(paths.parentDirectory), syncDirectory(archived.parentDirectory)])
399
+ publishTaskUpdate(paths.workspace, paths.parentSessionId)
400
+ return
401
+ }
402
+ throw new Error(`Archive already exists: ${archived.taskDirectory}`)
403
+ })
404
+ }
405
+
406
+ export function createTaskReference(kind: TaskMetadata["kind"] = "agent"): string {
407
+ return `${kind === "bash" ? "b" : "a"}_${randomBytes(4).toString("hex")}`
408
+ }
409
+
410
+ /** Creates and verifies the private root and exact parent partition. */
411
+ export async function ensureParentStorage(cwd: string, parentSessionId: string): Promise<ParentStoragePaths> {
412
+ const paths = parentStoragePaths(cwd, parentSessionId)
413
+ const configDirectory = dirname(paths.root)
414
+ await mkdir(configDirectory, { recursive: true })
415
+ await assertRegularDirectory(configDirectory)
416
+ const [realWorkspace, realConfigDirectory] = await Promise.all([realpath(paths.workspace), realpath(configDirectory)])
417
+ if (dirname(realConfigDirectory) !== realWorkspace) {
418
+ throw new Error(`Lovely Agents config directory escapes the workspace: ${configDirectory}`)
419
+ }
420
+
421
+ try {
422
+ await mkdir(paths.root, { mode: DIRECTORY_MODE })
423
+ } catch (error) {
424
+ if (!hasCode(error, "EEXIST")) throw error
425
+ }
426
+ await assertRegularDirectory(paths.root)
427
+ const realRoot = await realpath(paths.root)
428
+ if (dirname(realRoot) !== realConfigDirectory) throw new Error(`Lovely Agents storage escapes the workspace: ${paths.root}`)
429
+ await chmod(paths.root, DIRECTORY_MODE)
430
+
431
+ try {
432
+ await mkdir(paths.parentDirectory, { mode: DIRECTORY_MODE })
433
+ } catch (error) {
434
+ if (!hasCode(error, "EEXIST")) throw error
435
+ }
436
+ await assertRegularDirectory(paths.parentDirectory)
437
+ if (dirname(await realpath(paths.parentDirectory)) !== realRoot) {
438
+ throw new Error(`Lovely Agents parent partition escapes its storage root: ${paths.parentDirectory}`)
439
+ }
440
+ await chmod(paths.parentDirectory, DIRECTORY_MODE)
441
+ await ensureStorageGitignore(paths.root)
442
+ return paths
443
+ }
444
+
445
+ /**
446
+ * Acquires one durable parent-partition lease. Duplicate calls in this process
447
+ * return the same lease, including across extension runtime reloads.
448
+ */
449
+ export async function acquireParentLease(cwd: string, parentSessionId: string): Promise<ParentLease> {
450
+ const paths = await ensureParentStorage(cwd, parentSessionId)
451
+ const state = parentLeaseState()
452
+ return serializeOperation(state.queues, paths.lease, async () => {
453
+ const existing = state.leases.get(paths.lease)
454
+ if (existing) {
455
+ const loaded = await loadParentLease(paths.lease)
456
+ if (loaded.status === "ok" && loaded.lease.pid === existing.pid && loaded.lease.token === existing.token) {
457
+ return existing
458
+ }
459
+ state.leases.delete(paths.lease)
460
+ throw new ParentLeaseError(`Process-global lease ownership no longer matches ${paths.lease}`)
461
+ }
462
+
463
+ const leaseFile: ParentLeaseFile = {
464
+ version: PARENT_LEASE_VERSION,
465
+ pid: process.pid,
466
+ token: randomBytes(16).toString("hex"),
467
+ createdAt: Date.now()
468
+ }
469
+ const candidate = `${paths.lease}.${process.pid}.${leaseFile.token}.tmp`
470
+ await writePrivateFile(candidate, `${JSON.stringify(leaseFile)}\n`)
471
+ try {
472
+ for (let attempt = 0; attempt < MAX_LEASE_ACQUIRE_ATTEMPTS; attempt++) {
473
+ try {
474
+ await link(candidate, paths.lease)
475
+ try {
476
+ await syncDirectory(paths.parentDirectory)
477
+ } catch (error) {
478
+ await removeIfPresent(paths.lease)
479
+ throw error
480
+ }
481
+ const lease = Object.freeze({ ...leaseFile, paths: Object.freeze({ ...paths }) })
482
+ state.leases.set(paths.lease, lease)
483
+ return lease
484
+ } catch (error) {
485
+ if (!hasCode(error, "EEXIST")) throw error
486
+ }
487
+
488
+ const loaded = await loadParentLease(paths.lease)
489
+ if (loaded.status === "missing") continue
490
+ if (loaded.status === "invalid") {
491
+ throw new ParentLeaseError(`Cannot acquire invalid parent lease ${paths.lease}: ${loaded.message}`)
492
+ }
493
+ if (processIsAlive(loaded.lease.pid)) {
494
+ throw new ParentLeaseConflictError(paths.lease, loaded.lease.pid)
495
+ }
496
+
497
+ const confirmed = await loadParentLease(paths.lease)
498
+ if (confirmed.status !== "ok" || confirmed.lease.pid !== loaded.lease.pid || confirmed.lease.token !== loaded.lease.token) {
499
+ continue
500
+ }
501
+ try {
502
+ await unlink(paths.lease)
503
+ await syncDirectory(paths.parentDirectory)
504
+ } catch (error) {
505
+ if (!hasCode(error, "ENOENT")) throw error
506
+ }
507
+ }
508
+ throw new ParentLeaseError(`Unable to acquire changing parent lease: ${paths.lease}`)
509
+ } finally {
510
+ await removeIfPresent(candidate)
511
+ }
512
+ })
513
+ }
514
+
515
+ /** Releases only the matching process-global lease; repeated release is safe. */
516
+ export async function releaseParentLease(lease: ParentLease): Promise<void> {
517
+ const state = parentLeaseState()
518
+ await serializeOperation(state.queues, lease.paths.lease, async () => {
519
+ if (state.leases.get(lease.paths.lease) !== lease) return
520
+
521
+ const loaded = await loadParentLease(lease.paths.lease)
522
+ if (loaded.status === "missing") {
523
+ state.leases.delete(lease.paths.lease)
524
+ return
525
+ }
526
+ if (loaded.status === "invalid" || loaded.lease.pid !== lease.pid || loaded.lease.token !== lease.token) {
527
+ state.leases.delete(lease.paths.lease)
528
+ throw new ParentLeaseError(`Refusing to release a parent lease no longer owned by this process: ${lease.paths.lease}`)
529
+ }
530
+
531
+ await unlink(lease.paths.lease)
532
+ state.leases.delete(lease.paths.lease)
533
+ await syncDirectory(lease.paths.parentDirectory)
534
+ })
535
+ }
536
+
537
+ /** Releases a registered lease by identity after a semantic parent close. */
538
+ export async function releaseParentLeaseFor(cwd: string, parentSessionId: string): Promise<boolean> {
539
+ const path = parentStoragePaths(cwd, parentSessionId).lease
540
+ const lease = parentLeaseState().leases.get(path)
541
+ if (!lease) return false
542
+ await releaseParentLease(lease)
543
+ return true
544
+ }
545
+
546
+ /** Creates missing logs without overwriting retained content. */
547
+ export async function initializeRetainedLogs(paths: TaskStoragePaths): Promise<void> {
548
+ await ensurePrivateLogFile(paths.history)
549
+ await ensurePrivateLogFile(paths.taskRef.startsWith("b_") ? paths.output : paths.session)
550
+ await syncDirectory(paths.taskDirectory)
551
+ }
552
+
553
+ export async function appendHistoryLog(paths: TaskStoragePaths, entry: HistoryEntry): Promise<void> {
554
+ await appendRetainedLog(paths.history, renderHistoryEntry(entry))
555
+ publishTaskUpdate(paths.workspace, paths.parentSessionId)
556
+ }
557
+
558
+ /** Coalesced observed work; late events cannot overwrite a newer or settled run. */
559
+ export async function writeTaskProgress(
560
+ paths: TaskStoragePaths,
561
+ runId: string,
562
+ progress: Partial<Pick<TaskMetadata, "latestReply" | "lastActivity">> & { effectiveSystemPrompt?: string }
563
+ ): Promise<void> {
564
+ await mutateTaskMetadata(paths, metadata => {
565
+ if (metadata.discardedAt !== null || metadata.activeRun?.id !== runId || metadata.state !== "running") return metadata
566
+ if (metadata.kind === "bash" && progress.effectiveSystemPrompt !== undefined) {
567
+ throw new Error("Bash tasks do not have a system prompt")
568
+ }
569
+ return {
570
+ ...metadata,
571
+ ...progress,
572
+ updatedAt: Date.now()
573
+ }
574
+ })
575
+ }
576
+
577
+ /** Explain queued work without pretending to know its ETA or FIFO position. */
578
+ export function taskSchedulingStatus(
579
+ metadata: Pick<AgentTaskMetadata, "kind" | "state" | "model"> | Pick<BashTaskMetadata, "kind" | "state">
580
+ ): Pick<RetainedOutputRead, "queueReason" | "capacity"> {
581
+ const coordinator = metadata.kind === "bash" ? getBashCoordinator() : getAgentCoordinator()
582
+ const capacity = { active: coordinator.activeCount, limit: coordinator.maxConcurrency }
583
+ const queueReason =
584
+ metadata.state !== "queued"
585
+ ? null
586
+ : metadata.kind === "agent" && !coordinator.isTupleOpen({ provider: metadata.model.provider, model: metadata.model.id })
587
+ ? "provider-limit"
588
+ : capacity.active >= capacity.limit
589
+ ? "capacity"
590
+ : "starting"
591
+ return { queueReason, capacity }
592
+ }
593
+
594
+ export function retainedPaths(paths: TaskStoragePaths): RetainedPaths {
595
+ return {
596
+ history: displayWorkspacePath(paths.workspace, paths.history),
597
+ ...(paths.taskRef.startsWith("b_")
598
+ ? { output: displayWorkspacePath(paths.workspace, paths.output) }
599
+ : { session: displayWorkspacePath(paths.workspace, paths.session) })
600
+ }
601
+ }
602
+
603
+ export async function countRetainedOutputLines(paths: TaskStoragePaths): Promise<number> {
604
+ return splitCompleteLines((await requireTaskMetadata(paths)).latestReply?.text ?? "").length
605
+ }
606
+
607
+ /**
608
+ * Returns a snapshot, never transcript pages. With waitMs, wait for the current
609
+ * run to end or suspend, not for partial output, activity, or capacity changes.
610
+ */
611
+ export async function readRetainedOutput(paths: TaskStoragePaths, options: RetainedOutputReadOptions = {}): Promise<RetainedOutputRead> {
612
+ const waitMs = options.waitMs ?? 0
613
+ if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > RETAINED_OUTPUT_MAX_WAIT_MS) {
614
+ throw new Error(`waitMs must be an integer from 0 to ${RETAINED_OUTPUT_MAX_WAIT_MS}`)
615
+ }
616
+
617
+ if (options.signal?.aborted) throw abortReason(options.signal)
618
+ let metadata = await requireTaskMetadata(paths)
619
+ let timedOut = false
620
+ if (waitMs > 0 && metadata.activeRun && metadata.state !== "suspended") {
621
+ const settled = await waitForRunEnd(paths, metadata.activeRun.id, waitMs, options.signal)
622
+ timedOut = settled === undefined
623
+ metadata = settled ?? (await requireTaskMetadata(paths))
624
+ }
625
+
626
+ return retainedOutputSnapshot(paths, metadata, timedOut)
627
+ }
628
+
629
+ /** Formats an immutable run result before a later run can replace its reply. */
630
+ export function retainedOutputSnapshot(paths: TaskStoragePaths, metadata: TaskMetadata, timedOut = false): RetainedOutputRead {
631
+ const fullText = metadata.latestReply?.text ?? ""
632
+ const lines = splitCompleteLines(fullText)
633
+ const text = truncateUtf8(lines.slice(0, RETAINED_OUTPUT_MAX_LINES).join("\n"), RETAINED_OUTPUT_MAX_BYTES)
634
+ const snapshotTruncated = lines.length > RETAINED_OUTPUT_MAX_LINES || Buffer.byteLength(fullText) > RETAINED_OUTPUT_MAX_BYTES
635
+ const truncated = snapshotTruncated || (metadata.kind === "bash" && metadata.latestReply?.truncated === true)
636
+ return {
637
+ text:
638
+ metadata.kind === "bash"
639
+ ? `${snapshotTruncated ? text : fullText}\n\n[${truncated ? "Output truncated; showing tail. " : ""}Full output: ${retainedPaths(paths).output}]`
640
+ : truncated
641
+ ? `${text}\n\n[Reply truncated. Full replies: ${retainedPaths(paths).history}]`
642
+ : fullText,
643
+ totalLines: lines.length,
644
+ truncated,
645
+ timedOut,
646
+ state: metadata.state,
647
+ latestOutcome: metadata.activeRun ? null : metadata.latestOutcome,
648
+ streaming: metadata.state === "running" && (metadata.latestReply?.streaming ?? false),
649
+ queuedFollowUps: metadata.queuedFollowUps.length,
650
+ lastActivity: metadata.lastActivity ?? null,
651
+ ...(metadata.kind === "bash" ? { exitCode: metadata.exitCode, signal: metadata.signal } : {}),
652
+ ...taskSchedulingStatus(metadata),
653
+ paths: retainedPaths(paths)
654
+ }
655
+ }
656
+
657
+ /** Atomically reserves a fresh task directory; existing names are collisions. */
658
+ export async function reserveTaskStorage(
659
+ parent: ParentStoragePaths,
660
+ nextReference: () => string = createTaskReference
661
+ ): Promise<TaskStoragePaths> {
662
+ for (let attempt = 0; attempt < MAX_TASK_REFERENCE_ATTEMPTS; attempt++) {
663
+ const paths = taskStoragePaths(parent, nextReference())
664
+ try {
665
+ const reserved = await serializeMetadataMutation(paths.metadata, async () => {
666
+ try {
667
+ await lstat(archivedTaskStoragePaths(paths).taskDirectory)
668
+ return false
669
+ } catch (error) {
670
+ if (!hasCode(error, "ENOENT")) throw error
671
+ }
672
+ await mkdir(paths.taskDirectory, { mode: DIRECTORY_MODE })
673
+ return true
674
+ })
675
+ if (reserved) return paths
676
+ } catch (error) {
677
+ if (!hasCode(error, "EEXIST")) throw error
678
+ }
679
+ }
680
+ throw new Error(`Unable to reserve a unique Task Reference after ${MAX_TASK_REFERENCE_ATTEMPTS} attempts`)
681
+ }
682
+
683
+ export async function readTaskMetadata(paths: TaskStoragePaths): Promise<MetadataLoadResult> {
684
+ let source: string
685
+ try {
686
+ const stats = await lstat(paths.metadata)
687
+ if (!stats.isFile() || stats.isSymbolicLink()) {
688
+ return invalidMetadata(paths.metadata, "unreadable", "metadata.json is not a regular file")
689
+ }
690
+ source = await readFile(paths.metadata, "utf8")
691
+ } catch (error) {
692
+ if (hasCode(error, "ENOENT")) {
693
+ try {
694
+ if (await readTaskIdentity(archivedTaskStoragePaths(paths))) {
695
+ return invalidMetadata(paths.metadata, "unreadable", `Task ${paths.taskRef} has been discarded`)
696
+ }
697
+ return { status: "missing" }
698
+ } catch (archiveError) {
699
+ return invalidMetadata(paths.metadata, "unreadable", errorMessage(archiveError))
700
+ }
701
+ }
702
+ return invalidMetadata(paths.metadata, "unreadable", errorMessage(error))
703
+ }
704
+
705
+ let value: unknown
706
+ try {
707
+ value = JSON.parse(source)
708
+ } catch (error) {
709
+ return invalidMetadata(paths.metadata, "invalid-json", errorMessage(error))
710
+ }
711
+ const version = isRecord(value) ? property(value, "version") : undefined
712
+ if (typeof version === "number" && version !== TASK_METADATA_VERSION) {
713
+ return invalidMetadata(paths.metadata, "unsupported-version", `Unsupported metadata version ${version}`)
714
+ }
715
+ const validated = validateTaskMetadata(value)
716
+ if (!validated.ok) return invalidMetadata(paths.metadata, "invalid-metadata", validated.message)
717
+ if (validated.value.taskRef !== paths.taskRef || validated.value.parentSessionId !== paths.parentSessionId) {
718
+ return invalidMetadata(paths.metadata, "invalid-metadata", "Metadata identity does not match its parent/task path")
719
+ }
720
+ return { status: "ok", metadata: validated.value }
721
+ }
722
+
723
+ /** Replaces metadata through a same-directory, fsynced temporary file. */
724
+ export function writeTaskMetadata(paths: TaskStoragePaths, metadata: TaskMetadata): Promise<void> {
725
+ return serializeMetadataMutation(paths.metadata, async () => {
726
+ assertMetadataForPath(paths, metadata)
727
+ const snapshot = metadata.activeRun ? { ...metadata, inputPreview: historyPreview(metadata.activeRun.input, 512) } : metadata
728
+ await atomicWriteMetadata(paths.metadata, snapshot)
729
+ publishTaskUpdate(paths.workspace, paths.parentSessionId)
730
+ })
731
+ }
732
+
733
+ /** Reads, transforms, validates, and durably writes one task under a per-task queue. */
734
+ export function mutateTaskMetadata(
735
+ paths: TaskStoragePaths,
736
+ mutate: (metadata: TaskMetadata) => TaskMetadata | Promise<TaskMetadata>
737
+ ): Promise<TaskMetadata> {
738
+ return serializeMetadataMutation(paths.metadata, async () => {
739
+ const loaded = await readTaskMetadata(paths)
740
+ if (loaded.status !== "ok") throw new InvalidTaskMetadataError(metadataLoadError(loaded, paths.metadata))
741
+ const updated = await mutate(structuredClone(loaded.metadata))
742
+ if (updated.activeRun && updated.activeRun.id !== loaded.metadata.activeRun?.id) {
743
+ updated.latestReply = null
744
+ updated.lastActivity = { at: updated.updatedAt, action: updated.state }
745
+ updated.inputPreview = historyPreview(updated.activeRun.input, 512)
746
+ if (updated.kind === "agent") delete updated.effectiveSystemPrompt
747
+ }
748
+ if (updated.state !== "running" && updated.latestReply) updated.latestReply.streaming = false
749
+ assertMetadataForPath(paths, updated)
750
+ await atomicWriteMetadata(paths.metadata, updated)
751
+ publishTaskUpdate(paths.workspace, paths.parentSessionId)
752
+ return updated
753
+ })
754
+ }
755
+
756
+ export function assertTaskMetadata(value: unknown): asserts value is TaskMetadata {
757
+ const validated = validateTaskMetadata(value)
758
+ if (!validated.ok) throw new InvalidTaskMetadataError(validated.message)
759
+ }
760
+
761
+ function validateTaskMetadata(value: unknown): { ok: true; value: TaskMetadata } | { ok: false; message: string } {
762
+ if (!Value.Check(TaskMetadataSchema, value)) {
763
+ const error = Value.Errors(TaskMetadataSchema, value)[0]
764
+ return { ok: false, message: error ? `${error.instancePath || "/"} ${error.message}` : "Invalid task metadata" }
765
+ }
766
+ const semanticError = taskMetadataSemanticError(value)
767
+ return semanticError ? { ok: false, message: semanticError } : { ok: true, value }
768
+ }
769
+
770
+ function taskMetadataSemanticError(value: TaskMetadata): string | undefined {
771
+ if (value.kind === "bash") {
772
+ const commandError = inputValidationError(value.command, "/command")
773
+ if (commandError) return commandError
774
+ if (!isAbsolute(value.cwd)) return "/cwd must be absolute"
775
+ if (value.state === "suspended") return "Bash tasks cannot be suspended"
776
+ if (value.queuedFollowUps.length > 0) return "Bash tasks cannot queue Follow-ups"
777
+ if (value.lastRunSequence > 1 || (value.activeRun && (value.activeRun.kind !== "initial" || value.activeRun.sequence !== 1))) {
778
+ return "Bash tasks support only an initial run"
779
+ }
780
+ if (value.notifications.some(notification => notification.type === "suspension")) return "Bash tasks cannot suspend"
781
+ }
782
+ if (!value.label.trim()) return "/label must be nonblank"
783
+ if (Buffer.byteLength(value.label, "utf8") > MAX_AGENT_LABEL_BYTES) {
784
+ return `/label must be at most ${MAX_AGENT_LABEL_BYTES} UTF-8 bytes`
785
+ }
786
+ if (value.updatedAt < value.createdAt) return "/updatedAt must not precede /createdAt"
787
+ if (value.discardedAt !== null && value.discardedAt < value.createdAt) return "/discardedAt must not precede /createdAt"
788
+ if ((value.activeRun === null) !== (value.state === "idle" || value.state === "interrupted")) {
789
+ return "/activeRun must exist exactly while state is queued, running, or suspended"
790
+ }
791
+ if (value.activeRun) {
792
+ if (value.activeRun.state !== value.state) return "/activeRun/state must match /state"
793
+ if (value.activeRun.sequence > value.lastRunSequence) return "/activeRun/sequence exceeds /lastRunSequence"
794
+ if (value.activeRun.state !== "queued" && value.activeRun.startedAt === undefined) {
795
+ return "/activeRun/startedAt is required after leaving queued state"
796
+ }
797
+ const inputError = inputValidationError(value.activeRun.input, "/activeRun/input")
798
+ if (inputError) return inputError
799
+ }
800
+ let priorSequence = value.activeRun?.sequence ?? 0
801
+ for (let index = 0; index < value.queuedFollowUps.length; index++) {
802
+ const followUp = value.queuedFollowUps[index]
803
+ if (!followUp) continue
804
+ if (followUp.sequence <= priorSequence) return `/queuedFollowUps/${index}/sequence must be strictly increasing`
805
+ if (followUp.sequence > value.lastRunSequence) return `/queuedFollowUps/${index}/sequence exceeds /lastRunSequence`
806
+ const contentError = inputValidationError(followUp.content, `/queuedFollowUps/${index}/content`)
807
+ if (contentError) return contentError
808
+ priorSequence = followUp.sequence
809
+ }
810
+ for (let index = 0; index < value.notifications.length; index++) {
811
+ if (Buffer.byteLength(value.notifications[index]?.content ?? "", "utf8") > MAX_NOTIFICATION_CONTENT_BYTES) {
812
+ return `/notifications/${index}/content must be at most ${MAX_NOTIFICATION_CONTENT_BYTES} UTF-8 bytes`
813
+ }
814
+ }
815
+ return undefined
816
+ }
817
+
818
+ function inputValidationError(value: string, path: string): string | undefined {
819
+ if (!value.trim()) return `${path} must be nonblank`
820
+ if (Buffer.byteLength(value, "utf8") > MAX_AGENT_INPUT_BYTES) return `${path} must be at most ${MAX_AGENT_INPUT_BYTES} UTF-8 bytes`
821
+ return undefined
822
+ }
823
+
824
+ function assertMetadataForPath(paths: TaskStoragePaths, metadata: unknown): asserts metadata is TaskMetadata {
825
+ assertTaskMetadata(metadata)
826
+ if (metadata.taskRef !== paths.taskRef || metadata.parentSessionId !== paths.parentSessionId) {
827
+ throw new InvalidTaskMetadataError("Metadata identity does not match its parent/task path")
828
+ }
829
+ }
830
+
831
+ function renderHistoryEntry(entry: HistoryEntry): string {
832
+ switch (entry.type) {
833
+ case "run-start":
834
+ return `<run ${entry.sequence} ${entry.kind}>\n`
835
+ case "input":
836
+ return taggedBlockEntry(
837
+ entry.delivery === "stdin" ? "stdin" : entry.delivery === "steer" ? "steer" : "user",
838
+ entry.content,
839
+ entry.delivery === "stdin"
840
+ )
841
+ case "assistant":
842
+ return taggedBlockEntry("agent", entry.content)
843
+ case "output":
844
+ return taggedBlockEntry("output", entry.content)
845
+ case "stdin":
846
+ return `${taggedBlockEntry("stdin", entry.content, true)}${entry.eof ? "<stdin EOF>\n" : ""}`
847
+ case "run-end":
848
+ return `<outcome ${entry.outcome}>\n${entry.summary ? taggedBlockEntry("summary", entry.summary) : ""}\n`
849
+ case "tool":
850
+ return `<tool ${historyPreview(entry.tool, 80)} ${entry.isError ? "error" : "ok"}>\n${historyPreview(entry.arguments, 160)} → ${historyPreview(entry.result, 240)}\n`
851
+ }
852
+ }
853
+
854
+ function taggedBlockEntry(tag: string, content: string, literal = false): string {
855
+ const body = literal ? content : content.replace(/\r\n?/g, "\n").replace(/\n+$/, "")
856
+ return `<${tag}>\n${body}${body.endsWith("\n") ? "" : "\n"}`
857
+ }
858
+
859
+ function historyPreview(content: string, maximumBytes: number): string {
860
+ const singleLine = content.replace(/\s+/g, " ").trim()
861
+ return truncateUtf8(singleLine, maximumBytes)
862
+ }
863
+
864
+ function truncateUtf8(content: string, maximumBytes: number): string {
865
+ const bytes = Buffer.from(content)
866
+ if (bytes.length <= maximumBytes) return content
867
+ let end = maximumBytes - 3
868
+ while (end > 0 && isUtf8Continuation(bytes[end])) end--
869
+ return `${bytes.subarray(0, end).toString("utf8")}...`
870
+ }
871
+
872
+ async function requireTaskMetadata(paths: TaskStoragePaths): Promise<TaskMetadata> {
873
+ const loaded = await readTaskMetadata(paths)
874
+ if (loaded.status !== "ok") throw new InvalidTaskMetadataError(metadataLoadError(loaded, paths.metadata))
875
+ if (loaded.metadata.discardedAt !== null) throw new Error(`Task ${paths.taskRef} has been discarded`)
876
+ return loaded.metadata
877
+ }
878
+
879
+ async function waitForRunEnd(
880
+ paths: TaskStoragePaths,
881
+ runId: string,
882
+ waitMs: number,
883
+ signal?: AbortSignal
884
+ ): Promise<TaskMetadata | undefined> {
885
+ if (signal?.aborted) throw abortReason(signal)
886
+ return new Promise<TaskMetadata | undefined>((resolvePromise, rejectPromise) => {
887
+ let settled = false
888
+ let checking = false
889
+ let checkPending = false
890
+ const watcher = watch(paths.taskDirectory, { persistent: false }, () => {
891
+ requestCheck()
892
+ })
893
+ const timer = setTimeout(() => finish(undefined), waitMs)
894
+ const onAbort = () => fail(abortReason(signal))
895
+ const cleanup = () => {
896
+ clearTimeout(timer)
897
+ watcher.close()
898
+ signal?.removeEventListener("abort", onAbort)
899
+ }
900
+ const finish = (metadata: TaskMetadata | undefined) => {
901
+ if (settled) return
902
+ settled = true
903
+ cleanup()
904
+ resolvePromise(metadata)
905
+ }
906
+ const fail = (error: unknown) => {
907
+ if (settled) return
908
+ settled = true
909
+ cleanup()
910
+ rejectPromise(error)
911
+ }
912
+ const requestCheck = () => {
913
+ if (checking) {
914
+ checkPending = true
915
+ return
916
+ }
917
+ void check()
918
+ }
919
+ const check = async () => {
920
+ if (settled) return
921
+ checking = true
922
+ try {
923
+ const current = await requireTaskMetadata(paths)
924
+ // A later Follow-up must not extend a wait for the run we observed.
925
+ if (current.activeRun?.id !== runId || current.state === "suspended") finish(current)
926
+ } catch (error) {
927
+ fail(error)
928
+ } finally {
929
+ checking = false
930
+ if (checkPending) {
931
+ checkPending = false
932
+ requestCheck()
933
+ }
934
+ }
935
+ }
936
+
937
+ watcher.once("error", fail)
938
+ signal?.addEventListener("abort", onAbort, { once: true })
939
+ requestCheck()
940
+ })
941
+ }
942
+
943
+ function splitCompleteLines(content: string): string[] {
944
+ if (!content) return []
945
+ const lines = content.split("\n")
946
+ if (content.endsWith("\n")) lines.pop()
947
+ return lines
948
+ }
949
+
950
+ export function displayWorkspacePath(workspace: string, path: string): string {
951
+ const display = relative(workspace, path)
952
+ if (display === "" || display === ".." || display.startsWith(`..${sep}`) || isAbsolute(display)) return path
953
+ return display.split(sep).join("/")
954
+ }
955
+
956
+ function isUtf8Continuation(byte: number | undefined): boolean {
957
+ return byte !== undefined && (byte & 0xc0) === 0x80
958
+ }
959
+
960
+ function abortReason(signal?: AbortSignal): unknown {
961
+ return signal?.reason ?? new Error("Operation aborted")
962
+ }
963
+
964
+ async function ensureStorageGitignore(root: string): Promise<void> {
965
+ const path = join(root, ".gitignore")
966
+ try {
967
+ await writePrivateFile(path, STORAGE_GITIGNORE)
968
+ } catch (error) {
969
+ if (hasCode(error, "EEXIST")) return
970
+ throw error
971
+ }
972
+ }
973
+
974
+ async function assertRegularDirectory(path: string): Promise<void> {
975
+ const stats = await lstat(path)
976
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error(`Lovely Agents storage path is not a regular directory: ${path}`)
977
+ }
978
+
979
+ async function ensurePrivateLogFile(path: string): Promise<void> {
980
+ try {
981
+ await writePrivateFile(path, "")
982
+ return
983
+ } catch (error) {
984
+ if (!hasCode(error, "EEXIST")) throw error
985
+ }
986
+ const stats = await lstat(path)
987
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`Retained log is not a regular file: ${path}`)
988
+ await chmod(path, FILE_MODE)
989
+ }
990
+
991
+ function appendRetainedLog(path: string, content: string): Promise<void> {
992
+ return serializeOperation(taskQueueState().logs, path, async () => {
993
+ await ensurePrivateLogFile(path)
994
+ const handle = await open(path, "a", FILE_MODE)
995
+ try {
996
+ await handle.writeFile(content, "utf8")
997
+ await handle.sync()
998
+ } finally {
999
+ await handle.close()
1000
+ }
1001
+ })
1002
+ }
1003
+
1004
+ async function loadParentLease(path: string): Promise<ParentLeaseLoadResult> {
1005
+ let source: string
1006
+ try {
1007
+ const stats = await lstat(path)
1008
+ if (!stats.isFile() || stats.isSymbolicLink()) return { status: "invalid", message: "lease is not a regular file" }
1009
+ source = await readFile(path, "utf8")
1010
+ } catch (error) {
1011
+ if (hasCode(error, "ENOENT")) return { status: "missing" }
1012
+ return { status: "invalid", message: errorMessage(error) }
1013
+ }
1014
+
1015
+ let value: unknown
1016
+ try {
1017
+ value = JSON.parse(source)
1018
+ } catch (error) {
1019
+ return { status: "invalid", message: errorMessage(error) }
1020
+ }
1021
+ if (!Value.Check(ParentLeaseFileSchema, value)) {
1022
+ const error = Value.Errors(ParentLeaseFileSchema, value)[0]
1023
+ return { status: "invalid", message: error ? `${error.instancePath || "/"} ${error.message}` : "invalid lease data" }
1024
+ }
1025
+ return { status: "ok", lease: value }
1026
+ }
1027
+
1028
+ async function writePrivateFile(path: string, content: string): Promise<void> {
1029
+ let handle: Awaited<ReturnType<typeof open>> | undefined
1030
+ let complete = false
1031
+ try {
1032
+ handle = await open(path, "wx", FILE_MODE)
1033
+ await handle.writeFile(content, "utf8")
1034
+ await handle.sync()
1035
+ complete = true
1036
+ } finally {
1037
+ await handle?.close()
1038
+ if (handle && !complete) await removeIfPresent(path)
1039
+ }
1040
+ }
1041
+
1042
+ async function atomicWriteMetadata(path: string, metadata: TaskMetadata): Promise<void> {
1043
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`)
1044
+ let handle: Awaited<ReturnType<typeof open>> | undefined
1045
+ let renamed = false
1046
+ try {
1047
+ handle = await open(temporary, "wx", FILE_MODE)
1048
+ await handle.writeFile(`${JSON.stringify(metadata, null, 2)}\n`, "utf8")
1049
+ await handle.sync()
1050
+ await handle.close()
1051
+ handle = undefined
1052
+ await rename(temporary, path)
1053
+ renamed = true
1054
+ await syncDirectory(dirname(path))
1055
+ } finally {
1056
+ await handle?.close()
1057
+ if (!renamed) await removeIfPresent(temporary)
1058
+ }
1059
+ }
1060
+
1061
+ async function syncDirectory(path: string): Promise<void> {
1062
+ if (process.platform === "win32") return
1063
+ const handle = await open(path, "r")
1064
+ try {
1065
+ await handle.sync()
1066
+ } catch (error) {
1067
+ if (!hasCode(error, "EINVAL") && !hasCode(error, "ENOTSUP")) throw error
1068
+ } finally {
1069
+ await handle.close()
1070
+ }
1071
+ }
1072
+
1073
+ function serializeMetadataMutation<T>(path: string, operation: () => Promise<T>): Promise<T> {
1074
+ return serializeOperation(taskQueueState().metadata, path, operation)
1075
+ }
1076
+
1077
+ function serializeOperation<T>(queues: Map<string, Promise<void>>, path: string, operation: () => Promise<T>): Promise<T> {
1078
+ const preceding = queues.get(path) ?? Promise.resolve()
1079
+ const result = preceding.then(operation)
1080
+ const tail = result.then(
1081
+ () => undefined,
1082
+ () => undefined
1083
+ )
1084
+ queues.set(path, tail)
1085
+ return result.finally(() => {
1086
+ if (queues.get(path) === tail) queues.delete(path)
1087
+ })
1088
+ }
1089
+
1090
+ function parentLeaseState(): ParentLeaseState {
1091
+ const globals = globalThis as unknown as { [key: symbol]: unknown }
1092
+ const existing = globals[LEASE_STATE_SYMBOL]
1093
+ if (existing !== undefined) {
1094
+ if (!isParentLeaseState(existing)) throw new ParentLeaseError("Incompatible process-global Lovely Agents lease state")
1095
+ return existing
1096
+ }
1097
+ const state: ParentLeaseState = {
1098
+ version: PARENT_LEASE_VERSION,
1099
+ leases: new Map(),
1100
+ queues: new Map()
1101
+ }
1102
+ globals[LEASE_STATE_SYMBOL] = state
1103
+ return state
1104
+ }
1105
+
1106
+ function taskQueueState(): TaskQueueState {
1107
+ const globals = globalThis as unknown as { [key: symbol]: unknown }
1108
+ const existing = globals[TASK_QUEUE_STATE_SYMBOL]
1109
+ if (existing !== undefined) {
1110
+ const candidate = existing as Partial<TaskQueueState>
1111
+ if (candidate.version !== 1 || !(candidate.metadata instanceof Map) || !(candidate.logs instanceof Map)) {
1112
+ throw new Error("Incompatible process-global Lovely Agents task queue state")
1113
+ }
1114
+ return candidate as TaskQueueState
1115
+ }
1116
+ const created: TaskQueueState = { version: 1, metadata: new Map(), logs: new Map() }
1117
+ globals[TASK_QUEUE_STATE_SYMBOL] = created
1118
+ return created
1119
+ }
1120
+
1121
+ function isParentLeaseState(value: unknown): value is ParentLeaseState {
1122
+ return (
1123
+ isRecord(value) &&
1124
+ property(value, "version") === PARENT_LEASE_VERSION &&
1125
+ property(value, "leases") instanceof Map &&
1126
+ property(value, "queues") instanceof Map
1127
+ )
1128
+ }
1129
+
1130
+ function processIsAlive(pid: number): boolean {
1131
+ try {
1132
+ process.kill(pid, 0)
1133
+ return true
1134
+ } catch (error) {
1135
+ if (hasCode(error, "ESRCH")) return false
1136
+ if (hasCode(error, "EPERM")) return true
1137
+ throw error
1138
+ }
1139
+ }
1140
+
1141
+ function invalidMetadata(path: string, code: MetadataDiagnostic["code"], message: string): MetadataLoadResult {
1142
+ return { status: "invalid", diagnostic: { code, message, path } }
1143
+ }
1144
+
1145
+ function metadataLoadError(result: Exclude<MetadataLoadResult, { status: "ok" }>, path: string): string {
1146
+ return result.status === "missing" ? `Missing metadata: ${path}` : `${result.diagnostic.message}: ${result.diagnostic.path}`
1147
+ }
1148
+
1149
+ function assertSessionId(value: string): void {
1150
+ if (!SESSION_ID_PATTERN.test(value)) throw new Error(`Invalid parent session ID: ${value}`)
1151
+ }
1152
+
1153
+ function assertTaskReference(value: string): void {
1154
+ if (!TASK_REFERENCE_PATTERN.test(value)) throw new Error(`Invalid Task Reference: ${value}`)
1155
+ }
1156
+
1157
+ async function removeIfPresent(path: string): Promise<void> {
1158
+ try {
1159
+ await unlink(path)
1160
+ } catch (error) {
1161
+ if (!hasCode(error, "ENOENT")) throw error
1162
+ }
1163
+ }
1164
+
1165
+ function hasCode(error: unknown, code: string): boolean {
1166
+ return isRecord(error) && property(error, "code") === code
1167
+ }
1168
+
1169
+ function isRecord(value: unknown): value is Record<string, unknown> {
1170
+ return typeof value === "object" && value !== null
1171
+ }
1172
+
1173
+ function property(record: Record<string, unknown>, key: string): unknown {
1174
+ return record[key]
1175
+ }
1176
+
1177
+ function errorMessage(error: unknown): string {
1178
+ return error instanceof Error ? error.message : String(error)
1179
+ }