pi-better-background-tasks 0.2.20 → 0.4.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.
- package/README.md +46 -2
- package/package.json +1 -1
- package/src/failures.ts +101 -6
- package/src/index.ts +5 -0
- package/src/logs.ts +51 -7
- package/src/navigator-provider.ts +4 -3
- package/src/output.ts +783 -0
- package/src/process.ts +95 -8
- package/src/registry.ts +84 -7
- package/src/runtime.ts +228 -33
- package/src/sandbox.ts +6 -4
- package/src/shared-callback-batcher.ts +358 -29
- package/src/shared-failure-observations.ts +833 -23
- package/src/shared-log-utils.ts +1139 -2
- package/src/shared-navigator.ts +12 -10
- package/src/shared-sandbox-core.ts +660 -17
- package/src/tools.ts +201 -156
- package/src/types.ts +23 -0
package/README.md
CHANGED
|
@@ -39,6 +39,14 @@ If the foreground sandbox reports `unavailable` or `failed`, a local launch is
|
|
|
39
39
|
refused with an explanation instead of running unconfined. `/sandbox off` is the
|
|
40
40
|
deliberate way to run local tasks unsandboxed.
|
|
41
41
|
|
|
42
|
+
A task follows Main's permission profile. With Main's Outside project = Write, the
|
|
43
|
+
task writes across home and temp but can remove files outside the project only in
|
|
44
|
+
temp, hidden home directories, and worktree folders (see
|
|
45
|
+
[pi-better-sandbox](https://github.com/1aboveio/pi-better-harness/tree/main/packages/pi-better-sandbox#readme)).
|
|
46
|
+
On macOS, a task launched with Outside project = Write or Write & delete starts an
|
|
47
|
+
APFS local snapshot (`tmutil localsnapshot`) in the background as a recovery aid.
|
|
48
|
+
A failed snapshot is noted in the task log and never blocks the task.
|
|
49
|
+
|
|
42
50
|
Structured remote SSH tasks are unaffected: the foreground sandbox describes this
|
|
43
51
|
machine, and remote work keeps its existing remote semantics. Without
|
|
44
52
|
`pi-better-sandbox` installed, local tasks behave exactly as they always have.
|
|
@@ -80,6 +88,21 @@ timeout can terminate the local SSH client but the remote process may still be
|
|
|
80
88
|
running. See the detailed usage notes for bootstrap policy, watch conditions,
|
|
81
89
|
timeouts, and v1 non-goals.
|
|
82
90
|
|
|
91
|
+
## Reloads and session switches
|
|
92
|
+
|
|
93
|
+
Tasks belong to the Pi session that started them. After `/reload`, the same
|
|
94
|
+
session picks every task back up: watches keep polling, remote tmux output keeps
|
|
95
|
+
being collected, and a task that finished during the reload (or exits later) gets
|
|
96
|
+
its completion callback exactly once.
|
|
97
|
+
|
|
98
|
+
After `/new`, `/resume`, fork, or switching to another session, the previous
|
|
99
|
+
session's tasks keep running but are paused from Pi's side: watches do not poll,
|
|
100
|
+
remote tmux output is not collected, and `timeout_seconds` deadlines are not
|
|
101
|
+
enforced until that session is active again. An overdue deadline is enforced as
|
|
102
|
+
soon as the session resumes, so a timeout can land late but is never skipped. A
|
|
103
|
+
local process that exits in the meantime is recorded as finished, and its
|
|
104
|
+
callback is delivered when its session resumes.
|
|
105
|
+
|
|
83
106
|
## Watch conditions
|
|
84
107
|
|
|
85
108
|
JSON conditions require a root-prefixed path, such as `$.status` or
|
|
@@ -125,13 +148,34 @@ an explicitly configured nonzero success exit is treated as expected. Verbose
|
|
|
125
148
|
status includes observation details and the journal path. Corrupt or unreadable
|
|
126
149
|
evidence is reported as **observation incomplete**.
|
|
127
150
|
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
Pass `operation_id` and `expected_exit_codes` on `bg_task_spawn`, `bg_task_watch`,
|
|
152
|
+
or `bg_task` to declare intent before launch; a malformed declaration starts
|
|
153
|
+
nothing. An exit code in `expected_exit_codes` (distinct integers 1-255, such as
|
|
154
|
+
`[1]` for a no-match probe) is recorded as an **Expected failure** that needs no
|
|
155
|
+
action; signals and timeouts never are. When a task with an `operation_id`
|
|
156
|
+
succeeds, earlier failed tasks with the same `operation_id`, kind, cwd, SSH
|
|
157
|
+
target, and owner (the same session id, or for sessionless tasks the same Pi
|
|
158
|
+
process) recover, so a retry with a changed command or timeout
|
|
159
|
+
closes the original incident. Observation gaps are never recovered this way, and
|
|
160
|
+
no command text is compared.
|
|
161
|
+
|
|
162
|
+
Task failures are labeled **Action required**; the shared labels also include
|
|
163
|
+
**Expected failure** and **Observation incomplete**. Unresolved running failures
|
|
164
|
+
become eligible for attention after 60 seconds; observation gaps are eligible
|
|
165
|
+
immediately. Each notification lists only the incidents it is delivering;
|
|
166
|
+
earlier ones are counted, not repeated. Terminal failures use the normal
|
|
130
167
|
completion notification. A delivery receipt is stored only after handoff;
|
|
131
168
|
notification delivery does not clear the failure. `callback:false` stays quiet
|
|
132
169
|
while all inspection surfaces retain the evidence. Journals follow the task's
|
|
133
170
|
existing retention and explicit-clear behavior.
|
|
134
171
|
|
|
172
|
+
Status, log, and list count and list only failures that need action (**Action
|
|
173
|
+
required** and **Observation incomplete**). Expected and closed failures are one
|
|
174
|
+
history count line with no incident cursor; pass `history: true` to
|
|
175
|
+
`bg_task_status` (or `action:status`) to page them. Incident rows are compact
|
|
176
|
+
(120-byte excerpt, evidence such as `output.log#poll=3`); the raw log keeps the
|
|
177
|
+
full evidence path.
|
|
178
|
+
|
|
135
179
|
## When To Use
|
|
136
180
|
|
|
137
181
|
Use this package for shell commands that need logs, status, cancellation, or completion notifications across a Pi turn.
|
package/package.json
CHANGED
package/src/failures.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
|
-
failureAttentionHandled, failureIdentity, formatFailureSummary, markFailureAttentionDelivered,
|
|
5
|
-
observeFailures, pendingFailureAttention, readFailureState,
|
|
4
|
+
activeFailures, failureAttentionHandled, failureIdentity, formatFailureSummary, markFailureAttentionDelivered,
|
|
5
|
+
observeFailures, pendingAttentionNote, pendingAttentionRows, pendingFailureAttention, readCommandIntent, readFailureState,
|
|
6
|
+
type FailureEvent, type FailureState,
|
|
6
7
|
} from "./shared-failure-observations.js";
|
|
7
8
|
import { getCallbackBatcher } from "./shared-callback-batcher.js";
|
|
8
|
-
import { readMeta, taskDir } from "./registry.js";
|
|
9
|
+
import { listMetasForOrigin, originOf, readMeta, taskDir } from "./registry.js";
|
|
9
10
|
import type { ActiveSessionProvider } from "./runtime.js";
|
|
10
11
|
import type { BackgroundTaskMeta } from "./types.js";
|
|
11
12
|
|
|
@@ -32,16 +33,112 @@ export function recoverFailure(meta: BackgroundTaskMeta, operation: string, even
|
|
|
32
33
|
}]);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Validate `operation_id` / `expected_exit_codes` before anything is launched (#325), with the same
|
|
38
|
+
* validator the subagent task runtime's bash uses. Background commands are launched by the parent
|
|
39
|
+
* agent itself, so the declaration is trusted; a malformed one refuses the launch.
|
|
40
|
+
*/
|
|
41
|
+
export function readTaskIntent(params: { operation_id?: unknown; expected_exit_codes?: unknown }): Pick<BackgroundTaskMeta, "operationId" | "expectedExitCodes"> {
|
|
42
|
+
const { intent, error } = readCommandIntent({ operationId: params.operation_id, expectedExitCodes: params.expected_exit_codes },
|
|
43
|
+
{ operationId: "operation_id", expectedExitCodes: "expected_exit_codes" });
|
|
44
|
+
if (error) throw new Error(`Invalid command intent: ${error}. The task was not started.`);
|
|
45
|
+
return { ...(intent.operationId ? { operationId: intent.operationId } : {}), ...(intent.expectedExitCodes ? { expectedExitCodes: intent.expectedExitCodes } : {}) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A structured exit code the caller declared intentional before launch. Signals and timeouts never are. */
|
|
49
|
+
export function isDeclaredExpectedExit(meta: BackgroundTaskMeta, exitCode: number | null | undefined): exitCode is number {
|
|
50
|
+
return typeof exitCode === "number" && exitCode !== 0 && (meta.expectedExitCodes ?? []).includes(exitCode);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Record a non-zero command exit, classified as expected only when its code was declared before launch. */
|
|
54
|
+
export function recordExitFailure(meta: BackgroundTaskMeta, operation: string, summary: string, exitCode: number | null | undefined, eventKey: unknown,
|
|
55
|
+
options: { at?: number; expected?: boolean } = {}): void {
|
|
56
|
+
const declared = isDeclaredExpectedExit(meta, exitCode);
|
|
57
|
+
recordFailure(meta, operation, declared ? `${summary} (declared expected)` : summary, eventKey,
|
|
58
|
+
{ category: "exit", expected: declared || options.expected === true, at: options.at });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function declaredOperation(meta: BackgroundTaskMeta): string | undefined {
|
|
62
|
+
return meta.operationId ? failureIdentity("bg-operation", meta.kind, meta.operationId, meta.cwd, meta.ssh?.target ?? null) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Recovery crosses tasks only within one owner (#325): the same non-empty session id, or, for
|
|
67
|
+
* sessionless tasks, the same spawning process (#312's sessionless ownership rule). The origin
|
|
68
|
+
* index alone treats two sessionless sessions in one cwd as one origin.
|
|
69
|
+
*/
|
|
70
|
+
function sameRecoveryOwner(a: BackgroundTaskMeta, b: BackgroundTaskMeta): boolean {
|
|
71
|
+
const sa = a.callbackOrigin?.sessionId, sb = b.callbackOrigin?.sessionId;
|
|
72
|
+
if (sa || sb) return Boolean(sa) && sa === sb;
|
|
73
|
+
return a.spawnPid === b.spawnPid && a.spawnPidStartTime === b.spawnPidStartTime;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A task that succeeded with a declared `operationId` recovers the unresolved failures of earlier
|
|
78
|
+
* tasks of the same owner (session, or spawning process when sessionless) that declared the same operation (same kind, cwd, and SSH target) and
|
|
79
|
+
* failed before this task started: a modified retry as a new task. Observation gaps and expected
|
|
80
|
+
* failures are left alone; nothing is matched on command text.
|
|
81
|
+
*/
|
|
82
|
+
export function recoverDeclaredOperation(meta: BackgroundTaskMeta, at = Date.now()): void {
|
|
83
|
+
const operation = declaredOperation(meta);
|
|
84
|
+
if (!operation) return;
|
|
85
|
+
let earlier: BackgroundTaskMeta[];
|
|
86
|
+
try { earlier = listMetasForOrigin(originOf(meta)); } catch { return; }
|
|
87
|
+
for (const task of earlier) {
|
|
88
|
+
if (task.id === meta.id || task.startedAt >= meta.startedAt || declaredOperation(task) !== operation || !sameRecoveryOwner(task, meta)) continue;
|
|
89
|
+
const path = failurePath(task.id);
|
|
90
|
+
const events: FailureEvent[] = activeFailures(readFailureState(path))
|
|
91
|
+
.filter((x) => x.status === "unresolved" && x.category !== "observation-incomplete" && x.lastObservedAt <= meta.startedAt)
|
|
92
|
+
.map((x) => ({ id: failureIdentity(task.id, x.id, "recovered-by", meta.id), operation: x.operation, kind: "recovered", incidents: [x.id], at,
|
|
93
|
+
evidence: `task ${meta.id} (operation ${meta.operationId}) succeeded` }));
|
|
94
|
+
if (events.length) observeFailures(path, events);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
const attentionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
99
|
+
let attentionSuspended = false;
|
|
100
|
+
/** Session shutdown: stop this instance's attention timers; the next session_start reschedules (#324). */
|
|
101
|
+
export function suspendFailureAttention(): void {
|
|
102
|
+
attentionSuspended = true;
|
|
103
|
+
for (const timer of attentionTimers.values()) clearTimeout(timer);
|
|
104
|
+
attentionTimers.clear();
|
|
105
|
+
}
|
|
106
|
+
export function resumeFailureAttention(): void {
|
|
107
|
+
attentionSuspended = false;
|
|
108
|
+
}
|
|
36
109
|
export function stopFailureAttention(id: string): void {
|
|
37
110
|
const timer = attentionTimers.get(id);
|
|
38
111
|
if (timer) clearTimeout(timer);
|
|
39
112
|
attentionTimers.delete(id);
|
|
40
113
|
}
|
|
41
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Model-facing fields of a running task's failure attention. The notification
|
|
117
|
+
* identity is per incident set; the inspect target is the real task id.
|
|
118
|
+
* Only the pending incidents are rows; earlier deliveries are counted, never repeated (#315).
|
|
119
|
+
*/
|
|
120
|
+
export function failureAttentionFields(meta: BackgroundTaskMeta, state: FailureState, pending: { key: string; incidents: string[] }) {
|
|
121
|
+
const rows = pendingAttentionRows(state, pending.incidents);
|
|
122
|
+
const note = pendingAttentionNote(state, pending.incidents);
|
|
123
|
+
const due = pending.incidents.length;
|
|
124
|
+
return {
|
|
125
|
+
source: "background-task" as const,
|
|
126
|
+
id: `failure:${meta.id}:${pending.key}`,
|
|
127
|
+
inspectId: meta.id,
|
|
128
|
+
label: meta.name ?? meta.id,
|
|
129
|
+
status: "failure",
|
|
130
|
+
customType: "background-task-failure",
|
|
131
|
+
content: `Background task ${meta.id} is still running with ${due} failure observation${due === 1 ? "" : "s"} that need attention.${note ? ` ${note}` : ""}`,
|
|
132
|
+
detailTool: "bg_task_status" as const,
|
|
133
|
+
failureRows: rows,
|
|
134
|
+
incidentCount: rows.length || undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
42
138
|
/** Running incidents get one grace wake. Terminal incidents ride the completion callback. */
|
|
43
139
|
export function scheduleFailureAttention(pi: ExtensionAPI, id: string, getActiveSession?: ActiveSessionProvider): void {
|
|
44
140
|
stopFailureAttention(id);
|
|
141
|
+
if (attentionSuspended) return;
|
|
45
142
|
const meta = readMeta(id);
|
|
46
143
|
if (!meta) {
|
|
47
144
|
const timer = setTimeout(() => scheduleFailureAttention(pi, id, getActiveSession), 1_000);
|
|
@@ -54,9 +151,7 @@ export function scheduleFailureAttention(pi: ExtensionAPI, id: string, getActive
|
|
|
54
151
|
const pending = pendingFailureAttention(state, Date.now());
|
|
55
152
|
if (pending) {
|
|
56
153
|
const delivery = getCallbackBatcher(pi).deliverUrgent({
|
|
57
|
-
|
|
58
|
-
status: "failure", customType: "background-task-failure",
|
|
59
|
-
content: `Background task ${id}: ${pending.summary}\nInspect: bg_task_status id=${id}`,
|
|
154
|
+
...failureAttentionFields(meta, state, pending),
|
|
60
155
|
isDelivered: () => {
|
|
61
156
|
const current = readMeta(id);
|
|
62
157
|
if (!current) throw new Error("Task metadata is unavailable; defer failure notification");
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { disposeBackgroundWorkNavigator } from "./shared-navigator.ts";
|
|
3
3
|
import { registerBackgroundTasksGoalProvider } from "./goal-provider.js";
|
|
4
4
|
import { clearBackgroundTasksNavigatorSession, ensureBackgroundTasksNavigator, ensureBackgroundTasksNavigatorProvider } from "./navigator-provider.js";
|
|
5
|
+
import { resumeScheduledWork, suspendScheduledWork } from "./runtime.js";
|
|
5
6
|
import { observeForegroundSandboxPolicy } from "./sandbox.js";
|
|
6
7
|
import { registerTools } from "./tools.js";
|
|
7
8
|
|
|
@@ -11,7 +12,9 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
11
12
|
// Subscribed at load so a sandbox extension that publishes later is heard, and
|
|
12
13
|
// asked for a snapshot in case one published before this extension existed.
|
|
13
14
|
observeForegroundSandboxPolicy(pi);
|
|
15
|
+
// Registered before registerTools(pi), so this runs before running tasks are resumed.
|
|
14
16
|
pi.on("session_start", async (_event, ctx) => {
|
|
17
|
+
resumeScheduledWork();
|
|
15
18
|
registerBackgroundTasksGoalProvider(pi);
|
|
16
19
|
ensureBackgroundTasksNavigator(ctx);
|
|
17
20
|
});
|
|
@@ -20,6 +23,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
20
23
|
disposeBackgroundWorkNavigator();
|
|
21
24
|
});
|
|
22
25
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
26
|
+
// /reload, session replacement, and quit load a fresh instance; stop this one's timers (#324).
|
|
27
|
+
suspendScheduledWork();
|
|
23
28
|
clearBackgroundTasksNavigatorSession();
|
|
24
29
|
disposeBackgroundWorkNavigator(ctx);
|
|
25
30
|
});
|
package/src/logs.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { appendFileSync, closeSync, openSync, readSync, statSync, truncateSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { mkdirSync } from "node:fs";
|
|
4
|
-
import { readBoundedTail, tailTerminalDisplay, terminalDisplayRows } from "./shared-log-utils.js";
|
|
5
|
-
import type {
|
|
4
|
+
import { pageRetainedFile, readBoundedTail, tailTerminalDisplay, terminalDisplayRows } from "./shared-log-utils.js";
|
|
5
|
+
import type { FilePageRequest, PageResult } from "./shared-log-utils.js";
|
|
6
|
+
import type { BackgroundTaskMeta, CommandResult } from "./types.js";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_MAX_LOG_BYTES = 4 * 1024 * 1024;
|
|
8
9
|
export const MAX_LOG_TAIL_READ_BYTES = 512 * 1024;
|
|
@@ -10,7 +11,10 @@ const RETAINED_LOG_FRACTION = 0.75;
|
|
|
10
11
|
|
|
11
12
|
export function appendWatchResult(logPath: string, result: CommandResult): void {
|
|
12
13
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
13
|
-
const
|
|
14
|
+
const capture = result.captureTruncated
|
|
15
|
+
? ` capture_discarded_stdout=${result.stdoutDiscardedBytes ?? 0} capture_discarded_stderr=${result.stderrDiscardedBytes ?? 0}`
|
|
16
|
+
: "";
|
|
17
|
+
const header = `\n--- check ${new Date(result.startedAt).toISOString()} exit=${result.exitCode ?? "null"} signal=${result.signal ?? "null"} duration_ms=${result.endedAt - result.startedAt}${capture} ---\n`;
|
|
14
18
|
appendFileSync(logPath, header);
|
|
15
19
|
if (result.stdout) appendFileSync(logPath, result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
16
20
|
if (result.stderr) appendFileSync(logPath, `[stderr]\n${result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`}`);
|
|
@@ -27,18 +31,58 @@ export function appendTaskOutput(logPath: string, output: string): void {
|
|
|
27
31
|
appendFileSync(logPath, output);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
export
|
|
34
|
+
export interface LogRead {
|
|
35
|
+
text: string;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
totalBytes?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
/** Display rows before the returned tail within the bounded read window. */
|
|
40
|
+
omittedRows?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readLog(logPath: string, tailLines?: number): LogRead {
|
|
31
44
|
const requestedRows = tailLines && tailLines > 0 ? Math.floor(tailLines) : undefined;
|
|
32
45
|
const tail = readBoundedTail(logPath, MAX_LOG_TAIL_READ_BYTES);
|
|
33
|
-
if (
|
|
34
|
-
|
|
46
|
+
if (tail.error) {
|
|
47
|
+
return { text: "", truncated: tail.truncated, totalBytes: tail.totalBytes, error: tail.error };
|
|
48
|
+
}
|
|
49
|
+
if (!tail.text) return { text: "", truncated: false, totalBytes: tail.totalBytes };
|
|
50
|
+
if (!requestedRows) return { text: tail.text, truncated: tail.truncated, totalBytes: tail.totalBytes };
|
|
35
51
|
const rows = terminalDisplayRows(tail.text);
|
|
52
|
+
const omittedRows = Math.max(0, rows.length - requestedRows);
|
|
36
53
|
return {
|
|
37
54
|
text: tailTerminalDisplay(tail.text, requestedRows),
|
|
38
|
-
truncated: tail.truncated ||
|
|
55
|
+
truncated: tail.truncated || omittedRows > 0,
|
|
56
|
+
totalBytes: tail.totalBytes,
|
|
57
|
+
...(omittedRows > 0 ? { omittedRows } : {}),
|
|
39
58
|
};
|
|
40
59
|
}
|
|
41
60
|
|
|
61
|
+
export function captureGapsFor(meta: BackgroundTaskMeta): Array<{ bytes: number; detail?: string }> {
|
|
62
|
+
const gaps: Array<{ bytes: number; detail?: string }> = [];
|
|
63
|
+
if (meta.stdoutDiscardedBytes) {
|
|
64
|
+
gaps.push({ bytes: meta.stdoutDiscardedBytes, detail: "stdout capture overflow" });
|
|
65
|
+
}
|
|
66
|
+
if (meta.stderrDiscardedBytes) {
|
|
67
|
+
gaps.push({ bytes: meta.stderrDiscardedBytes, detail: "stderr capture overflow" });
|
|
68
|
+
}
|
|
69
|
+
if (!gaps.length && meta.captureDiscardedBytes) {
|
|
70
|
+
gaps.push({ bytes: meta.captureDiscardedBytes, detail: `${meta.captureOverflowEvents ?? 1} capture overflow(s)` });
|
|
71
|
+
}
|
|
72
|
+
return gaps;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Page retained raw bytes with this task's generation and disclosed capture/retention loss. */
|
|
76
|
+
export function pageTaskLog(meta: BackgroundTaskMeta, request: FilePageRequest = {}): PageResult {
|
|
77
|
+
return pageRetainedFile(meta.logPath, {
|
|
78
|
+
...request,
|
|
79
|
+
resource: request.resource ?? meta.id,
|
|
80
|
+
generation: request.generation ?? meta.logGeneration ?? 0,
|
|
81
|
+
discardedBytes: request.discardedBytes ?? meta.logDiscardedBytes,
|
|
82
|
+
captureGaps: request.captureGaps ?? captureGapsFor(meta),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
42
86
|
export function resolveMaxLogBytes(value: number | undefined): number {
|
|
43
87
|
if (value === undefined || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_LOG_BYTES;
|
|
44
88
|
return Math.max(64 * 1024, Math.floor(value));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_LOG_TAIL_ROWS as NAVIGATOR_DETAIL_ROWS,
|
|
3
4
|
ensureBackgroundWorkNavigator,
|
|
4
5
|
refreshBackgroundWorkNavigator,
|
|
5
6
|
registerBackgroundWorkProvider,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
} from "./shared-navigator.ts";
|
|
10
11
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
12
|
-
import { activeFailures, readFailureState } from "./shared-failure-observations.js";
|
|
13
|
+
import { activeFailures, failureLabel, readFailureState } from "./shared-failure-observations.js";
|
|
13
14
|
import { failurePath, failureSummary } from "./failures.js";
|
|
14
15
|
import { readLog } from "./logs.js";
|
|
15
16
|
import { listMetasForOrigin, onMetaChanged, readMeta, writeMeta } from "./registry.js";
|
|
@@ -129,7 +130,7 @@ function rowFromMeta(meta: BackgroundTaskMeta, now: number): BackgroundWorkRow {
|
|
|
129
130
|
function detailFromMeta(meta: BackgroundTaskMeta | undefined, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null {
|
|
130
131
|
if (!meta) return null;
|
|
131
132
|
const failure = failureSummary(meta.id);
|
|
132
|
-
const log = readLog(meta.logPath, options?.logTailLines ??
|
|
133
|
+
const log = readLog(meta.logPath, options?.logTailLines ?? NAVIGATOR_DETAIL_ROWS);
|
|
133
134
|
const command = commandLabel(meta);
|
|
134
135
|
const metadata = [
|
|
135
136
|
{ label: "provider", value: "Background Tasks" },
|
|
@@ -217,7 +218,7 @@ function secondaryLabel(meta: BackgroundTaskMeta): string | undefined {
|
|
|
217
218
|
function factsForMeta(meta: BackgroundTaskMeta, now: number): string[] {
|
|
218
219
|
const facts: string[] = [];
|
|
219
220
|
const incident = activeFailures(readFailureState(failurePath(meta.id)))[0];
|
|
220
|
-
if (incident) facts.push(`${incident
|
|
221
|
+
if (incident) facts.push(`${failureLabel(incident)}: ${incident.summary}`);
|
|
221
222
|
if (meta.status === "running") {
|
|
222
223
|
const stall = observeBackgroundTaskStall(meta, now);
|
|
223
224
|
if (stall.state === "stalled") facts.push("stalled");
|