@alisio/core 0.1.0-alpha.8 → 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.
- package/README.md +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +118 -3
- package/dist/application.js +510 -19
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +293 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +252 -5
- package/dist/config.js +514 -13
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +152 -4
- package/dist/core/runner.js +792 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +51 -6
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +39 -1
- package/dist/plugins/host.js +150 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.d.ts +5 -0
- package/dist/resources/context.js +13 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/startup/screen.js +6 -5
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const STATUS = {
|
|
2
|
+
queued: "queued",
|
|
3
|
+
running: "running",
|
|
4
|
+
waiting: "running",
|
|
5
|
+
completed: "succeeded",
|
|
6
|
+
failed: "failed",
|
|
7
|
+
cancelled: "cancelled",
|
|
8
|
+
interrupted: "lost",
|
|
9
|
+
};
|
|
10
|
+
/** Maps one panel node to a task (an unknown status is shown as `lost`, never as running). */
|
|
11
|
+
export function nodeToTask(node, rootSession) {
|
|
12
|
+
return {
|
|
13
|
+
id: node.id,
|
|
14
|
+
kind: "subagent",
|
|
15
|
+
label: node.label,
|
|
16
|
+
status: STATUS[node.status] ?? "lost",
|
|
17
|
+
...(node.detail ? { command: node.detail } : {}),
|
|
18
|
+
sessionId: node.sessionId ?? rootSession,
|
|
19
|
+
...(node.parentId ? { parentId: node.parentId } : {}),
|
|
20
|
+
bytes: 0,
|
|
21
|
+
createdAt: node.startedAt ?? 0,
|
|
22
|
+
...(node.startedAt ? { startedAt: node.startedAt } : {}),
|
|
23
|
+
...(node.endedAt ? { endedAt: node.endedAt } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The subagent tasks of a root session, from the panels the plugin registered. */
|
|
27
|
+
export function subagentTasks(panels, rootSession) {
|
|
28
|
+
const tasks = [];
|
|
29
|
+
for (const entry of panels.values()) {
|
|
30
|
+
if (entry.plugin !== "subagents")
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
for (const node of entry.provider.nodes({ sessionId: rootSession }))
|
|
34
|
+
tasks.push(nodeToTask(node, rootSession));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* a failing panel never breaks the task list */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return tasks;
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { BackgroundTaskStore, TaskRow } from "./store.ts";
|
|
2
|
+
export type WakeOutcome =
|
|
3
|
+
/** A run that carries the notification has started. */
|
|
4
|
+
"started"
|
|
5
|
+
/** The session cannot take a run right now (a run, an approval, another surface): retry. */
|
|
6
|
+
| "busy"
|
|
7
|
+
/** The session can never take it (missing, archived, finished): give up without delivering. */
|
|
8
|
+
| "skip";
|
|
9
|
+
export interface TaskWakeRequest {
|
|
10
|
+
/** The ROOT session to wake. */
|
|
11
|
+
sessionId: string;
|
|
12
|
+
/** The message the run starts from (persisted as the user message of the run). */
|
|
13
|
+
text: string;
|
|
14
|
+
/** What the UIs show instead of `text`. */
|
|
15
|
+
display: string;
|
|
16
|
+
/** Idempotency key of the batch (the host uses it as the run's request id). */
|
|
17
|
+
requestId: string;
|
|
18
|
+
taskIds: string[];
|
|
19
|
+
}
|
|
20
|
+
export type TaskWake = (request: TaskWakeRequest) => WakeOutcome | Promise<WakeOutcome>;
|
|
21
|
+
export interface NotifierTiming {
|
|
22
|
+
batchMs: number;
|
|
23
|
+
minIntervalMs: number;
|
|
24
|
+
retryMs: number;
|
|
25
|
+
burstMax: number;
|
|
26
|
+
burstWindowMs: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const DEFAULT_NOTIFIER_TIMING: NotifierTiming;
|
|
29
|
+
/** The bounded message of one batch: ids, status and how to read the output, nothing else. */
|
|
30
|
+
export declare function buildTaskNotification(rows: TaskRow[]): {
|
|
31
|
+
text: string;
|
|
32
|
+
display: string;
|
|
33
|
+
};
|
|
34
|
+
export declare class TaskNotifier {
|
|
35
|
+
private options;
|
|
36
|
+
private sessions;
|
|
37
|
+
private readonly timing;
|
|
38
|
+
private disposed;
|
|
39
|
+
constructor(options: {
|
|
40
|
+
store: BackgroundTaskStore;
|
|
41
|
+
wake: TaskWake;
|
|
42
|
+
timing?: Partial<NotifierTiming>;
|
|
43
|
+
now?: () => number;
|
|
44
|
+
/** Errors of the wake callback (it never throws into the caller). */
|
|
45
|
+
onError?: (error: unknown) => void;
|
|
46
|
+
});
|
|
47
|
+
private now;
|
|
48
|
+
private state;
|
|
49
|
+
/** Number of finished tasks of a session waiting to be announced. */
|
|
50
|
+
pending(session: string): number;
|
|
51
|
+
/** A task ended and may be announced to its root session. */
|
|
52
|
+
note(row: TaskRow): void;
|
|
53
|
+
/** The host says the session is idle now: try again without waiting for the retry timer. */
|
|
54
|
+
poke(session: string): void;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
private schedule;
|
|
57
|
+
private attempt;
|
|
58
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export const DEFAULT_NOTIFIER_TIMING = {
|
|
2
|
+
batchMs: 2_000,
|
|
3
|
+
minIntervalMs: 10_000,
|
|
4
|
+
retryMs: 2_000,
|
|
5
|
+
burstMax: 6,
|
|
6
|
+
burstWindowMs: 10 * 60_000,
|
|
7
|
+
};
|
|
8
|
+
const MAX_LISTED = 10;
|
|
9
|
+
const oneLine = (text, max) => {
|
|
10
|
+
const flat = text
|
|
11
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
12
|
+
.replace(/[<>]/g, "")
|
|
13
|
+
.trim();
|
|
14
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
15
|
+
};
|
|
16
|
+
/** Statuses a notification can report. */
|
|
17
|
+
const ENDED = ["succeeded", "failed"];
|
|
18
|
+
/** The bounded message of one batch: ids, status and how to read the output, nothing else. */
|
|
19
|
+
export function buildTaskNotification(rows) {
|
|
20
|
+
const listed = rows.slice(0, MAX_LISTED).map((row) => {
|
|
21
|
+
const detail = [
|
|
22
|
+
`status=${row.status}`,
|
|
23
|
+
...(row.exitCode !== undefined ? [`exit_code=${row.exitCode}`] : []),
|
|
24
|
+
...(row.errorCode ? [`error=${row.errorCode}`] : []),
|
|
25
|
+
`output_bytes=${row.bytes}`,
|
|
26
|
+
].join(" ");
|
|
27
|
+
return `- id=${row.id} label="${oneLine(row.label, 80)}" ${detail}`;
|
|
28
|
+
});
|
|
29
|
+
const more = rows.length - listed.length;
|
|
30
|
+
const text = [
|
|
31
|
+
"<background-task-notification>",
|
|
32
|
+
`${rows.length} background task${rows.length === 1 ? "" : "s"} finished:`,
|
|
33
|
+
...listed,
|
|
34
|
+
...(more > 0 ? [`- and ${more} more (use bg_list)`] : []),
|
|
35
|
+
'Read the output with bg_output {"id": "<id>"}; pass next_offset as offset to continue.',
|
|
36
|
+
"This is an automatic notification, not a message from the user.",
|
|
37
|
+
"</background-task-notification>",
|
|
38
|
+
].join("\n");
|
|
39
|
+
const first = rows[0];
|
|
40
|
+
const display = rows.length === 1
|
|
41
|
+
? `Background task finished: ${oneLine(first.label, 60)} (${first.status})`
|
|
42
|
+
: `${rows.length} background tasks finished`;
|
|
43
|
+
return { text, display };
|
|
44
|
+
}
|
|
45
|
+
export class TaskNotifier {
|
|
46
|
+
options;
|
|
47
|
+
sessions = new Map();
|
|
48
|
+
timing;
|
|
49
|
+
disposed = false;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
this.timing = { ...DEFAULT_NOTIFIER_TIMING, ...options.timing };
|
|
53
|
+
}
|
|
54
|
+
now() {
|
|
55
|
+
return (this.options.now ?? Date.now)();
|
|
56
|
+
}
|
|
57
|
+
state(session) {
|
|
58
|
+
let state = this.sessions.get(session);
|
|
59
|
+
if (!state) {
|
|
60
|
+
state = { ids: new Set(), wakes: [], attempting: false };
|
|
61
|
+
this.sessions.set(session, state);
|
|
62
|
+
}
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
/** Number of finished tasks of a session waiting to be announced. */
|
|
66
|
+
pending(session) {
|
|
67
|
+
return this.sessions.get(session)?.ids.size ?? 0;
|
|
68
|
+
}
|
|
69
|
+
/** A task ended and may be announced to its root session. */
|
|
70
|
+
note(row) {
|
|
71
|
+
if (this.disposed || !ENDED.includes(row.status))
|
|
72
|
+
return;
|
|
73
|
+
const state = this.state(row.rootSession);
|
|
74
|
+
state.ids.add(row.id);
|
|
75
|
+
this.schedule(row.rootSession, state, this.timing.batchMs);
|
|
76
|
+
}
|
|
77
|
+
/** The host says the session is idle now: try again without waiting for the retry timer. */
|
|
78
|
+
poke(session) {
|
|
79
|
+
const state = this.sessions.get(session);
|
|
80
|
+
if (this.disposed || !state?.ids.size)
|
|
81
|
+
return;
|
|
82
|
+
this.schedule(session, state, 0);
|
|
83
|
+
}
|
|
84
|
+
dispose() {
|
|
85
|
+
this.disposed = true;
|
|
86
|
+
for (const state of this.sessions.values())
|
|
87
|
+
if (state.timer)
|
|
88
|
+
clearTimeout(state.timer);
|
|
89
|
+
this.sessions.clear();
|
|
90
|
+
}
|
|
91
|
+
schedule(session, state, delay) {
|
|
92
|
+
const due = this.now() + Math.max(0, delay);
|
|
93
|
+
// A sooner timer wins; a later request never postpones an armed one.
|
|
94
|
+
if (state.timer && state.dueAt !== undefined && state.dueAt <= due)
|
|
95
|
+
return;
|
|
96
|
+
if (state.timer)
|
|
97
|
+
clearTimeout(state.timer);
|
|
98
|
+
state.dueAt = due;
|
|
99
|
+
state.timer = setTimeout(() => {
|
|
100
|
+
state.timer = undefined;
|
|
101
|
+
state.dueAt = undefined;
|
|
102
|
+
void this.attempt(session, state);
|
|
103
|
+
}, Math.max(0, delay));
|
|
104
|
+
state.timer.unref?.();
|
|
105
|
+
}
|
|
106
|
+
async attempt(session, state) {
|
|
107
|
+
if (this.disposed || state.attempting || !state.ids.size)
|
|
108
|
+
return;
|
|
109
|
+
const now = this.now();
|
|
110
|
+
const { minIntervalMs, burstMax, burstWindowMs, retryMs, batchMs } = this.timing;
|
|
111
|
+
state.wakes = state.wakes.filter((at) => now - at < burstWindowMs);
|
|
112
|
+
const earliest = Math.max(state.lastWakeAt !== undefined ? state.lastWakeAt + minIntervalMs : 0, state.wakes.length >= burstMax ? state.wakes[0] + burstWindowMs : 0);
|
|
113
|
+
if (earliest > now)
|
|
114
|
+
return this.schedule(session, state, earliest - now);
|
|
115
|
+
const { store } = this.options;
|
|
116
|
+
const claimed = [];
|
|
117
|
+
for (const id of [...state.ids]) {
|
|
118
|
+
const row = store.get(id);
|
|
119
|
+
state.ids.delete(id);
|
|
120
|
+
// Read after it finished (delivered_at set by `bg_output`) or removed: nothing to announce.
|
|
121
|
+
if (!row || row.deliveredAt !== undefined)
|
|
122
|
+
continue;
|
|
123
|
+
if (store.claimDelivery(id, now))
|
|
124
|
+
claimed.push(row);
|
|
125
|
+
}
|
|
126
|
+
if (!claimed.length)
|
|
127
|
+
return;
|
|
128
|
+
state.attempting = true;
|
|
129
|
+
const release = () => {
|
|
130
|
+
for (const row of claimed) {
|
|
131
|
+
store.releaseDelivery(row.id, now);
|
|
132
|
+
state.ids.add(row.id);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
const { text, display } = buildTaskNotification(claimed.map((row) => store.get(row.id) ?? row));
|
|
137
|
+
const outcome = await this.options.wake({
|
|
138
|
+
sessionId: session,
|
|
139
|
+
text,
|
|
140
|
+
display,
|
|
141
|
+
requestId: `bg-${claimed[0]?.id}`,
|
|
142
|
+
taskIds: claimed.map((row) => row.id),
|
|
143
|
+
});
|
|
144
|
+
if (outcome === "started") {
|
|
145
|
+
state.lastWakeAt = now;
|
|
146
|
+
state.wakes.push(now);
|
|
147
|
+
if (state.ids.size)
|
|
148
|
+
this.schedule(session, state, batchMs);
|
|
149
|
+
}
|
|
150
|
+
else if (outcome === "busy") {
|
|
151
|
+
release();
|
|
152
|
+
this.schedule(session, state, retryMs);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// `skip`: the session cannot be woken at all; stop retrying, deliver nothing.
|
|
156
|
+
for (const row of claimed)
|
|
157
|
+
store.releaseDelivery(row.id, now);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.options.onError?.(error);
|
|
162
|
+
release();
|
|
163
|
+
this.schedule(session, state, retryMs);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
state.attempting = false;
|
|
167
|
+
// Tasks that finished while the wake-up was in flight must not wait for another `note`.
|
|
168
|
+
if (!this.disposed && state.ids.size && !state.timer)
|
|
169
|
+
this.schedule(session, state, retryMs);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const TAIL_BYTES: number;
|
|
2
|
+
/** Default size of one `bg_output` read and its hard maximum. */
|
|
3
|
+
export declare const DEFAULT_READ_BYTES: number;
|
|
4
|
+
export declare const MAX_READ_BYTES: number;
|
|
5
|
+
/** The folder and relative path of a task log under the state root. */
|
|
6
|
+
export declare const taskLogPath: (rootSession: string, id: string) => string;
|
|
7
|
+
/** Absolute path of a stored (relative) log path; refuses anything that escapes the state root. */
|
|
8
|
+
export declare function resolveLogPath(stateRoot: string, relativePath: string): string;
|
|
9
|
+
/** Length of the longest prefix of `bytes` that does not end in the middle of a UTF-8 sequence. */
|
|
10
|
+
export declare function completeUtf8Length(bytes: Uint8Array): number;
|
|
11
|
+
/** The writing side of one task log. */
|
|
12
|
+
export declare class TaskLogWriter {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
private readonly maxBytes;
|
|
15
|
+
private fd;
|
|
16
|
+
private written;
|
|
17
|
+
private cut;
|
|
18
|
+
private received;
|
|
19
|
+
private tail;
|
|
20
|
+
private tailBytes;
|
|
21
|
+
private closed;
|
|
22
|
+
constructor(path: string, maxBytes: number);
|
|
23
|
+
/** Bytes in the file so far. */
|
|
24
|
+
get bytes(): number;
|
|
25
|
+
/** Whether output was left out of the stored log. */
|
|
26
|
+
get truncated(): boolean;
|
|
27
|
+
append(chunk: string | Buffer): void;
|
|
28
|
+
private keepTail;
|
|
29
|
+
private write;
|
|
30
|
+
/** Appends the tail of a truncated log and closes the file. Idempotent. */
|
|
31
|
+
finish(): void;
|
|
32
|
+
}
|
|
33
|
+
export interface LogRead {
|
|
34
|
+
text: string;
|
|
35
|
+
nextOffset: number;
|
|
36
|
+
/** Size of the file at the time of the read. */
|
|
37
|
+
size: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads at most `limit` bytes of a log from `offset`. The text ends at a character boundary:
|
|
41
|
+
* `nextOffset` is where the next read must start. A missing file reads as empty.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readLog(path: string, offset: number, limit: number): Promise<LogRead>;
|
|
44
|
+
/** Appends a note to a finished log file (used for spawn errors); best effort. */
|
|
45
|
+
export declare function appendNote(path: string, note: string): void;
|
|
46
|
+
export declare function removeLog(path: string): Promise<void>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task logs: one append-only file per task under `<state>/tasks/<root session>/<task id>.log`,
|
|
3
|
+
* written with synchronous appends (chunks are small) so a reader never sees less than what the
|
|
4
|
+
* writer reported. Readers address the file by byte offset: nothing is ever rewritten, so an offset
|
|
5
|
+
* stays valid for the life of the task.
|
|
6
|
+
*
|
|
7
|
+
* Size limit: the first `maxBytes` of output are kept, then ONE marker line. The rest is not
|
|
8
|
+
* stored, except for a rolling tail of the last `TAIL_BYTES` which is appended when the task ends,
|
|
9
|
+
* so the error at the end of a very long build is still readable.
|
|
10
|
+
*/
|
|
11
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, writeSync } from "node:fs";
|
|
12
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
13
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
|
+
import { inside } from "../runtime/paths.js";
|
|
15
|
+
export const TAIL_BYTES = 32 * 1024;
|
|
16
|
+
/** Default size of one `bg_output` read and its hard maximum. */
|
|
17
|
+
export const DEFAULT_READ_BYTES = 16 * 1024;
|
|
18
|
+
export const MAX_READ_BYTES = 64 * 1024;
|
|
19
|
+
/** The folder and relative path of a task log under the state root. */
|
|
20
|
+
export const taskLogPath = (rootSession, id) => join("tasks", rootSession, `${id}.log`);
|
|
21
|
+
/** Absolute path of a stored (relative) log path; refuses anything that escapes the state root. */
|
|
22
|
+
export function resolveLogPath(stateRoot, relativePath) {
|
|
23
|
+
const absolute = resolve(stateRoot, relativePath);
|
|
24
|
+
if (isAbsolute(relativePath) || !inside(resolve(stateRoot), absolute))
|
|
25
|
+
throw new Error("Invalid task log path");
|
|
26
|
+
return absolute;
|
|
27
|
+
}
|
|
28
|
+
/** Length of the longest prefix of `bytes` that does not end in the middle of a UTF-8 sequence. */
|
|
29
|
+
export function completeUtf8Length(bytes) {
|
|
30
|
+
let end = bytes.length;
|
|
31
|
+
// Walk back over at most 3 continuation bytes to the lead byte of the last character.
|
|
32
|
+
for (let back = 1; back <= 4 && end - back >= 0; back++) {
|
|
33
|
+
const byte = bytes[end - back];
|
|
34
|
+
if ((byte & 0xc0) === 0x80)
|
|
35
|
+
continue; // continuation byte
|
|
36
|
+
const needed = byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : byte >= 0xc0 ? 2 : 1;
|
|
37
|
+
return back < needed ? end - back : end;
|
|
38
|
+
}
|
|
39
|
+
return end;
|
|
40
|
+
}
|
|
41
|
+
/** The writing side of one task log. */
|
|
42
|
+
export class TaskLogWriter {
|
|
43
|
+
path;
|
|
44
|
+
maxBytes;
|
|
45
|
+
fd;
|
|
46
|
+
written = 0;
|
|
47
|
+
cut = false;
|
|
48
|
+
received = 0;
|
|
49
|
+
tail = [];
|
|
50
|
+
tailBytes = 0;
|
|
51
|
+
closed = false;
|
|
52
|
+
constructor(path, maxBytes) {
|
|
53
|
+
this.path = path;
|
|
54
|
+
this.maxBytes = maxBytes;
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
56
|
+
this.fd = openSync(path, "a", 0o600);
|
|
57
|
+
}
|
|
58
|
+
/** Bytes in the file so far. */
|
|
59
|
+
get bytes() {
|
|
60
|
+
return this.written;
|
|
61
|
+
}
|
|
62
|
+
/** Whether output was left out of the stored log. */
|
|
63
|
+
get truncated() {
|
|
64
|
+
return this.cut;
|
|
65
|
+
}
|
|
66
|
+
append(chunk) {
|
|
67
|
+
if (this.closed || this.fd === undefined)
|
|
68
|
+
return;
|
|
69
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
70
|
+
if (!buffer.length)
|
|
71
|
+
return;
|
|
72
|
+
this.received += buffer.length;
|
|
73
|
+
if (this.cut)
|
|
74
|
+
return this.keepTail(buffer);
|
|
75
|
+
const room = this.maxBytes - this.written;
|
|
76
|
+
if (buffer.length <= room)
|
|
77
|
+
return this.write(buffer);
|
|
78
|
+
const head = buffer.subarray(0, Math.max(0, room));
|
|
79
|
+
// Never end the stored head in the middle of a character.
|
|
80
|
+
const whole = head.subarray(0, completeUtf8Length(head));
|
|
81
|
+
if (whole.length)
|
|
82
|
+
this.write(whole);
|
|
83
|
+
this.write(Buffer.from(`\n[output truncated: the log limit of ${this.maxBytes} bytes was reached; the last ${TAIL_BYTES} bytes are appended when the task ends]\n`));
|
|
84
|
+
this.cut = true;
|
|
85
|
+
this.keepTail(buffer.subarray(whole.length));
|
|
86
|
+
}
|
|
87
|
+
keepTail(buffer) {
|
|
88
|
+
this.tail.push(buffer);
|
|
89
|
+
this.tailBytes += buffer.length;
|
|
90
|
+
while (this.tailBytes - (this.tail[0]?.length ?? 0) >= TAIL_BYTES) {
|
|
91
|
+
this.tailBytes -= this.tail.shift().length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
write(buffer) {
|
|
95
|
+
if (this.fd === undefined)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
writeSync(this.fd, buffer);
|
|
99
|
+
this.written += buffer.length;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* A full disk never fails the task itself. */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Appends the tail of a truncated log and closes the file. Idempotent. */
|
|
106
|
+
finish() {
|
|
107
|
+
if (this.closed)
|
|
108
|
+
return;
|
|
109
|
+
if (this.cut && this.tailBytes > 0) {
|
|
110
|
+
let tail = Buffer.concat(this.tail);
|
|
111
|
+
if (tail.length > TAIL_BYTES)
|
|
112
|
+
tail = tail.subarray(tail.length - TAIL_BYTES);
|
|
113
|
+
// The cut may split a character at the start of the tail.
|
|
114
|
+
let start = 0;
|
|
115
|
+
while (start < tail.length && start < 4 && (tail[start] & 0xc0) === 0x80)
|
|
116
|
+
start++;
|
|
117
|
+
tail = tail.subarray(start);
|
|
118
|
+
const omitted = this.received - this.written - tail.length;
|
|
119
|
+
if (omitted > 0)
|
|
120
|
+
this.write(Buffer.from(`\n[... ${omitted} bytes omitted; the end of the output follows ...]\n`));
|
|
121
|
+
this.write(tail);
|
|
122
|
+
}
|
|
123
|
+
this.closed = true;
|
|
124
|
+
if (this.fd !== undefined) {
|
|
125
|
+
try {
|
|
126
|
+
closeSync(this.fd);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* already closed */
|
|
130
|
+
}
|
|
131
|
+
this.fd = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reads at most `limit` bytes of a log from `offset`. The text ends at a character boundary:
|
|
137
|
+
* `nextOffset` is where the next read must start. A missing file reads as empty.
|
|
138
|
+
*/
|
|
139
|
+
export async function readLog(path, offset, limit) {
|
|
140
|
+
const start = Math.max(0, Math.floor(offset));
|
|
141
|
+
const want = Math.max(1, Math.min(Math.floor(limit), MAX_READ_BYTES));
|
|
142
|
+
let size = 0;
|
|
143
|
+
try {
|
|
144
|
+
size = (await stat(path)).size;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { text: "", nextOffset: start, size: 0 };
|
|
148
|
+
}
|
|
149
|
+
if (start >= size)
|
|
150
|
+
return { text: "", nextOffset: start, size };
|
|
151
|
+
const handle = await open(path, "r");
|
|
152
|
+
try {
|
|
153
|
+
const buffer = Buffer.alloc(Math.min(want, size - start));
|
|
154
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
155
|
+
let slice = buffer.subarray(0, bytesRead);
|
|
156
|
+
// Hold back an incomplete trailing character (the next read completes it); if that leaves
|
|
157
|
+
// nothing (a lone broken byte), return the raw bytes so a reader always makes progress.
|
|
158
|
+
slice = slice.subarray(0, completeUtf8Length(slice));
|
|
159
|
+
if (!slice.length && bytesRead)
|
|
160
|
+
slice = buffer.subarray(0, bytesRead);
|
|
161
|
+
return { text: new TextDecoder().decode(slice), nextOffset: start + slice.length, size };
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await handle.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Appends a note to a finished log file (used for spawn errors); best effort. */
|
|
168
|
+
export function appendNote(path, note) {
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
171
|
+
appendFileSync(path, note, { mode: 0o600 });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* best effort */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function removeLog(path) {
|
|
178
|
+
await rm(path, { force: true });
|
|
179
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BackgroundTasks`: the runtime behind `bg_run`, `bg_list`, `bg_output` and `bg_stop`, and behind
|
|
3
|
+
* the `/tasks` panels of the TUI and the web.
|
|
4
|
+
*
|
|
5
|
+
* A task is an ordinary child process of Alisio started through the shared process runner
|
|
6
|
+
* (`runProcess`: same shell, same environment allowlist, process-group kill, `taskkill /T /F` on
|
|
7
|
+
* Windows). It is NOT detached and NOT sandboxed: it dies with Alisio. The process group of every
|
|
8
|
+
* live task is killed on an orderly `close()` and, as a last resort, from a synchronous `exit`
|
|
9
|
+
* handler; after an abrupt death of Alisio (SIGKILL, power loss) nothing can run, so the next start
|
|
10
|
+
* marks the leftovers `lost` (by owner pid) without touching them.
|
|
11
|
+
*
|
|
12
|
+
* Every state change is a compare-and-set in the database (see `store.ts`). The origin of an abort
|
|
13
|
+
* is kept apart from its effect: a stop by the user or the model, or a shutdown, ends `cancelled`
|
|
14
|
+
* (never `failed`); the watchdog ends `failed` with the code `timeout`.
|
|
15
|
+
*/
|
|
16
|
+
import type { BackgroundTaskInfo, BackgroundTaskOutput, BackgroundTaskStatus, SqlDatabase } from "@alisio/sdk";
|
|
17
|
+
import { type NotifierTiming, type TaskWake } from "./notify.ts";
|
|
18
|
+
import { BackgroundTaskStore } from "./store.ts";
|
|
19
|
+
/** Why a task could not be started. */
|
|
20
|
+
export type TaskAdmissionCode = "disabled" | "read_only" | "shutting_down" | "session_limit" | "process_limit";
|
|
21
|
+
export declare class TaskAdmissionError extends Error {
|
|
22
|
+
readonly code: TaskAdmissionCode;
|
|
23
|
+
constructor(code: TaskAdmissionCode, message: string);
|
|
24
|
+
}
|
|
25
|
+
/** An unknown task id, or one that belongs to another session tree. */
|
|
26
|
+
export declare class TaskNotFoundError extends Error {
|
|
27
|
+
constructor(id: string);
|
|
28
|
+
}
|
|
29
|
+
/** The task belongs to another live Alisio process: only that process can stop it. */
|
|
30
|
+
export declare class TaskForeignError extends Error {
|
|
31
|
+
readonly ownerPid: number;
|
|
32
|
+
constructor(ownerPid: number);
|
|
33
|
+
}
|
|
34
|
+
export interface TaskLimits {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
maxPerSession: number;
|
|
37
|
+
maxRunMs: number;
|
|
38
|
+
maxOutputBytes: number;
|
|
39
|
+
}
|
|
40
|
+
export interface BackgroundTasksOptions {
|
|
41
|
+
db: SqlDatabase;
|
|
42
|
+
/** State root: logs live under `<stateRoot>/tasks/<root session>/`. */
|
|
43
|
+
stateRoot: string;
|
|
44
|
+
/** The root of a session (a child session shares its root's tasks). */
|
|
45
|
+
rootOf: (session: string) => string;
|
|
46
|
+
/** Read live on every call, so a changed setting applies to the next task. */
|
|
47
|
+
limits: () => TaskLimits;
|
|
48
|
+
/** `--read-only`: no task can be started. */
|
|
49
|
+
readOnly?: boolean;
|
|
50
|
+
/** Host hook that starts a run to tell the owner about finished tasks (absent: no wake-ups). */
|
|
51
|
+
wake?: TaskWake;
|
|
52
|
+
/** Whether the session may still be woken (default: always). */
|
|
53
|
+
timing?: Partial<NotifierTiming>;
|
|
54
|
+
/** Live tasks across every application of this process (default 16). */
|
|
55
|
+
maxPerProcess?: number;
|
|
56
|
+
/** SIGTERM → SIGKILL grace of a stop (default 3 s). */
|
|
57
|
+
killGraceMs?: number;
|
|
58
|
+
/** How long `close()` waits for processes to end before killing their groups (default 1.5 s). */
|
|
59
|
+
closeGraceMs?: number;
|
|
60
|
+
/** A task appeared or changed state. */
|
|
61
|
+
onChange?: (task: BackgroundTaskInfo, rootSession: string) => void;
|
|
62
|
+
/** Read-only mirror of other task sources (subagents) for the unified list. */
|
|
63
|
+
mirror?: (rootSession: string) => BackgroundTaskInfo[];
|
|
64
|
+
onError?: (error: unknown) => void;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
/** Owner pid recorded on new rows (tests simulate another process). */
|
|
67
|
+
pid?: number;
|
|
68
|
+
}
|
|
69
|
+
export declare class BackgroundTasks {
|
|
70
|
+
private options;
|
|
71
|
+
readonly store: BackgroundTaskStore;
|
|
72
|
+
private live;
|
|
73
|
+
private closing;
|
|
74
|
+
private notifier;
|
|
75
|
+
private readonly pid;
|
|
76
|
+
constructor(options: BackgroundTasksOptions);
|
|
77
|
+
private now;
|
|
78
|
+
/** Whether finished tasks wake the owner agent in this host (tools say so in their result). */
|
|
79
|
+
get notifies(): boolean;
|
|
80
|
+
/** Startup recovery: tasks of a dead owner process become `lost`. */
|
|
81
|
+
recover(): BackgroundTaskInfo[];
|
|
82
|
+
private emit;
|
|
83
|
+
private info;
|
|
84
|
+
/** Starts `command` in the background. Throws `TaskAdmissionError` when it cannot be admitted. */
|
|
85
|
+
start(input: {
|
|
86
|
+
session: string;
|
|
87
|
+
workspace: string;
|
|
88
|
+
command: string;
|
|
89
|
+
/** Absolute working directory (already resolved through the path policy). */
|
|
90
|
+
cwd: string;
|
|
91
|
+
label?: string;
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
}): BackgroundTaskInfo;
|
|
94
|
+
private run;
|
|
95
|
+
/** Final state of a task, exactly once (compare-and-set), then the notification decision. */
|
|
96
|
+
private settle;
|
|
97
|
+
/** Moves a live task to `stopping` (compare-and-set) and aborts it. Idempotent. */
|
|
98
|
+
private requestStop;
|
|
99
|
+
private ownRow;
|
|
100
|
+
/** One task of a root session (its tree), or `TaskNotFoundError`. */
|
|
101
|
+
get(rootSession: string, id: string): BackgroundTaskInfo;
|
|
102
|
+
/** Tasks of a root session, newest first (shell tasks, plus the read-only subagent mirror). */
|
|
103
|
+
list(rootSession: string, options?: {
|
|
104
|
+
status?: BackgroundTaskStatus[];
|
|
105
|
+
limit?: number;
|
|
106
|
+
mirror?: boolean;
|
|
107
|
+
}): BackgroundTaskInfo[];
|
|
108
|
+
/** Live (queued, running, stopping) tasks of this service, optionally of one root session. */
|
|
109
|
+
liveCount(rootSession?: string): number;
|
|
110
|
+
/**
|
|
111
|
+
* Reads the log from `offset`. `reader: "model"` (the `bg_output` tool) also marks a finished
|
|
112
|
+
* task as consumed: the agent has seen that it ended, so it is never announced afterwards.
|
|
113
|
+
*/
|
|
114
|
+
read(rootSession: string, id: string, options?: {
|
|
115
|
+
offset?: number;
|
|
116
|
+
limit?: number;
|
|
117
|
+
reader?: "model" | "user";
|
|
118
|
+
}): Promise<BackgroundTaskOutput>;
|
|
119
|
+
/** The agent saw that these tasks finished (read or listed them): nothing to announce. */
|
|
120
|
+
markSeen(ids: string[]): void;
|
|
121
|
+
/**
|
|
122
|
+
* Stops a task. The origin (`user` from a UI, `model` from `bg_stop`) is kept: the task ends
|
|
123
|
+
* `cancelled`, never `failed`. Resolves with the task as it is after the process ended (or after
|
|
124
|
+
* the grace period when it is slow to die). A finished task is returned unchanged.
|
|
125
|
+
*/
|
|
126
|
+
stop(rootSession: string, id: string, origin: "user" | "model"): Promise<BackgroundTaskInfo>;
|
|
127
|
+
/** The host reports that a session is idle again: a notification held back may go now. */
|
|
128
|
+
sessionIdle(rootSession: string): void;
|
|
129
|
+
/** Tasks announced as finished but not yet delivered (tests and diagnostics). */
|
|
130
|
+
pendingNotifications(rootSession: string): number;
|
|
131
|
+
/**
|
|
132
|
+
* Orderly shutdown: refuse new tasks, stop announcing, abort every live task (origin
|
|
133
|
+
* `shutdown`, so they end `cancelled`), wait for the processes and kill the groups of any that
|
|
134
|
+
* ignore SIGTERM. Nothing is left running when it resolves.
|
|
135
|
+
*/
|
|
136
|
+
close(): Promise<void>;
|
|
137
|
+
}
|