@xl0/pi-lovely-agents 0.1.0 → 0.1.2
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/CHANGELOG.md +31 -0
- package/README.md +44 -5
- package/extensions/lovely-agents/agent.ts +50 -20
- package/extensions/lovely-agents/bash.ts +5 -2
- package/extensions/lovely-agents/child-session.ts +43 -2
- package/extensions/lovely-agents/coordinator.ts +2 -0
- package/extensions/lovely-agents/definitions.ts +2 -1
- package/extensions/lovely-agents/index.ts +1 -1
- package/extensions/lovely-agents/lifecycle.ts +70 -39
- package/extensions/lovely-agents/management.ts +49 -10
- package/extensions/lovely-agents/notifications.ts +18 -5
- package/extensions/lovely-agents/state.ts +188 -56
- package/extensions/lovely-agents/storage.ts +204 -0
- package/extensions/lovely-agents/task-panel.ts +78 -29
- package/extensions/lovely-agents/tools.ts +38 -8
- package/package.json +3 -1
- package/scripts/prune-tasks.ts +244 -0
- package/skills/agent/SKILL.md +8 -6
|
@@ -4,9 +4,8 @@ import { appendTaskNotification, prepareTaskNotification } from "./notifications
|
|
|
4
4
|
import {
|
|
5
5
|
acquireParentLease,
|
|
6
6
|
appendHistoryLog,
|
|
7
|
-
archivedTaskStoragePaths,
|
|
8
|
-
archiveTaskStorage,
|
|
9
7
|
mutateTaskMetadata,
|
|
8
|
+
type ParentLease,
|
|
10
9
|
parentStoragePaths,
|
|
11
10
|
readTaskIdentity,
|
|
12
11
|
readTaskMetadata,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
type TaskStoragePaths,
|
|
17
16
|
taskStoragePaths
|
|
18
17
|
} from "./state.js"
|
|
18
|
+
import { readTaskDiscardMarker, rebuildActiveTaskLinks, syncActiveTaskLink, writeTaskDiscardMarker } from "./storage.js"
|
|
19
19
|
|
|
20
20
|
export type ReconciliationResult = {
|
|
21
21
|
interrupted: number
|
|
@@ -29,7 +29,7 @@ export type RecoveryResult = {
|
|
|
29
29
|
|
|
30
30
|
const DISCARD_OPERATIONS = Symbol.for("@xl0/pi-lovely-agents/discards/v1")
|
|
31
31
|
|
|
32
|
-
/** Stops and
|
|
32
|
+
/** Stops and tombstones an owned subtree without changing its canonical paths. */
|
|
33
33
|
export function discardTask(paths: TaskStoragePaths, visited = new Set([paths.parentSessionId])): Promise<void> {
|
|
34
34
|
const global = globalThis as typeof globalThis & { [DISCARD_OPERATIONS]?: Map<string, Promise<void>> }
|
|
35
35
|
global[DISCARD_OPERATIONS] ??= new Map()
|
|
@@ -38,11 +38,9 @@ export function discardTask(paths: TaskStoragePaths, visited = new Set([paths.pa
|
|
|
38
38
|
if (existing) return existing
|
|
39
39
|
const pending = (async () => {
|
|
40
40
|
const identity = await readTaskIdentity(paths)
|
|
41
|
-
if (!identity) {
|
|
42
|
-
if (await readTaskIdentity(archivedTaskStoragePaths(paths))) return
|
|
43
|
-
throw new Error(`Unknown Task Reference: ${paths.taskRef}`)
|
|
44
|
-
}
|
|
41
|
+
if (!identity) throw new Error(`Unknown Task Reference: ${paths.taskRef}`)
|
|
45
42
|
if (identity.kind === "agent" && visited.has(identity.childSessionId)) throw new Error("Cyclic task ownership")
|
|
43
|
+
const marked = await readTaskDiscardMarker(paths)
|
|
46
44
|
const loaded = await readTaskMetadata(paths)
|
|
47
45
|
if (loaded.status === "ok") {
|
|
48
46
|
await stopTask(paths)
|
|
@@ -54,23 +52,25 @@ export function discardTask(paths: TaskStoragePaths, visited = new Set([paths.pa
|
|
|
54
52
|
}))
|
|
55
53
|
// Fence input that raced the first stop before the tombstone committed.
|
|
56
54
|
await stopTask(paths)
|
|
57
|
-
} else if (loaded.status === "invalid" && loaded.diagnostic.code === "unsupported-version") {
|
|
58
|
-
|
|
55
|
+
} else if (marked || (loaded.status === "invalid" && loaded.diagnostic.code === "unsupported-version")) {
|
|
56
|
+
if (!marked) {
|
|
57
|
+
await getAgentCoordinator().getResident(paths.taskDirectory)?.stop()
|
|
58
|
+
await writeTaskDiscardMarker(paths)
|
|
59
|
+
}
|
|
59
60
|
} else {
|
|
60
61
|
throw new Error(loaded.status === "invalid" ? loaded.diagnostic.message : `Missing metadata: ${paths.metadata}`)
|
|
61
62
|
}
|
|
62
63
|
if (identity.kind === "agent") {
|
|
63
64
|
const descendants = new Set([...visited, identity.childSessionId])
|
|
64
65
|
const lease = await acquireParentLease(paths.workspace, identity.childSessionId)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await discardTask(child, descendants)
|
|
68
|
-
}
|
|
69
|
-
} finally {
|
|
70
|
-
await releaseParentLease(lease)
|
|
66
|
+
for (const child of await directTaskPaths(paths.workspace, identity.childSessionId, true)) {
|
|
67
|
+
await discardTask(child, descendants)
|
|
71
68
|
}
|
|
69
|
+
// Failed cleanup may leave residents using this borrowed partition lease.
|
|
70
|
+
await releaseParentLease(lease)
|
|
72
71
|
}
|
|
73
|
-
await
|
|
72
|
+
const warning = await syncActiveTaskLink(paths, true)
|
|
73
|
+
if (warning) throw new Error(warning)
|
|
74
74
|
})().finally(() => {
|
|
75
75
|
if (operations.get(paths.taskDirectory) === pending) operations.delete(paths.taskDirectory)
|
|
76
76
|
})
|
|
@@ -85,13 +85,27 @@ export async function reconcileParentTasks(cwd: string, parentSessionId: string)
|
|
|
85
85
|
await acquireParentLease(cwd, parentSessionId)
|
|
86
86
|
|
|
87
87
|
const result: ReconciliationResult = { interrupted: 0, diagnostics: [] }
|
|
88
|
+
try {
|
|
89
|
+
result.diagnostics.push(...(await rebuildActiveTaskLinks(parent)))
|
|
90
|
+
} catch (error) {
|
|
91
|
+
result.diagnostics.push(`Active task index: ${errorMessage(error)}`)
|
|
92
|
+
}
|
|
88
93
|
for (const paths of await directTaskPaths(cwd, parentSessionId)) {
|
|
94
|
+
try {
|
|
95
|
+
if (await readTaskDiscardMarker(paths)) continue
|
|
96
|
+
} catch (error) {
|
|
97
|
+
result.diagnostics.push(`${paths.taskDirectory}: ${errorMessage(error)}`)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
89
100
|
const loaded = await readTaskMetadata(paths)
|
|
90
101
|
if (loaded.status !== "ok") {
|
|
91
|
-
if (loaded.status === "invalid")
|
|
102
|
+
if (loaded.status === "invalid") {
|
|
103
|
+
const diagnostic = `${paths.taskDirectory}: ${loaded.diagnostic.message}`
|
|
104
|
+
if (!result.diagnostics.includes(diagnostic)) result.diagnostics.push(diagnostic)
|
|
105
|
+
}
|
|
92
106
|
continue
|
|
93
107
|
}
|
|
94
|
-
if (getAgentCoordinator().getResident(paths.taskDirectory)) continue
|
|
108
|
+
if (loaded.metadata.discardedAt !== null || getAgentCoordinator().getResident(paths.taskDirectory)) continue
|
|
95
109
|
if (loaded.metadata.state === "idle" || loaded.metadata.state === "interrupted") {
|
|
96
110
|
if (loaded.metadata.queuedFollowUps.length > 0) {
|
|
97
111
|
await mutateTaskMetadata(paths, metadata => ({ ...metadata, queuedFollowUps: [], updatedAt: Date.now() }))
|
|
@@ -135,7 +149,10 @@ export async function reconcileParentTasks(cwd: string, parentSessionId: string)
|
|
|
135
149
|
|
|
136
150
|
/** Stops active work and clears queued input throughout an owned task tree. */
|
|
137
151
|
export async function stopOwnedTaskTree(cwd: string, parentSessionId: string): Promise<void> {
|
|
138
|
-
|
|
152
|
+
const leases: ParentLease[] = []
|
|
153
|
+
await stopPartition(cwd, parentSessionId, new Set(), leases)
|
|
154
|
+
// A failed owner can remain live even when stopping its descendants succeeded.
|
|
155
|
+
for (const lease of leases.reverse()) await releaseParentLease(lease)
|
|
139
156
|
}
|
|
140
157
|
|
|
141
158
|
/** Requeues resident suspended work in the exact owned descendant tree. */
|
|
@@ -152,31 +169,32 @@ export async function stopTask(paths: TaskStoragePaths): Promise<void> {
|
|
|
152
169
|
else await settleStopped(paths)
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
async function stopPartition(cwd: string, parentSessionId: string, visited: Set<string
|
|
172
|
+
async function stopPartition(cwd: string, parentSessionId: string, visited: Set<string>, leases: ParentLease[]): Promise<void> {
|
|
156
173
|
if (visited.has(parentSessionId)) return
|
|
157
174
|
visited.add(parentSessionId)
|
|
158
175
|
const parent = parentStoragePaths(cwd, parentSessionId)
|
|
159
176
|
if (!(await isDirectory(parent.parentDirectory))) return
|
|
160
|
-
|
|
177
|
+
leases.push(await acquireParentLease(cwd, parentSessionId))
|
|
161
178
|
const errors: unknown[] = []
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
await stopTask(paths)
|
|
169
|
-
} catch (error) {
|
|
170
|
-
errors.push(error)
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
if (loaded.metadata.kind === "agent") await stopPartition(cwd, loaded.metadata.childSessionId, visited)
|
|
174
|
-
} catch (error) {
|
|
175
|
-
errors.push(error)
|
|
180
|
+
for (const paths of await directTaskPaths(cwd, parentSessionId)) {
|
|
181
|
+
const loaded = await readTaskMetadata(paths)
|
|
182
|
+
if (loaded.status !== "ok") {
|
|
183
|
+
if (getAgentCoordinator().getResident(paths.taskDirectory)) {
|
|
184
|
+
errors.push(new Error(`Cannot validate live task ownership: ${paths.metadata}`))
|
|
176
185
|
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await stopTask(paths)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
errors.push(error)
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
if (loaded.metadata.kind === "agent") await stopPartition(cwd, loaded.metadata.childSessionId, visited, leases)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
errors.push(error)
|
|
177
197
|
}
|
|
178
|
-
} finally {
|
|
179
|
-
await releaseParentLease(lease)
|
|
180
198
|
}
|
|
181
199
|
if (errors.length > 0) throw new AggregateError(errors, `Failed to stop ${errors.length} owned task operation(s)`)
|
|
182
200
|
}
|
|
@@ -188,13 +206,19 @@ async function recoverPartition(cwd: string, parentSessionId: string, visited: S
|
|
|
188
206
|
if (!(await isDirectory(parent.parentDirectory))) return
|
|
189
207
|
|
|
190
208
|
for (const paths of await directTaskPaths(cwd, parentSessionId)) {
|
|
209
|
+
try {
|
|
210
|
+
if (await readTaskDiscardMarker(paths)) continue
|
|
211
|
+
} catch (error) {
|
|
212
|
+
result.diagnostics.push(`${paths.taskDirectory}: ${errorMessage(error)}`)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
191
215
|
const loaded = await readTaskMetadata(paths)
|
|
192
216
|
if (loaded.status !== "ok") {
|
|
193
217
|
if (loaded.status === "invalid") result.diagnostics.push(`${paths.taskDirectory}: ${loaded.diagnostic.message}`)
|
|
194
218
|
continue
|
|
195
219
|
}
|
|
196
|
-
if (loaded.metadata.kind !== "agent") continue
|
|
197
|
-
if (loaded.metadata.
|
|
220
|
+
if (loaded.metadata.discardedAt !== null || loaded.metadata.kind !== "agent") continue
|
|
221
|
+
if (loaded.metadata.state === "suspended" && loaded.metadata.activeRun?.background) {
|
|
198
222
|
const resident = getAgentCoordinator().getResident(paths.taskDirectory)
|
|
199
223
|
if (!resident?.recover) result.diagnostics.push(`${paths.taskDirectory}: suspended task has no recoverable resident`)
|
|
200
224
|
else if (await resident.recover()) result.resumed++
|
|
@@ -223,9 +247,16 @@ async function settleStopped(paths: TaskStoragePaths): Promise<void> {
|
|
|
223
247
|
}
|
|
224
248
|
}
|
|
225
249
|
|
|
226
|
-
async function directTaskPaths(cwd: string, parentSessionId: string): Promise<TaskStoragePaths[]> {
|
|
250
|
+
async function directTaskPaths(cwd: string, parentSessionId: string, strict = false): Promise<TaskStoragePaths[]> {
|
|
227
251
|
const parent = parentStoragePaths(cwd, parentSessionId)
|
|
228
252
|
const entries = await readdir(parent.parentDirectory, { withFileTypes: true })
|
|
253
|
+
if (strict) {
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
if (TASK_REFERENCE_PATTERN.test(entry.name) && (!entry.isDirectory() || entry.isSymbolicLink())) {
|
|
256
|
+
throw new Error(`Unsafe descendant task path: ${parent.parentDirectory}/${entry.name}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
229
260
|
return entries
|
|
230
261
|
.filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && TASK_REFERENCE_PATTERN.test(entry.name))
|
|
231
262
|
.map(entry => taskStoragePaths(parent, entry.name))
|
|
@@ -276,7 +276,7 @@ async function manageTask(ctx: ExtensionContext, id: string, options: Management
|
|
|
276
276
|
{ value: "steer", label: "Steer", description: "Redirect running work; otherwise becomes a Follow-up" }
|
|
277
277
|
]),
|
|
278
278
|
{ value: "stop", label: "Stop", description: "Stop work and preserve retained files" },
|
|
279
|
-
{ value: "discard", label: "Discard", description: "Stop and
|
|
279
|
+
{ value: "discard", label: "Discard", description: "Stop and remove from active work; keep files in place" }
|
|
280
280
|
])
|
|
281
281
|
if (!choice) return
|
|
282
282
|
if (choice === "details") await showText(ctx, task.label, renderTask(task))
|
|
@@ -302,7 +302,9 @@ async function manageTask(ctx: ExtensionContext, id: string, options: Management
|
|
|
302
302
|
) {
|
|
303
303
|
await options.controlTask(task.id, "stop")
|
|
304
304
|
}
|
|
305
|
-
} else if (
|
|
305
|
+
} else if (
|
|
306
|
+
await ctx.ui.confirm(`Discard ${task.id}?`, "Files stay at their original paths. Results remain readable; further input is rejected.")
|
|
307
|
+
) {
|
|
306
308
|
await options.controlTask(task.id, "discard")
|
|
307
309
|
return
|
|
308
310
|
}
|
|
@@ -344,6 +346,10 @@ async function showLiveTaskOutput(ctx: ExtensionContext, task: TaskListRow): Pro
|
|
|
344
346
|
let closed = false
|
|
345
347
|
let loading = false
|
|
346
348
|
let refreshAgain = false
|
|
349
|
+
let offset = 0
|
|
350
|
+
let pageSize = 1
|
|
351
|
+
let total = 0
|
|
352
|
+
let follow = task.kind === "bash"
|
|
347
353
|
const refresh = async () => {
|
|
348
354
|
if (closed) return
|
|
349
355
|
refreshAgain = true
|
|
@@ -364,26 +370,58 @@ async function showLiveTaskOutput(ctx: ExtensionContext, task: TaskListRow): Pro
|
|
|
364
370
|
const unbind = bindTaskUpdateRoute(ctx.cwd, ctx.sessionManager.getSessionId(), refresh)
|
|
365
371
|
return {
|
|
366
372
|
render(width: number) {
|
|
373
|
+
const exitStatus =
|
|
374
|
+
output.exitCode !== undefined ? ` · Exit code: ${output.exitCode ?? "unknown"} · Signal: ${output.signal ?? "none"}` : ""
|
|
367
375
|
const heading = theme.fg(
|
|
368
376
|
"accent",
|
|
369
377
|
theme.bold(
|
|
370
|
-
`${task.id} · ${output.state}${output.latestOutcome ? `/${output.latestOutcome}` : ""}${output.streaming ? " · streaming" : ""}`
|
|
378
|
+
`${task.id} · ${output.state}${output.latestOutcome ? `/${output.latestOutcome}` : ""}${output.streaming ? " · streaming" : ""}${exitStatus}`
|
|
371
379
|
)
|
|
372
380
|
)
|
|
373
|
-
const
|
|
381
|
+
const progressLines = [
|
|
374
382
|
`Capacity: ${output.capacity.active}/${output.capacity.limit} execution permits`,
|
|
383
|
+
...(output.progress ? [`Progress: ${JSON.stringify(output.progress)}`] : []),
|
|
375
384
|
...(output.queueReason ? [`Waiting: ${output.queueReason}`] : []),
|
|
376
385
|
...(output.lastActivity ? [`${output.lastActivity.action} · ${relativeTime(output.lastActivity.at, Date.now())}`] : [])
|
|
377
|
-
]
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
]
|
|
387
|
+
const bodyLines = new Text(output.text || "(no output)", 0, 0).render(width)
|
|
388
|
+
total = bodyLines.length
|
|
389
|
+
pageSize = Math.max(1, Math.floor(tui.terminal.rows * 0.6) - (1 + progressLines.length + 2))
|
|
390
|
+
const maxOffset = Math.max(0, total - pageSize)
|
|
391
|
+
if (follow) offset = maxOffset
|
|
392
|
+
else offset = Math.max(0, Math.min(offset, maxOffset))
|
|
393
|
+
const position = `${Math.min(offset + 1, total)}-${Math.min(offset + pageSize, total)}/${total}${follow ? " · follow" : ""}`
|
|
394
|
+
return [
|
|
395
|
+
truncateToWidth(heading, width),
|
|
396
|
+
...progressLines.map(line => truncateToWidth(line, width)),
|
|
397
|
+
"",
|
|
398
|
+
...bodyLines.slice(offset, offset + pageSize).map(line => truncateToWidth(line, width)),
|
|
399
|
+
truncateToWidth(theme.fg("dim", `Enter/Esc back · ↑↓ PgUp/PgDn Home/End · ${position}`), width)
|
|
400
|
+
]
|
|
383
401
|
},
|
|
384
402
|
invalidate() {},
|
|
385
403
|
handleInput(data: string) {
|
|
386
404
|
if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) done(undefined)
|
|
405
|
+
else if (matchesKey(data, Key.up)) {
|
|
406
|
+
offset--
|
|
407
|
+
follow = false
|
|
408
|
+
} else if (matchesKey(data, Key.down)) {
|
|
409
|
+
offset++
|
|
410
|
+
} else if (matchesKey(data, Key.pageUp)) {
|
|
411
|
+
offset -= pageSize
|
|
412
|
+
follow = false
|
|
413
|
+
} else if (matchesKey(data, Key.pageDown)) {
|
|
414
|
+
offset += pageSize
|
|
415
|
+
} else if (matchesKey(data, Key.home)) {
|
|
416
|
+
offset = 0
|
|
417
|
+
follow = false
|
|
418
|
+
} else if (matchesKey(data, Key.end)) {
|
|
419
|
+
offset = Math.max(0, total - pageSize)
|
|
420
|
+
follow = true
|
|
421
|
+
} else return
|
|
422
|
+
offset = Math.max(0, Math.min(offset, total - pageSize))
|
|
423
|
+
if (offset >= Math.max(0, total - pageSize)) follow = true
|
|
424
|
+
tui.requestRender()
|
|
387
425
|
},
|
|
388
426
|
dispose() {
|
|
389
427
|
closed = true
|
|
@@ -610,6 +648,7 @@ function renderTask(task: TaskListRow): string {
|
|
|
610
648
|
`Task: ${task.id}`,
|
|
611
649
|
`State: ${task.state}`,
|
|
612
650
|
`Outcome: ${task.latestOutcome ?? "none"}`,
|
|
651
|
+
...(task.progress ? [`Progress: ${JSON.stringify(task.progress)}`] : []),
|
|
613
652
|
...(task.kind === "bash"
|
|
614
653
|
? [
|
|
615
654
|
`Command: ${task.command}`,
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
TASK_REFERENCE_PATTERN,
|
|
13
13
|
type TaskMetadata,
|
|
14
14
|
type TaskStoragePaths,
|
|
15
|
-
taskStoragePaths
|
|
15
|
+
taskStoragePaths,
|
|
16
|
+
truncateUtf8Tail
|
|
16
17
|
} from "./state.js"
|
|
18
|
+
import { readTaskDiscardMarker } from "./storage.js"
|
|
17
19
|
import { loadTaskList } from "./tools.js"
|
|
18
20
|
|
|
19
21
|
export const NOTIFICATION_CUSTOM_TYPE = "lovely-agents:notification"
|
|
@@ -46,7 +48,10 @@ export async function prepareTaskNotification(
|
|
|
46
48
|
type: NotificationType,
|
|
47
49
|
outcome?: TaskMetadata["latestOutcome"]
|
|
48
50
|
): Promise<TaskNotification> {
|
|
49
|
-
const output =
|
|
51
|
+
const output =
|
|
52
|
+
metadata.kind === "bash"
|
|
53
|
+
? truncateUtf8Tail(metadata.latestReply?.text ?? "", NOTIFICATION_OUTPUT_PREVIEW_BYTES)
|
|
54
|
+
: truncateUtf8(metadata.latestReply?.text ?? "", NOTIFICATION_OUTPUT_PREVIEW_BYTES)
|
|
50
55
|
const taskList = await loadTaskList(paths.workspace, metadata.parentSessionId).catch(() => undefined)
|
|
51
56
|
const descendants = taskList?.details.tasks.find(task => task.id === metadata.taskRef)?.descendants
|
|
52
57
|
const descendantText = descendants && descendants.total > 0 ? `\nDescendants: ${JSON.stringify(descendants)}` : ""
|
|
@@ -55,11 +60,12 @@ export async function prepareTaskNotification(
|
|
|
55
60
|
const content = truncateUtf8(
|
|
56
61
|
[
|
|
57
62
|
`[Lovely ${metadata.kind === "bash" ? "Bash" : "Agent"} ${metadata.taskRef}:${run.id}:${type}]`,
|
|
58
|
-
`Task ${metadata.taskRef} ${JSON.stringify(metadata.label)} ${status}`,
|
|
63
|
+
`Task ${metadata.taskRef} run=${run.sequence} ${JSON.stringify(metadata.label)} ${status}`,
|
|
59
64
|
metadata.kind === "bash"
|
|
60
65
|
? `Command: ${truncateUtf8(metadata.command, 1024)}\nExit: ${metadata.exitCode ?? "unknown"}${metadata.signal ? ` signal=${metadata.signal}` : ""}`
|
|
61
66
|
: `Model: ${metadata.model.provider}/${metadata.model.id}:${metadata.thinking}`,
|
|
62
|
-
|
|
67
|
+
`Read: task_output(id: "${metadata.taskRef}", run: ${run.sequence})`,
|
|
68
|
+
output ? `Output preview:\n${output}` : "Output preview: (empty)",
|
|
63
69
|
`Files: history=${pathsForDisplay.history} ${metadata.kind === "bash" ? `output=${pathsForDisplay.output}` : `session=${pathsForDisplay.session}`}${descendantText}`
|
|
64
70
|
].join("\n"),
|
|
65
71
|
MAX_NOTIFICATION_CONTENT_BYTES
|
|
@@ -85,7 +91,8 @@ export function appendTaskNotification(notifications: TaskNotification[], notifi
|
|
|
85
91
|
/** Sends every pending notification for one task, without marking delivery. */
|
|
86
92
|
export async function deliverTaskNotifications(paths: TaskStoragePaths): Promise<number> {
|
|
87
93
|
const loaded = await readTaskMetadata(paths)
|
|
88
|
-
|
|
94
|
+
// Discard leaves result paths intact; accepted notices still reconcile normally.
|
|
95
|
+
if (loaded.status !== "ok") return 0
|
|
89
96
|
const routeKey = notificationRouteKey(paths.workspace, loaded.metadata.parentSessionId)
|
|
90
97
|
const route = getAgentCoordinator().getNotificationRoute(routeKey)
|
|
91
98
|
if (!route) return 0
|
|
@@ -118,6 +125,12 @@ export async function reconcileParentNotifications(
|
|
|
118
125
|
for (const paths of await directTaskPaths(cwd, parentSessionId)) {
|
|
119
126
|
const loaded = await readTaskMetadata(paths)
|
|
120
127
|
if (loaded.status !== "ok") {
|
|
128
|
+
try {
|
|
129
|
+
if (await readTaskDiscardMarker(paths)) continue
|
|
130
|
+
} catch (error) {
|
|
131
|
+
result.diagnostics.push(`${paths.taskDirectory}: ${errorMessage(error)}`)
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
121
134
|
if (loaded.status === "invalid") result.diagnostics.push(`${paths.taskDirectory}: ${loaded.diagnostic.message}`)
|
|
122
135
|
continue
|
|
123
136
|
}
|