arisa 5.1.68 → 5.2.17
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 +28 -6
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +123 -8
- package/src/core/agent/agent-session-lifecycle.js +108 -8
- package/src/core/agent/agent-turn-coordinator.js +143 -0
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-capability-tools.js +7 -4
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/session-history-reader.js +168 -0
- package/src/core/agent/session-preload-migration.js +162 -0
- package/src/core/agent/session-rotation.js +29 -0
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/agent/worker-tool-fanout.js +117 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +4 -3
- package/src/core/config/config-defaults.js +59 -1
- package/src/core/config/config-store.js +1 -1
- package/src/core/conversation/session-seed-store.js +1 -1
- package/src/core/tasks/task-runner.js +8 -4
- package/src/core/tasks/task-store.js +48 -5
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +26 -3
- package/src/core/tools/daemon-protocol.js +72 -0
- package/src/core/tools/daemon-runtime.js +13 -490
- package/src/core/tools/daemon-worker.js +310 -0
- package/src/core/tools/ipc-client.js +2 -2
- package/src/core/tools/memory-pressure.js +56 -0
- package/src/core/tools/official-tool-installer.js +1 -1
- package/src/core/tools/tool-config.js +1 -1
- package/src/core/tools/tool-process-output.js +100 -0
- package/src/core/tools/tool-process-runner.js +175 -0
- package/src/core/tools/tool-registry.js +131 -191
- package/src/core/tools/tool-resource-note-store.js +1 -1
- package/src/core/tools/tool-usage-store.js +1 -1
- package/src/core/tools/weighted-resource-governor.js +189 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +526 -78
- package/src/platform/paths.js +152 -0
- package/src/runtime/bootstrap-cli.js +121 -0
- package/src/runtime/bootstrap-config.js +97 -0
- package/src/runtime/bootstrap-telegram.js +325 -0
- package/src/runtime/bootstrap.js +6 -543
- package/src/runtime/doctor.js +26 -76
- package/src/runtime/flush.js +1 -1
- package/src/runtime/ipc/ipc-server.js +1 -1
- package/src/runtime/log-viewer.js +1 -1
- package/src/runtime/obsolete-daemon-reaper.js +43 -0
- package/src/runtime/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- package/src/runtime/process-inspection.js +78 -0
- package/src/runtime/restart-receipt.js +1 -1
- package/src/runtime/service-manager.js +1 -1
- package/src/runtime/service-supervisor.js +14 -0
- package/src/runtime/slave-cli.js +1 -1
- package/src/runtime/tool-process-supervisor.js +36 -9
- package/src/runtime/tui.js +200 -0
- package/src/runtime/update-manager.js +1 -1
- package/src/runtime/worker-recovery-report.js +142 -0
- package/src/transport/telegram/bot.js +45 -321
- package/src/transport/telegram/chat-queue.js +6 -2
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/task-dispatcher.js +36 -11
- package/src/transport/telegram/telegram-prompt-controller.js +359 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/agent-turn-coordinator.test.js +45 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/context-and-task-bounds.test.js +2 -1
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +38 -5
- package/test/doctor.test.js +41 -0
- package/test/memory-pressure.test.js +41 -0
- package/test/model-selection.test.js +11 -1
- package/test/obsolete-daemon-reaper.test.js +61 -0
- package/test/official-tool-dependencies.test.js +7 -2
- package/test/official-tool-installer.test.js +18 -1
- package/test/oom-protection.test.js +32 -0
- package/test/paths.test.js +7 -0
- package/test/pi-compaction.test.js +30 -0
- package/test/service-manager.test.js +6 -1
- package/test/session-history-reader.test.js +84 -0
- package/test/session-preload-migration.test.js +120 -0
- package/test/session-rotation.test.js +110 -0
- package/test/task-store.test.js +32 -0
- package/test/telegram-prompt-controller.test.js +82 -0
- package/test/telegram-task-dispatcher.test.js +66 -5
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +117 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +125 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- package/test/worker-tool-fanout.test.js +79 -0
- package/test-fixtures/fake-daemon.js +5 -0
|
@@ -18,7 +18,7 @@ function nativeTools(policy) {
|
|
|
18
18
|
}];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function createPiCapabilityTools({ capabilityService, telegram, chatId, policy, logger }) {
|
|
21
|
+
export function createPiCapabilityTools({ capabilityService, telegram, chatId, policy, logger, toolFanout }) {
|
|
22
22
|
if (!capabilityService?.execute) throw new Error("Pi capability tools require CapabilityService");
|
|
23
23
|
|
|
24
24
|
const baseContext = {
|
|
@@ -38,6 +38,7 @@ export function createPiCapabilityTools({ capabilityService, telegram, chatId, p
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const runWithFanout = (work) => toolFanout?.run ? toolFanout.run(work) : work();
|
|
41
42
|
const execute = (actorToolName, method, params = {}, context = {}) => capabilityService.execute({
|
|
42
43
|
method,
|
|
43
44
|
actorToolName,
|
|
@@ -113,12 +114,14 @@ export function createPiCapabilityTools({ capabilityService, telegram, chatId, p
|
|
|
113
114
|
args: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
114
115
|
deliver: Type.Optional(Type.Boolean())
|
|
115
116
|
}),
|
|
116
|
-
execute: async (_id, params) => jsonResult(await
|
|
117
|
+
execute: async (_id, params) => jsonResult(await runWithFanout(
|
|
118
|
+
() => execute("run_tool", "tools.run", params)
|
|
119
|
+
))
|
|
117
120
|
}),
|
|
118
121
|
defineTool({
|
|
119
122
|
name: "list_scheduled_tasks",
|
|
120
123
|
label: "List scheduled tasks",
|
|
121
|
-
description: "List scheduled async tasks for the current Telegram chat. Results default to 50 tasks, always include pending
|
|
124
|
+
description: "List scheduled async tasks for the current Telegram chat. Results default to 50 tasks, always include pending, running, and authentication-blocked tasks, and accept an optional limit up to 100.",
|
|
122
125
|
parameters: Type.Object({
|
|
123
126
|
status: Type.Optional(Type.String()),
|
|
124
127
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxScheduledTaskListLimit }))
|
|
@@ -135,7 +138,7 @@ export function createPiCapabilityTools({ capabilityService, telegram, chatId, p
|
|
|
135
138
|
defineTool({
|
|
136
139
|
name: "cancel_all_scheduled_tasks",
|
|
137
140
|
label: "Cancel all scheduled tasks",
|
|
138
|
-
description: "Cancel all
|
|
141
|
+
description: "Cancel all active async tasks, including authentication-blocked tasks, for the current Telegram chat.",
|
|
139
142
|
parameters: Type.Object({}),
|
|
140
143
|
execute: async () => jsonResult(await execute("cancel_all_scheduled_tasks", "tasks.cancelAll"))
|
|
141
144
|
}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { piAuthFile } from "../../
|
|
2
|
+
import { piAuthFile } from "../../platform/paths.js";
|
|
3
3
|
|
|
4
4
|
function compareText(a, b) {
|
|
5
5
|
return a.localeCompare(b, undefined, { sensitivity: "base", numeric: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../
|
|
3
|
+
import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../platform/paths.js";
|
|
4
4
|
|
|
5
5
|
export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
|
|
6
6
|
export const arisaAgentsFile = path.join(arisaPackageDir, "AGENTS.md");
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
|
|
5
|
+
const readBufferBytes = 1024 * 1024;
|
|
6
|
+
const supportedSessionVersion = 3;
|
|
7
|
+
|
|
8
|
+
function parseEntry(line) {
|
|
9
|
+
if (!line.trim()) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(line);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readSessionHeader(filePath) {
|
|
18
|
+
const descriptor = openSync(filePath, "r");
|
|
19
|
+
try {
|
|
20
|
+
const buffer = Buffer.alloc(4096);
|
|
21
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
22
|
+
const line = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0];
|
|
23
|
+
const header = parseEntry(line);
|
|
24
|
+
return header?.type === "session" ? header : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
} finally {
|
|
28
|
+
closeSync(descriptor);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findMostRecentSessionFile(sessionDir, cwd) {
|
|
33
|
+
const expectedCwd = path.resolve(cwd);
|
|
34
|
+
try {
|
|
35
|
+
return readdirSync(sessionDir)
|
|
36
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
37
|
+
.map((name) => path.join(sessionDir, name))
|
|
38
|
+
.map((filePath) => ({ filePath, header: readSessionHeader(filePath) }))
|
|
39
|
+
.filter(({ header }) => header && typeof header.cwd === "string" && path.resolve(header.cwd) === expectedCwd)
|
|
40
|
+
.map(({ filePath }) => ({ filePath, mtimeMs: statSync(filePath).mtimeMs }))
|
|
41
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]?.filePath || null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function streamSessionEntries(filePath, visit) {
|
|
48
|
+
const descriptor = openSync(filePath, "r");
|
|
49
|
+
try {
|
|
50
|
+
const decoder = new StringDecoder("utf8");
|
|
51
|
+
const buffer = Buffer.allocUnsafe(readBufferBytes);
|
|
52
|
+
let pending = "";
|
|
53
|
+
while (true) {
|
|
54
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
|
|
55
|
+
if (!bytesRead) break;
|
|
56
|
+
pending += decoder.write(buffer.subarray(0, bytesRead));
|
|
57
|
+
let newline = pending.indexOf("\n");
|
|
58
|
+
while (newline !== -1) {
|
|
59
|
+
const entry = parseEntry(pending.slice(0, newline));
|
|
60
|
+
if (entry) visit(entry);
|
|
61
|
+
pending = pending.slice(newline + 1);
|
|
62
|
+
newline = pending.indexOf("\n");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
pending += decoder.end();
|
|
66
|
+
const entry = parseEntry(pending);
|
|
67
|
+
if (entry) visit(entry);
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(descriptor);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function traceActivePath(entries, leafId) {
|
|
74
|
+
const reversed = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
let currentId = leafId;
|
|
77
|
+
while (currentId) {
|
|
78
|
+
if (seen.has(currentId)) return null;
|
|
79
|
+
seen.add(currentId);
|
|
80
|
+
const entry = entries.get(currentId);
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
reversed.push(entry);
|
|
83
|
+
currentId = entry.parentId || null;
|
|
84
|
+
}
|
|
85
|
+
return reversed.reverse();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findLatestValidCompaction(path) {
|
|
89
|
+
const pathIndex = new Map(path.map((entry, index) => [entry.id, index]));
|
|
90
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
91
|
+
const entry = path[index];
|
|
92
|
+
if (entry.type !== "compaction" || !entry.hasSummary) continue;
|
|
93
|
+
if (!entry.firstKeptEntryId) return { entry, index, firstKeptIndex: index };
|
|
94
|
+
const firstKeptIndex = pathIndex.get(entry.firstKeptEntryId);
|
|
95
|
+
if (firstKeptIndex !== undefined && firstKeptIndex < index) {
|
|
96
|
+
return { entry, index, firstKeptIndex };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inspectSessionGraph(filePath) {
|
|
103
|
+
let header = null;
|
|
104
|
+
let firstEntry = true;
|
|
105
|
+
let invalidHeader = false;
|
|
106
|
+
let leafId = null;
|
|
107
|
+
let duplicateId = false;
|
|
108
|
+
const entries = new Map();
|
|
109
|
+
streamSessionEntries(filePath, (entry) => {
|
|
110
|
+
if (firstEntry) {
|
|
111
|
+
firstEntry = false;
|
|
112
|
+
header = entry.type === "session" ? entry : null;
|
|
113
|
+
invalidHeader = !header;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (invalidHeader) return;
|
|
117
|
+
if (!entry.id || entry.type === "session") return;
|
|
118
|
+
if (entries.has(entry.id)) duplicateId = true;
|
|
119
|
+
entries.set(entry.id, {
|
|
120
|
+
id: entry.id,
|
|
121
|
+
parentId: entry.parentId || null,
|
|
122
|
+
type: entry.type,
|
|
123
|
+
firstKeptEntryId: entry.type === "compaction" ? entry.firstKeptEntryId || null : null,
|
|
124
|
+
hasSummary: entry.type === "compaction" && Boolean(String(entry.summary || "").trim())
|
|
125
|
+
});
|
|
126
|
+
leafId = entry.id;
|
|
127
|
+
});
|
|
128
|
+
if (invalidHeader || !header || header.version !== supportedSessionVersion || duplicateId || !leafId) return null;
|
|
129
|
+
const path = traceActivePath(entries, leafId);
|
|
130
|
+
if (!path) return null;
|
|
131
|
+
const compaction = findLatestValidCompaction(path);
|
|
132
|
+
if (!compaction) return null;
|
|
133
|
+
return { header, path, compaction };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loadMigrationPayload(filePath, path, compaction) {
|
|
137
|
+
const before = path.slice(compaction.firstKeptIndex, compaction.index);
|
|
138
|
+
const after = path.slice(compaction.index + 1);
|
|
139
|
+
const contextIds = [...before, ...after].map((entry) => entry.id);
|
|
140
|
+
const wanted = new Set(contextIds);
|
|
141
|
+
const loaded = new Map();
|
|
142
|
+
let summary = "";
|
|
143
|
+
streamSessionEntries(filePath, (entry) => {
|
|
144
|
+
if (entry.id === compaction.entry.id) summary = String(entry.summary || "").trim();
|
|
145
|
+
if (wanted.has(entry.id)) loaded.set(entry.id, entry);
|
|
146
|
+
});
|
|
147
|
+
if (!summary || loaded.size !== wanted.size) return null;
|
|
148
|
+
return {
|
|
149
|
+
summary,
|
|
150
|
+
contextEntries: contextIds.map((id) => loaded.get(id))
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function inspectSessionForPreloadMigration(filePath, maxPersistedBytes) {
|
|
155
|
+
const sourceBytes = statSync(filePath).size;
|
|
156
|
+
if (sourceBytes <= Math.max(1, Number(maxPersistedBytes) || 1)) return null;
|
|
157
|
+
const graph = inspectSessionGraph(filePath);
|
|
158
|
+
if (!graph) return null;
|
|
159
|
+
const payload = loadMigrationPayload(filePath, graph.path, graph.compaction);
|
|
160
|
+
if (!payload) return null;
|
|
161
|
+
return {
|
|
162
|
+
sourceFile: filePath,
|
|
163
|
+
sourceBytes,
|
|
164
|
+
header: graph.header,
|
|
165
|
+
compactionId: graph.compaction.entry.id,
|
|
166
|
+
...payload
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
fsyncSync,
|
|
4
|
+
openSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
statSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import {
|
|
13
|
+
findMostRecentSessionFile,
|
|
14
|
+
inspectSessionForPreloadMigration
|
|
15
|
+
} from "./session-history-reader.js";
|
|
16
|
+
import { normalizeSessionRotationPolicy } from "./session-rotation.js";
|
|
17
|
+
|
|
18
|
+
const contextEntryTypes = new Set([
|
|
19
|
+
"message",
|
|
20
|
+
"custom_message",
|
|
21
|
+
"branch_summary",
|
|
22
|
+
"thinking_level_change",
|
|
23
|
+
"model_change"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function createEntry(type, parentId, fields = {}) {
|
|
27
|
+
return {
|
|
28
|
+
type,
|
|
29
|
+
...fields,
|
|
30
|
+
id: randomUUID(),
|
|
31
|
+
parentId,
|
|
32
|
+
timestamp: fields.timestamp || new Date().toISOString()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendDurably(filePath, entry) {
|
|
37
|
+
writeFileSync(filePath, `${JSON.stringify(entry)}\n`, { encoding: "utf8", flag: "a" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function syncFile(filePath) {
|
|
41
|
+
const descriptor = openSync(filePath, "r");
|
|
42
|
+
try {
|
|
43
|
+
fsyncSync(descriptor);
|
|
44
|
+
} finally {
|
|
45
|
+
closeSync(descriptor);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function syncDirectory(directory) {
|
|
50
|
+
const descriptor = openSync(directory, "r");
|
|
51
|
+
try {
|
|
52
|
+
fsyncSync(descriptor);
|
|
53
|
+
} finally {
|
|
54
|
+
closeSync(descriptor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function migratedEntry(source, parentId) {
|
|
59
|
+
const { id: _id, parentId: _parentId, ...fields } = source;
|
|
60
|
+
return createEntry(source.type, parentId, fields);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createPreloadMigrationChild({
|
|
64
|
+
sessionDir,
|
|
65
|
+
cwd,
|
|
66
|
+
migration,
|
|
67
|
+
operationalNotes = "",
|
|
68
|
+
now = new Date()
|
|
69
|
+
}) {
|
|
70
|
+
const timestamp = now.toISOString();
|
|
71
|
+
const sessionId = randomUUID();
|
|
72
|
+
const filenameTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
73
|
+
const targetFile = path.join(sessionDir, `${filenameTimestamp}_${sessionId}.jsonl`);
|
|
74
|
+
const temporaryFile = path.join(sessionDir, `.session-migration-${sessionId}.tmp`);
|
|
75
|
+
const header = {
|
|
76
|
+
type: "session",
|
|
77
|
+
version: 3,
|
|
78
|
+
id: sessionId,
|
|
79
|
+
timestamp,
|
|
80
|
+
cwd,
|
|
81
|
+
parentSession: migration.sourceFile
|
|
82
|
+
};
|
|
83
|
+
let parentId = null;
|
|
84
|
+
let copiedEntries = 0;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
writeFileSync(temporaryFile, `${JSON.stringify(header)}\n`, { encoding: "utf8", flag: "wx" });
|
|
88
|
+
const notes = String(operationalNotes || "").trim();
|
|
89
|
+
if (notes) {
|
|
90
|
+
const entry = createEntry("custom_message", parentId, {
|
|
91
|
+
customType: "arisa-operational-notes",
|
|
92
|
+
content: notes,
|
|
93
|
+
display: false,
|
|
94
|
+
details: { source: "session-start" }
|
|
95
|
+
});
|
|
96
|
+
appendDurably(temporaryFile, entry);
|
|
97
|
+
parentId = entry.id;
|
|
98
|
+
}
|
|
99
|
+
const handoff = createEntry("custom_message", parentId, {
|
|
100
|
+
customType: "arisa-session-handoff",
|
|
101
|
+
content: [
|
|
102
|
+
"Automatic session migration before loading. Continue from this checkpoint:",
|
|
103
|
+
"",
|
|
104
|
+
migration.summary
|
|
105
|
+
].join("\n"),
|
|
106
|
+
display: false,
|
|
107
|
+
details: { source: "preload-migration" }
|
|
108
|
+
});
|
|
109
|
+
appendDurably(temporaryFile, handoff);
|
|
110
|
+
parentId = handoff.id;
|
|
111
|
+
|
|
112
|
+
for (const sourceEntry of migration.contextEntries) {
|
|
113
|
+
if (!contextEntryTypes.has(sourceEntry.type)) continue;
|
|
114
|
+
const entry = migratedEntry(sourceEntry, parentId);
|
|
115
|
+
appendDurably(temporaryFile, entry);
|
|
116
|
+
parentId = entry.id;
|
|
117
|
+
copiedEntries += 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
syncFile(temporaryFile);
|
|
121
|
+
renameSync(temporaryFile, targetFile);
|
|
122
|
+
syncDirectory(sessionDir);
|
|
123
|
+
return {
|
|
124
|
+
sourceFile: migration.sourceFile,
|
|
125
|
+
sourceBytes: migration.sourceBytes,
|
|
126
|
+
targetFile,
|
|
127
|
+
targetBytes: statSync(targetFile).size,
|
|
128
|
+
copiedEntries
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync(temporaryFile);
|
|
133
|
+
} catch {}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function migrateRecentSessionBeforeLoad({
|
|
139
|
+
sessionDir,
|
|
140
|
+
cwd,
|
|
141
|
+
policy,
|
|
142
|
+
operationalNotes = ""
|
|
143
|
+
}) {
|
|
144
|
+
const normalized = normalizeSessionRotationPolicy(policy);
|
|
145
|
+
if (!normalized.enabled) return null;
|
|
146
|
+
const sourceFile = findMostRecentSessionFile(sessionDir, cwd);
|
|
147
|
+
if (!sourceFile) return null;
|
|
148
|
+
const sourceBytes = statSync(sourceFile).size;
|
|
149
|
+
if (sourceBytes <= normalized.maxPersistedBytes) return null;
|
|
150
|
+
const migration = inspectSessionForPreloadMigration(sourceFile, normalized.maxPersistedBytes);
|
|
151
|
+
if (!migration) {
|
|
152
|
+
const error = new Error("Oversized Pi session could not be migrated safely before loading");
|
|
153
|
+
error.code = "PI_SESSION_PRELOAD_MIGRATION_UNAVAILABLE";
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
return createPreloadMigrationChild({
|
|
157
|
+
sessionDir,
|
|
158
|
+
cwd,
|
|
159
|
+
migration,
|
|
160
|
+
operationalNotes
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const defaultCompactAtPersistedBytes = 24 * 1024 * 1024;
|
|
2
|
+
const defaultMaxPersistedBytes = 32 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
export function normalizeSessionRotationPolicy(policy = {}) {
|
|
5
|
+
const maxPersistedBytes = Math.max(1, Number(policy?.maxPersistedBytes) || defaultMaxPersistedBytes);
|
|
6
|
+
return {
|
|
7
|
+
enabled: policy?.enabled !== false,
|
|
8
|
+
compactAtPersistedBytes: Math.min(
|
|
9
|
+
maxPersistedBytes,
|
|
10
|
+
Math.max(1, Number(policy?.compactAtPersistedBytes) || defaultCompactAtPersistedBytes)
|
|
11
|
+
),
|
|
12
|
+
maxPersistedBytes
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function compactionRotationRequest(event, persistedBytes, policy = {}) {
|
|
17
|
+
const normalized = normalizeSessionRotationPolicy(policy);
|
|
18
|
+
if (!normalized.enabled || event?.type !== "compaction_end" || event.aborted || event.errorMessage) return null;
|
|
19
|
+
const summary = String(event.result?.summary || "").trim();
|
|
20
|
+
if (!summary || Math.max(0, Number(persistedBytes) || 0) <= normalized.compactAtPersistedBytes) return null;
|
|
21
|
+
return {
|
|
22
|
+
handoff: [
|
|
23
|
+
"Automatic session rotation after compaction. Continue from this checkpoint:",
|
|
24
|
+
"",
|
|
25
|
+
summary
|
|
26
|
+
].join("\n"),
|
|
27
|
+
persistedBytes: Math.max(0, Number(persistedBytes) || 0)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import v8 from "node:v8";
|
|
2
|
+
|
|
3
|
+
const defaults = Object.freeze({
|
|
4
|
+
enabled: true,
|
|
5
|
+
softPercent: 70,
|
|
6
|
+
criticalPercent: 82,
|
|
7
|
+
waitMs: 15_000,
|
|
8
|
+
pollMs: 500
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function boundedPercent(value, fallback) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
return Number.isFinite(number) ? Math.min(99, Math.max(1, number)) : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function boundedMilliseconds(value, fallback, minimum, maximum) {
|
|
17
|
+
const number = Number(value);
|
|
18
|
+
return Number.isFinite(number) ? Math.min(maximum, Math.max(minimum, Math.floor(number))) : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeConfig(config = {}) {
|
|
22
|
+
const softPercent = boundedPercent(config.softPercent, defaults.softPercent);
|
|
23
|
+
return {
|
|
24
|
+
enabled: config.enabled !== false,
|
|
25
|
+
softPercent,
|
|
26
|
+
criticalPercent: Math.max(softPercent + 1, boundedPercent(config.criticalPercent, defaults.criticalPercent)),
|
|
27
|
+
waitMs: boundedMilliseconds(config.waitMs, defaults.waitMs, 0, 120_000),
|
|
28
|
+
pollMs: boundedMilliseconds(config.pollMs, defaults.pollMs, 50, 5_000)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readWorkerHeapPressure({ memoryUsage = process.memoryUsage, heapStatistics = v8.getHeapStatistics } = {}) {
|
|
33
|
+
const heapUsed = Math.max(0, Number(memoryUsage().heapUsed) || 0);
|
|
34
|
+
const heapLimit = Math.max(1, Number(heapStatistics().heap_size_limit) || 1);
|
|
35
|
+
return {
|
|
36
|
+
heapUsed,
|
|
37
|
+
heapLimit,
|
|
38
|
+
percent: heapUsed / heapLimit * 100
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class WorkerHeapPressureError extends Error {
|
|
43
|
+
constructor(pressure) {
|
|
44
|
+
super(`Worker heap pressure remained critical at ${pressure.percent.toFixed(1)}%; retry this request shortly`);
|
|
45
|
+
this.name = "WorkerHeapPressureError";
|
|
46
|
+
this.code = "WORKER_HEAP_PRESSURE";
|
|
47
|
+
this.retryable = true;
|
|
48
|
+
this.pressure = pressure;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class WorkerHeapCircuitBreaker {
|
|
53
|
+
constructor({ lifecycle, logger, config, measure = readWorkerHeapPressure, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now }) {
|
|
54
|
+
this.lifecycle = lifecycle;
|
|
55
|
+
this.logger = logger;
|
|
56
|
+
this.measure = measure;
|
|
57
|
+
this.sleep = sleep;
|
|
58
|
+
this.now = now;
|
|
59
|
+
this.metrics = {
|
|
60
|
+
pressureEvents: 0,
|
|
61
|
+
evictedSessions: 0,
|
|
62
|
+
delayedAdmissions: 0,
|
|
63
|
+
rejectedAdmissions: 0,
|
|
64
|
+
peakHeapPercent: 0
|
|
65
|
+
};
|
|
66
|
+
this.setConfig(config);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setConfig(config) {
|
|
70
|
+
this.config = normalizeConfig(config);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sample() {
|
|
74
|
+
const pressure = this.measure();
|
|
75
|
+
this.metrics.peakHeapPercent = Math.max(this.metrics.peakHeapPercent, pressure.percent);
|
|
76
|
+
return pressure;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async relievePressure() {
|
|
80
|
+
const evicted = await this.lifecycle.evictInactive();
|
|
81
|
+
this.metrics.evictedSessions += evicted.length;
|
|
82
|
+
return evicted;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async admit() {
|
|
86
|
+
let pressure = this.sample();
|
|
87
|
+
if (!this.config.enabled || pressure.percent < this.config.softPercent) return pressure;
|
|
88
|
+
|
|
89
|
+
this.metrics.pressureEvents += 1;
|
|
90
|
+
const evicted = await this.relievePressure();
|
|
91
|
+
pressure = this.sample();
|
|
92
|
+
this.logger?.log("agent", `worker heap pressure ${pressure.percent.toFixed(1)}%; evicted ${evicted.length} inactive session(s)`);
|
|
93
|
+
if (pressure.percent < this.config.criticalPercent) return pressure;
|
|
94
|
+
|
|
95
|
+
this.metrics.delayedAdmissions += 1;
|
|
96
|
+
const deadline = this.now() + this.config.waitMs;
|
|
97
|
+
while (this.now() < deadline) {
|
|
98
|
+
await this.sleep(Math.min(this.config.pollMs, Math.max(0, deadline - this.now())));
|
|
99
|
+
await this.relievePressure();
|
|
100
|
+
pressure = this.sample();
|
|
101
|
+
if (pressure.percent < this.config.criticalPercent) {
|
|
102
|
+
this.logger?.log("agent", `worker heap pressure recovered to ${pressure.percent.toFixed(1)}%`);
|
|
103
|
+
return pressure;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.metrics.rejectedAdmissions += 1;
|
|
108
|
+
this.logger?.log("agent", `worker heap admission rejected at ${pressure.percent.toFixed(1)}%`);
|
|
109
|
+
throw new WorkerHeapPressureError(pressure);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getDiagnostic() {
|
|
113
|
+
const pressure = this.sample();
|
|
114
|
+
return {
|
|
115
|
+
...this.config,
|
|
116
|
+
heapUsed: pressure.heapUsed,
|
|
117
|
+
heapLimit: pressure.heapLimit,
|
|
118
|
+
currentHeapPercent: pressure.percent,
|
|
119
|
+
...this.metrics
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const defaults = Object.freeze({
|
|
2
|
+
enabled: true,
|
|
3
|
+
maxConcurrent: 2,
|
|
4
|
+
pressureConcurrent: 1,
|
|
5
|
+
serializePercent: 60
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
9
|
+
const number = Number(value);
|
|
10
|
+
return Number.isFinite(number) ? Math.min(maximum, Math.max(minimum, Math.floor(number))) : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function boundedPercent(value, fallback) {
|
|
14
|
+
const number = Number(value);
|
|
15
|
+
return Number.isFinite(number) ? Math.min(99, Math.max(1, number)) : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeToolFanoutConfig(config = {}) {
|
|
19
|
+
const maxConcurrent = boundedInteger(config.maxConcurrent, defaults.maxConcurrent, 1, 16);
|
|
20
|
+
return {
|
|
21
|
+
enabled: config.enabled !== false,
|
|
22
|
+
maxConcurrent,
|
|
23
|
+
pressureConcurrent: Math.min(
|
|
24
|
+
maxConcurrent,
|
|
25
|
+
boundedInteger(config.pressureConcurrent, defaults.pressureConcurrent, 1, 16)
|
|
26
|
+
),
|
|
27
|
+
serializePercent: boundedPercent(config.serializePercent, defaults.serializePercent)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class WorkerToolFanoutController {
|
|
32
|
+
constructor({ heapCircuitBreaker, logger, config = {} }) {
|
|
33
|
+
this.heapCircuitBreaker = heapCircuitBreaker;
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
this.active = 0;
|
|
36
|
+
this.queue = [];
|
|
37
|
+
this.draining = false;
|
|
38
|
+
this.metrics = {
|
|
39
|
+
peakActive: 0,
|
|
40
|
+
peakQueued: 0,
|
|
41
|
+
pressureSerializations: 0,
|
|
42
|
+
rejectedAdmissions: 0,
|
|
43
|
+
completed: 0
|
|
44
|
+
};
|
|
45
|
+
this.setConfig(config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setConfig(config = {}) {
|
|
49
|
+
this.config = normalizeToolFanoutConfig(config);
|
|
50
|
+
this.drain();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
capacity() {
|
|
54
|
+
if (!this.config.enabled) return Number.POSITIVE_INFINITY;
|
|
55
|
+
const pressure = this.heapCircuitBreaker.sample();
|
|
56
|
+
if (pressure.percent >= this.config.serializePercent) {
|
|
57
|
+
this.metrics.pressureSerializations += 1;
|
|
58
|
+
return this.config.pressureConcurrent;
|
|
59
|
+
}
|
|
60
|
+
return this.config.maxConcurrent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async drain() {
|
|
64
|
+
if (this.draining) return;
|
|
65
|
+
this.draining = true;
|
|
66
|
+
try {
|
|
67
|
+
while (this.queue.length && this.active < this.capacity()) {
|
|
68
|
+
const job = this.queue.shift();
|
|
69
|
+
try {
|
|
70
|
+
await this.heapCircuitBreaker.admit();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.metrics.rejectedAdmissions += 1;
|
|
73
|
+
job.reject(error);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
this.active += 1;
|
|
77
|
+
this.metrics.peakActive = Math.max(this.metrics.peakActive, this.active);
|
|
78
|
+
job.resolve();
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
this.draining = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
acquire() {
|
|
86
|
+
if (!this.config.enabled) return Promise.resolve();
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
this.queue.push({ resolve, reject });
|
|
89
|
+
this.metrics.peakQueued = Math.max(this.metrics.peakQueued, this.queue.length);
|
|
90
|
+
this.drain();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
release() {
|
|
95
|
+
if (this.config.enabled) this.active = Math.max(0, this.active - 1);
|
|
96
|
+
this.metrics.completed += 1;
|
|
97
|
+
this.drain();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async run(work) {
|
|
101
|
+
await this.acquire();
|
|
102
|
+
try {
|
|
103
|
+
return await work();
|
|
104
|
+
} finally {
|
|
105
|
+
this.release();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getDiagnostic() {
|
|
110
|
+
return {
|
|
111
|
+
...this.config,
|
|
112
|
+
active: this.active,
|
|
113
|
+
queued: this.queue.length,
|
|
114
|
+
...this.metrics
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copyFile, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { getChatArtifactsDir, getChatArtifactsIndexFile } from "../../
|
|
4
|
+
import { getChatArtifactsDir, getChatArtifactsIndexFile } from "../../platform/paths.js";
|
|
5
5
|
|
|
6
6
|
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
7
7
|
const indexOperations = new Map();
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getChatToolTmpDir,
|
|
6
6
|
getToolStateDir,
|
|
7
7
|
getToolTmpDir
|
|
8
|
-
} from "../../
|
|
8
|
+
} from "../../platform/paths.js";
|
|
9
9
|
import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
|
|
10
10
|
import { installBundledOfficialTool } from "../tools/official-tool-installer.js";
|
|
11
11
|
import { searchOfficialToolCatalog } from "../tools/official-tool-catalog.js";
|
|
@@ -71,11 +71,12 @@ export function selectScheduledTasks(tasks = [], { status, limit = defaultSchedu
|
|
|
71
71
|
maxScheduledTaskListLimit
|
|
72
72
|
);
|
|
73
73
|
const allTasks = Array.isArray(tasks) ? tasks : [];
|
|
74
|
+
const activeStatuses = new Set(["pending", "running", "blocked_auth"]);
|
|
74
75
|
const orderedTasks = status
|
|
75
76
|
? [...allTasks].reverse()
|
|
76
77
|
: [
|
|
77
|
-
...allTasks.filter((task) =>
|
|
78
|
-
...allTasks.filter((task) =>
|
|
78
|
+
...allTasks.filter((task) => activeStatuses.has(task.status)).reverse(),
|
|
79
|
+
...allTasks.filter((task) => !activeStatuses.has(task.status)).reverse()
|
|
79
80
|
];
|
|
80
81
|
const visibleTasks = orderedTasks.slice(0, resolvedLimit);
|
|
81
82
|
return {
|