arisa 5.1.68 → 5.2.7
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 +7 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +46 -6
- package/src/core/agent/agent-session-lifecycle.js +80 -3
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +1 -1
- package/src/core/config/config-defaults.js +30 -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-store.js +1 -1
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +19 -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 +99 -187
- 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 +188 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +424 -50
- 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 +6 -3
- 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/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- 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 +1 -1
- 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 +42 -320
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/telegram-prompt-controller.js +346 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +36 -1
- package/test/doctor.test.js +22 -0
- package/test/memory-pressure.test.js +36 -0
- package/test/model-selection.test.js +11 -1
- package/test/official-tool-dependencies.test.js +1 -1
- 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 +9 -0
- package/test/service-manager.test.js +6 -1
- package/test/telegram-prompt-controller.test.js +81 -0
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +108 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +97 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- package/test-fixtures/fake-daemon.js +5 -0
package/README.md
CHANGED
|
@@ -113,8 +113,11 @@ Managed daemons become ready only after their tool-defined health operation succ
|
|
|
113
113
|
Daemon tools may opt into the `arisa-daemon-v1` local protocol for immediate
|
|
114
114
|
multiplexed jobs and incremental NDJSON events over a capability-protected local
|
|
115
115
|
socket. The runtime persists request, accepted and terminal records so a restart
|
|
116
|
-
can recover queued work and will not silently repeat an accepted effect.
|
|
117
|
-
|
|
116
|
+
can recover queued work and will not silently repeat an accepted effect. When a
|
|
117
|
+
client deadline expires, the runtime sends a scoped cancellation signal to that
|
|
118
|
+
job before closing the request; cooperative tools can stop it without restarting
|
|
119
|
+
the shared daemon or interrupting unrelated sessions. Legacy daemon tools
|
|
120
|
+
continue to use the existing request-file contract.
|
|
118
121
|
|
|
119
122
|
### Arisa Master and Slave
|
|
120
123
|
|
|
@@ -181,9 +184,9 @@ arisa --silent # run without verbose logs
|
|
|
181
184
|
|
|
182
185
|
Authorized Telegram chats can run the same safe service lifecycle with `/restart`.
|
|
183
186
|
|
|
184
|
-
Background mode runs the Telegram/Pi worker under a lightweight supervisor. Unexpected worker exits use bounded exponential restart backoff. Scheduled agent tasks are serialized FIFO per conversation while different conversations remain independent; execution deadlines default to 15 minutes for scheduled prompts and 5 minutes for agent events. A timed-out turn is marked outcome-uncertain and is never replayed automatically. These policies can be overridden with `service.workerRestart*` and `tasks.*TimeoutMs` in the Arisa config.
|
|
187
|
+
Background mode runs the Telegram/Pi worker under a lightweight supervisor. Unexpected worker exits use bounded exponential restart backoff. After recovery, authorized Telegram chats automatically receive a bounded report with the classified exit cause, recent tool names and counts, uncertain scheduled executions, restart delay, and running version; prompts and private payloads are never included. Scheduled agent tasks are serialized FIFO per conversation while different conversations remain independent; execution deadlines default to 15 minutes for scheduled prompts and 5 minutes for agent events. A timed-out turn is marked outcome-uncertain and is never replayed automatically. These policies can be overridden with `service.workerRestart*` and `tasks.*TimeoutMs` in the Arisa config.
|
|
185
188
|
|
|
186
|
-
Tools may
|
|
189
|
+
Tools may opt into weighted resource governance with a manifest declaration such as `"execution": { "resourceClass": "browser", "weight": 1 }`; undeclared lightweight and nested orchestration calls remain unconstrained. Declared runs queue when their class reaches its concurrency capacity. A global memory broker also reserves RAM across declared classes, leaving configurable system and core reserves before it admits work. The broker starts declared tools with a 384 MiB recommendation, derives their V8 heap from the granted memory, and raises the recommendation after an isolated memory-limit failure. Manifest values for `maxHeapMb` and `maxMemoryMb` act as ceilings; `maxOutputBytes` bounds protocol output. On systemd-based Linux hosts, each declared process tree runs in `arisa-tools.slice` with `MemoryHigh`, `MemoryMax`, bounded swap, and a higher OOM-kill priority than the core. Tool daemons receive the same OOM priority. Arisa lowers the core OOM score when the service account permits it. A limit failure stays inside the tool process tree and returns an uncertain result. Set host reserves and dynamic grants with `toolExecution.systemReserveMb`, `coreReserveMb`, `initialToolMemoryMb`, `minimumToolMemoryMb`, and `maximumToolMemoryMb`. Heap, soft-pressure, and swap controls use `toolHeapPercent`, `toolMemoryHighPercent`, and `toolSwapMaxMb`. Admission still checks `maxWorkerRssMb` and `maxSwapUsedPercent`. Class controls remain available through `defaultCapacity`, `capacities`, and `maxQueuedPerClass`. `deduplicateConcurrent: true` joins exact concurrent duplicates from the same chat.
|
|
187
190
|
|
|
188
191
|
Runtime model override (current process only):
|
|
189
192
|
|
package/package.json
CHANGED
|
@@ -8,11 +8,12 @@ import { buildPiToolPolicy } from "./core-tools.js";
|
|
|
8
8
|
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
9
9
|
import { clampModelThinkingLevel } from "./pi-runtime.js";
|
|
10
10
|
import { clampModelSpeed, createModelSpeedController } from "./model-speed.js";
|
|
11
|
-
import { arisaHomeDir } from "../../
|
|
11
|
+
import { arisaHomeDir } from "../../platform/paths.js";
|
|
12
12
|
import { AgentSessionLifecycle } from "./agent-session-lifecycle.js";
|
|
13
13
|
import { createPiCapabilityTools } from "./pi-capability-tools.js";
|
|
14
14
|
import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
|
|
15
15
|
import { materializeToolOutput } from "../tools/tool-output-materializer.js";
|
|
16
|
+
import { WorkerHeapCircuitBreaker } from "./worker-heap-circuit-breaker.js";
|
|
16
17
|
|
|
17
18
|
const piValidationTimeoutMs = 60_000;
|
|
18
19
|
const arisaToolNames = [
|
|
@@ -154,7 +155,13 @@ export class AgentManager {
|
|
|
154
155
|
this.resourceNotes = new ToolResourceNoteStore();
|
|
155
156
|
this.sessionLifecycle = new AgentSessionLifecycle({
|
|
156
157
|
logger,
|
|
157
|
-
summarizeContext: summarizeRetainedContext
|
|
158
|
+
summarizeContext: summarizeRetainedContext,
|
|
159
|
+
cachePolicy: config.pi.sessionCache
|
|
160
|
+
});
|
|
161
|
+
this.heapCircuitBreaker = new WorkerHeapCircuitBreaker({
|
|
162
|
+
lifecycle: this.sessionLifecycle,
|
|
163
|
+
logger,
|
|
164
|
+
config: config.pi.heapCircuitBreaker
|
|
158
165
|
});
|
|
159
166
|
this.sessions = this.sessionLifecycle.sessions;
|
|
160
167
|
this.pendingNewSessions = this.sessionLifecycle.pendingNewSessions;
|
|
@@ -188,6 +195,8 @@ export class AgentManager {
|
|
|
188
195
|
|
|
189
196
|
setConfig(config) {
|
|
190
197
|
this.sessionLifecycle.resetConfigState();
|
|
198
|
+
this.sessionLifecycle.setCachePolicy(config.pi.sessionCache);
|
|
199
|
+
this.heapCircuitBreaker.setConfig(config.pi.heapCircuitBreaker);
|
|
191
200
|
this.config = config;
|
|
192
201
|
}
|
|
193
202
|
|
|
@@ -209,14 +218,44 @@ export class AgentManager {
|
|
|
209
218
|
}
|
|
210
219
|
}
|
|
211
220
|
|
|
212
|
-
getRuntimeDiagnostic() {
|
|
213
|
-
|
|
221
|
+
async getRuntimeDiagnostic() {
|
|
222
|
+
const diagnostic = await this.sessionLifecycle.getDiagnostic();
|
|
223
|
+
return {
|
|
224
|
+
...diagnostic,
|
|
225
|
+
heapCircuitBreaker: this.heapCircuitBreaker.getDiagnostic()
|
|
226
|
+
};
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
createSessionManager(chatId, workspaceDir = arisaInstallDir, sessionRevision = 0) {
|
|
217
230
|
return this.sessionLifecycle.createSessionManager(chatId, workspaceDir, sessionRevision);
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
async estimatePersistedSessionBytes(session) {
|
|
234
|
+
const sessionStats = session?.getSessionStats?.();
|
|
235
|
+
const sessionFile = session?.sessionManager?.getSessionFile?.() || sessionStats?.sessionFile;
|
|
236
|
+
if (!sessionFile) return 0;
|
|
237
|
+
try {
|
|
238
|
+
return (await stat(sessionFile)).size;
|
|
239
|
+
} catch {
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async acquireSessionContext(sessionKey, context) {
|
|
245
|
+
const persistedBytes = await this.estimatePersistedSessionBytes(context.session);
|
|
246
|
+
this.sessionLifecycle.acquireCached(sessionKey, persistedBytes);
|
|
247
|
+
context.release = () => this.releaseSessionContext(sessionKey, context);
|
|
248
|
+
await this.sessionLifecycle.enforceCachePolicy({ protectedSessionKeys: [sessionKey] });
|
|
249
|
+
return context;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async releaseSessionContext(sessionKey, context) {
|
|
253
|
+
if (this.sessions.get(String(sessionKey)) !== context) return;
|
|
254
|
+
const persistedBytes = await this.estimatePersistedSessionBytes(context.session);
|
|
255
|
+
this.sessionLifecycle.releaseCached(sessionKey, persistedBytes);
|
|
256
|
+
await this.sessionLifecycle.enforceCachePolicy();
|
|
257
|
+
}
|
|
258
|
+
|
|
220
259
|
async validatePiAgent(config = this.config) {
|
|
221
260
|
this.logger?.log("agent", "validating Pi session");
|
|
222
261
|
const { authStorage, modelRegistry } = createPiRuntime({
|
|
@@ -254,6 +293,7 @@ export class AgentManager {
|
|
|
254
293
|
}
|
|
255
294
|
|
|
256
295
|
async getSessionContext(chatId, telegram, { scopeChatId = chatId, accessGuard = async () => {} } = {}) {
|
|
296
|
+
await this.heapCircuitBreaker.admit();
|
|
257
297
|
const sessionKey = String(chatId);
|
|
258
298
|
const modelSelection = resolveChatModelSelection(this.config, sessionKey);
|
|
259
299
|
const effectiveModelId = modelSelection.model;
|
|
@@ -274,7 +314,7 @@ export class AgentManager {
|
|
|
274
314
|
existing.telegramTarget.current = telegram;
|
|
275
315
|
existing.accessGuardTarget.current = accessGuard;
|
|
276
316
|
this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
|
|
277
|
-
return existing;
|
|
317
|
+
return this.acquireSessionContext(sessionKey, existing);
|
|
278
318
|
}
|
|
279
319
|
this.logger?.log("agent", `model changed for chat ${sessionKey}: ${existing?.modelKey || "unknown"} -> ${effectiveModelKey}; recreating session`);
|
|
280
320
|
this.closeCachedSession(sessionKey);
|
|
@@ -366,7 +406,7 @@ export class AgentManager {
|
|
|
366
406
|
};
|
|
367
407
|
this.sessions.set(sessionKey, ctx);
|
|
368
408
|
if (isNewSession) this.sessionLifecycle.completeNewSession(sessionKey);
|
|
369
|
-
return ctx;
|
|
409
|
+
return this.acquireSessionContext(sessionKey, ctx);
|
|
370
410
|
}
|
|
371
411
|
|
|
372
412
|
async getAvailableModels(chatId) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { getChatPiSessionsDir, sessionStartOperationalNotesFile } from "../../
|
|
3
|
+
import { getChatPiSessionsDir, sessionStartOperationalNotesFile } from "../../platform/paths.js";
|
|
4
4
|
import { arisaInstallDir } from "./runtime-context.js";
|
|
5
5
|
|
|
6
6
|
const operationalNoteMaxChars = 220;
|
|
@@ -41,13 +41,84 @@ function closeAgentSession(session) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export class AgentSessionLifecycle {
|
|
44
|
-
constructor({ logger, summarizeContext }) {
|
|
44
|
+
constructor({ logger, summarizeContext, cachePolicy = {} }) {
|
|
45
45
|
this.logger = logger;
|
|
46
46
|
this.summarizeContext = summarizeContext;
|
|
47
47
|
this.sessions = new Map();
|
|
48
48
|
this.pendingNewSessions = new Set();
|
|
49
49
|
this.pendingSessionHandoffs = new Map();
|
|
50
50
|
this.sessionClosePromises = new Map();
|
|
51
|
+
this.setCachePolicy(cachePolicy);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setCachePolicy(cachePolicy = {}) {
|
|
55
|
+
this.cachePolicy = {
|
|
56
|
+
maxSessions: Math.max(1, Number(cachePolicy.maxSessions) || 3),
|
|
57
|
+
maxPersistedBytes: Math.max(1, Number(cachePolicy.maxPersistedBytes) || 48 * 1024 * 1024)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
acquireCached(sessionKey, persistedBytes = 0) {
|
|
62
|
+
const context = this.sessions.get(String(sessionKey));
|
|
63
|
+
if (!context) return;
|
|
64
|
+
context.activeUsers = (context.activeUsers || 0) + 1;
|
|
65
|
+
context.lastAccessedAt = Date.now();
|
|
66
|
+
context.persistedBytes = Math.max(0, Number(persistedBytes) || 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
releaseCached(sessionKey, persistedBytes = 0) {
|
|
70
|
+
const context = this.sessions.get(String(sessionKey));
|
|
71
|
+
if (!context) return;
|
|
72
|
+
context.activeUsers = Math.max(0, (context.activeUsers || 0) - 1);
|
|
73
|
+
context.lastAccessedAt = Date.now();
|
|
74
|
+
context.persistedBytes = Math.max(0, Number(persistedBytes) || 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
cacheUsage() {
|
|
78
|
+
return {
|
|
79
|
+
sessions: this.sessions.size,
|
|
80
|
+
persistedBytes: [...this.sessions.values()].reduce((total, context) => total + (context.persistedBytes || 0), 0)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cacheOverLimit() {
|
|
85
|
+
const usage = this.cacheUsage();
|
|
86
|
+
return usage.sessions > this.cachePolicy.maxSessions
|
|
87
|
+
|| usage.persistedBytes > this.cachePolicy.maxPersistedBytes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
evictionCandidate(protectedKeys = new Set()) {
|
|
91
|
+
return [...this.sessions.entries()]
|
|
92
|
+
.filter(([key, context]) => !protectedKeys.has(key) && !(context.activeUsers > 0) && !context.session?.isStreaming)
|
|
93
|
+
.sort((left, right) => (left[1].lastAccessedAt || 0) - (right[1].lastAccessedAt || 0))[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async evictInactive({ protectedSessionKeys = [] } = {}) {
|
|
97
|
+
const protectedKeys = new Set(protectedSessionKeys.map(String));
|
|
98
|
+
const evicted = [];
|
|
99
|
+
let candidate = this.evictionCandidate(protectedKeys);
|
|
100
|
+
while (candidate) {
|
|
101
|
+
const [sessionKey, context] = candidate;
|
|
102
|
+
evicted.push({ sessionKey, persistedBytes: context.persistedBytes || 0 });
|
|
103
|
+
this.logger?.log("agent", `evicting inactive Pi session for chat ${sessionKey} from resident cache`);
|
|
104
|
+
await this.closeCached(sessionKey);
|
|
105
|
+
candidate = this.evictionCandidate(protectedKeys);
|
|
106
|
+
}
|
|
107
|
+
return evicted;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async enforceCachePolicy({ protectedSessionKeys = [] } = {}) {
|
|
111
|
+
const protectedKeys = new Set(protectedSessionKeys.map(String));
|
|
112
|
+
const evicted = [];
|
|
113
|
+
while (this.cacheOverLimit()) {
|
|
114
|
+
const candidate = this.evictionCandidate(protectedKeys);
|
|
115
|
+
if (!candidate) break;
|
|
116
|
+
const [sessionKey, context] = candidate;
|
|
117
|
+
evicted.push({ sessionKey, persistedBytes: context.persistedBytes || 0 });
|
|
118
|
+
this.logger?.log("agent", `evicting inactive Pi session for chat ${sessionKey} from resident cache`);
|
|
119
|
+
await this.closeCached(sessionKey);
|
|
120
|
+
}
|
|
121
|
+
return evicted;
|
|
51
122
|
}
|
|
52
123
|
|
|
53
124
|
closeCached(sessionKey) {
|
|
@@ -147,7 +218,12 @@ export class AgentSessionLifecycle {
|
|
|
147
218
|
|
|
148
219
|
async getDiagnostic() {
|
|
149
220
|
const contexts = await Promise.all([...this.sessions.entries()].map(async ([chatId, context]) => {
|
|
150
|
-
const base = {
|
|
221
|
+
const base = {
|
|
222
|
+
chatId,
|
|
223
|
+
activeUsers: context.activeUsers || 0,
|
|
224
|
+
persistedBytes: context.persistedBytes || 0,
|
|
225
|
+
lastAccessedAt: context.lastAccessedAt || null
|
|
226
|
+
};
|
|
151
227
|
try {
|
|
152
228
|
const stats = context.session.getSessionStats();
|
|
153
229
|
const retained = this.summarizeContext(context.session.messages);
|
|
@@ -166,6 +242,7 @@ export class AgentSessionLifecycle {
|
|
|
166
242
|
harness: "pi",
|
|
167
243
|
sessions: this.sessions.size,
|
|
168
244
|
closingSessions: this.sessionClosePromises.size,
|
|
245
|
+
cache: { ...this.cachePolicy, ...this.cacheUsage() },
|
|
169
246
|
contexts
|
|
170
247
|
};
|
|
171
248
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { piAuthFile } from "../../
|
|
2
|
+
import { piAuthFile } from "../../platform/paths.js";
|
|
3
3
|
|
|
4
4
|
export function createPiOAuthLogin({ provider, onAuth, onDeviceCode, onPrompt, onProgress, onSelect } = {}) {
|
|
5
5
|
const authStorage = AuthStorage.create(piAuthFile);
|
|
@@ -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,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
|
+
}
|
|
@@ -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";
|
|
@@ -19,7 +19,17 @@ export const daemonConfigDefaults = Object.freeze({
|
|
|
19
19
|
export const toolExecutionConfigDefaults = Object.freeze({
|
|
20
20
|
defaultCapacity: 2,
|
|
21
21
|
maxQueuedPerClass: 100,
|
|
22
|
-
|
|
22
|
+
maxWorkerRssMb: 384,
|
|
23
|
+
maxSwapUsedPercent: 95,
|
|
24
|
+
initialToolMemoryMb: 384,
|
|
25
|
+
minimumToolMemoryMb: 128,
|
|
26
|
+
maximumToolMemoryMb: 4096,
|
|
27
|
+
systemReserveMb: 128,
|
|
28
|
+
coreReserveMb: 384,
|
|
29
|
+
toolHeapPercent: 65,
|
|
30
|
+
toolMemoryHighPercent: 85,
|
|
31
|
+
toolSwapMaxMb: 128,
|
|
32
|
+
capacities: Object.freeze({ browser: 1, orchestrator: 1 })
|
|
23
33
|
});
|
|
24
34
|
|
|
25
35
|
export const telegramConfigDefaults = Object.freeze({
|
|
@@ -58,6 +68,17 @@ export const taskConfigDefaults = Object.freeze({
|
|
|
58
68
|
export const piConfigDefaults = Object.freeze({
|
|
59
69
|
thinkingLevel: "medium",
|
|
60
70
|
speed: 1,
|
|
71
|
+
sessionCache: Object.freeze({
|
|
72
|
+
maxSessions: 3,
|
|
73
|
+
maxPersistedBytes: 48 * 1024 * 1024
|
|
74
|
+
}),
|
|
75
|
+
heapCircuitBreaker: Object.freeze({
|
|
76
|
+
enabled: true,
|
|
77
|
+
softPercent: 70,
|
|
78
|
+
criticalPercent: 82,
|
|
79
|
+
waitMs: 15_000,
|
|
80
|
+
pollMs: 500
|
|
81
|
+
}),
|
|
61
82
|
compaction: Object.freeze({
|
|
62
83
|
enabled: true,
|
|
63
84
|
reserveTokens: 120_000,
|
|
@@ -108,6 +129,14 @@ export function applyConfigDefaults(config) {
|
|
|
108
129
|
pi: {
|
|
109
130
|
...piConfigDefaults,
|
|
110
131
|
...configuredPi,
|
|
132
|
+
sessionCache: {
|
|
133
|
+
...piConfigDefaults.sessionCache,
|
|
134
|
+
...(configuredPi.sessionCache || {})
|
|
135
|
+
},
|
|
136
|
+
heapCircuitBreaker: {
|
|
137
|
+
...piConfigDefaults.heapCircuitBreaker,
|
|
138
|
+
...(configuredPi.heapCircuitBreaker || {})
|
|
139
|
+
},
|
|
111
140
|
compaction: {
|
|
112
141
|
...piConfigDefaults.compaction,
|
|
113
142
|
...(configuredPi.compaction || {})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { configFile } from "../../
|
|
3
|
+
import { configFile } from "../../platform/paths.js";
|
|
4
4
|
import { applyConfigDefaults } from "./config-defaults.js";
|
|
5
5
|
|
|
6
6
|
export function prepareConfigForSave(config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { tasksFile } from "../../
|
|
4
|
+
import { tasksFile } from "../../platform/paths.js";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_RETRY = Object.freeze({
|
|
7
7
|
maxAttempts: 3,
|