agentlas 1.0.65 → 1.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
|
@@ -32,42 +32,93 @@ const AUTOMATION_HEADING = "## Automation";
|
|
|
32
32
|
const ASK_OPEN = "<<agentlas-ask>>";
|
|
33
33
|
const ASK_CLOSE = "<</agentlas-ask>>";
|
|
34
34
|
|
|
35
|
+
// Fence payloads are model-authored input. Keep parsing and downstream
|
|
36
|
+
// side-effect loops bounded; oversized entries are refused, not truncated.
|
|
37
|
+
const MAX_FENCE_JSON_BYTES = 256 * 1024;
|
|
38
|
+
const MAX_DELEGATIONS = 64;
|
|
39
|
+
const MAX_AUTOMATIONS = 64;
|
|
40
|
+
const MAX_ASKS = 32;
|
|
41
|
+
const MAX_STEPS = 32;
|
|
42
|
+
const MAX_DELEGATE_ENTRY_BYTES = 16 * 1024;
|
|
43
|
+
const MAX_AUTOMATION_ENTRY_BYTES = 32 * 1024;
|
|
44
|
+
const MAX_STEPS_BYTES = 64 * 1024;
|
|
45
|
+
const MAX_TARGET_BYTES = 256;
|
|
46
|
+
const MAX_BRIEF_BYTES = 8 * 1024;
|
|
47
|
+
const MAX_NAME_BYTES = 256;
|
|
48
|
+
const MAX_PROMPT_BYTES = 16 * 1024;
|
|
49
|
+
const MAX_AGENT_BYTES = 256;
|
|
50
|
+
const MAX_SCHEDULE_BYTES = 512;
|
|
51
|
+
const MAX_ASK_BODY_BYTES = 64 * 1024;
|
|
52
|
+
|
|
53
|
+
function utf8Bytes(value) {
|
|
54
|
+
return Buffer.byteLength(String(value || ""), "utf8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function boundedString(value, maxBytes) {
|
|
58
|
+
if (typeof value !== "string") return "";
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
return utf8Bytes(trimmed) <= maxBytes ? trimmed : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function jsonBytes(value) {
|
|
64
|
+
try { return utf8Bytes(JSON.stringify(value)); } catch { return Infinity; }
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
/* ── ## Delegate (delegate.ts parseDelegations 포팅) ─────────────────────── */
|
|
36
68
|
|
|
37
69
|
function parseDelegateBlock(text) {
|
|
38
70
|
const idx = text.lastIndexOf(DELEGATE_HEADING);
|
|
39
|
-
if (idx < 0) return { delegations: [], synthesisAllocation: null, cleanedText: text.trim() };
|
|
71
|
+
if (idx < 0) return { delegations: [], synthesisAllocation: null, cleanedText: text.trim(), errors: [] };
|
|
40
72
|
|
|
41
73
|
const after = text.slice(idx + DELEGATE_HEADING.length);
|
|
42
74
|
const fence = after.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
43
75
|
let delegations = [];
|
|
44
76
|
let synthesisAllocation = null;
|
|
77
|
+
const errors = [];
|
|
45
78
|
if (fence) {
|
|
46
|
-
|
|
47
|
-
|
|
79
|
+
const jsonText = fence[1].trim();
|
|
80
|
+
if (utf8Bytes(jsonText) > MAX_FENCE_JSON_BYTES) {
|
|
81
|
+
errors.push("Delegate JSON exceeds " + MAX_FENCE_JSON_BYTES + " bytes");
|
|
82
|
+
} else try {
|
|
83
|
+
const data = JSON.parse(jsonText);
|
|
48
84
|
const rawDelegations = Array.isArray(data)
|
|
49
85
|
? data
|
|
50
86
|
: data && typeof data === "object" && Array.isArray(data.delegations)
|
|
51
87
|
? data.delegations
|
|
52
88
|
: [];
|
|
53
|
-
if (
|
|
54
|
-
|
|
89
|
+
if (rawDelegations.length > MAX_DELEGATIONS) {
|
|
90
|
+
errors.push("Delegate block exceeds maximum of " + MAX_DELEGATIONS + " entries");
|
|
91
|
+
} else {
|
|
92
|
+
if (!Array.isArray(data) && data && typeof data === "object" && data.synthesis) {
|
|
93
|
+
synthesisAllocation = workloadRouting.normalizeAllocation(data.synthesis, "synthesize");
|
|
94
|
+
}
|
|
95
|
+
delegations = rawDelegations
|
|
96
|
+
.map((d, index) => {
|
|
97
|
+
if (!d || typeof d !== "object" || Array.isArray(d)) return null;
|
|
98
|
+
if (jsonBytes(d) > MAX_DELEGATE_ENTRY_BYTES) {
|
|
99
|
+
errors.push("Delegate entry " + (index + 1) + " exceeds " + MAX_DELEGATE_ENTRY_BYTES + " bytes");
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const target = boundedString(d.target, MAX_TARGET_BYTES);
|
|
103
|
+
const brief = boundedString(d.brief, MAX_BRIEF_BYTES);
|
|
104
|
+
if (target === null || brief === null) {
|
|
105
|
+
errors.push("Delegate entry " + (index + 1) + " contains an oversized string");
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Desktop 계약: target 만 필수. allocation 은 정규화 실패 시 null 로 남고
|
|
109
|
+
// (레거시 텍스트 위임과 동일) 실행 측이 현재 모델 폴백 영수증을 남긴다.
|
|
110
|
+
return target
|
|
111
|
+
? { target, brief, allocation: workloadRouting.normalizeAllocation(d.allocation, "delegate") }
|
|
112
|
+
: null;
|
|
113
|
+
})
|
|
114
|
+
.filter(Boolean);
|
|
55
115
|
}
|
|
56
|
-
delegations = rawDelegations
|
|
57
|
-
.map((d) => {
|
|
58
|
-
if (!d || typeof d !== "object") return null;
|
|
59
|
-
const target = typeof d.target === "string" ? d.target.trim() : "";
|
|
60
|
-
const brief = typeof d.brief === "string" ? d.brief.trim() : "";
|
|
61
|
-
// Desktop 계약: target 만 필수. allocation 은 정규화 실패 시 null 로 남고
|
|
62
|
-
// (레거시 텍스트 위임과 동일) 실행 측이 현재 모델 폴백 영수증을 남긴다.
|
|
63
|
-
return target
|
|
64
|
-
? { target, brief, allocation: workloadRouting.normalizeAllocation(d.allocation, "delegate") }
|
|
65
|
-
: null;
|
|
66
|
-
})
|
|
67
|
-
.filter(Boolean);
|
|
68
116
|
} catch {
|
|
69
117
|
delegations = [];
|
|
118
|
+
errors.push("Delegate JSON parse failed");
|
|
70
119
|
}
|
|
120
|
+
} else {
|
|
121
|
+
errors.push("Delegate heading present but no JSON fence found");
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
let cut = text.length;
|
|
@@ -77,7 +128,7 @@ function parseDelegateBlock(text) {
|
|
|
77
128
|
cut = idx; // 펜스 없으면 dangling heading 도 제거 (Desktop 과 동일)
|
|
78
129
|
}
|
|
79
130
|
const cleanedText = (text.slice(0, idx) + text.slice(cut)).trim();
|
|
80
|
-
return { delegations, synthesisAllocation, cleanedText };
|
|
131
|
+
return { delegations, synthesisAllocation, cleanedText, errors };
|
|
81
132
|
}
|
|
82
133
|
|
|
83
134
|
/* ── ## Automation (automation-emitter.ts parseAutomations 포팅, 정직 거부판) ── */
|
|
@@ -182,28 +233,58 @@ function parseAutomationBlock(text) {
|
|
|
182
233
|
let automations = [];
|
|
183
234
|
|
|
184
235
|
if (fence) {
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
const jsonText = fence[1].trim();
|
|
237
|
+
if (utf8Bytes(jsonText) > MAX_FENCE_JSON_BYTES) {
|
|
238
|
+
errors.push("Automation JSON exceeds " + MAX_FENCE_JSON_BYTES + " bytes");
|
|
239
|
+
} else try {
|
|
240
|
+
const data = JSON.parse(jsonText);
|
|
187
241
|
if (Array.isArray(data)) {
|
|
188
|
-
|
|
189
|
-
.
|
|
242
|
+
if (data.length > MAX_AUTOMATIONS) {
|
|
243
|
+
errors.push("Automation block exceeds maximum of " + MAX_AUTOMATIONS + " entries");
|
|
244
|
+
} else {
|
|
245
|
+
automations = data
|
|
246
|
+
.map((d, index) => {
|
|
190
247
|
if (!d || typeof d !== "object") {
|
|
191
248
|
errors.push("Automation entry was not an object");
|
|
192
249
|
return null;
|
|
193
250
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
251
|
+
if (jsonBytes(d) > MAX_AUTOMATION_ENTRY_BYTES) {
|
|
252
|
+
errors.push("Automation entry " + (index + 1) + " exceeds " + MAX_AUTOMATION_ENTRY_BYTES + " bytes");
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const name = boundedString(d.name, MAX_NAME_BYTES);
|
|
256
|
+
const prompt = boundedString(d.prompt, MAX_PROMPT_BYTES);
|
|
257
|
+
const agent = boundedString(d.agent, MAX_AGENT_BYTES);
|
|
258
|
+
const hubAgent = boundedString(d.hubAgent, MAX_AGENT_BYTES);
|
|
259
|
+
if (name === null || prompt === null || agent === null || hubAgent === null) {
|
|
260
|
+
errors.push("Automation entry " + (index + 1) + " contains an oversized string");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
198
263
|
if (!name || !prompt) {
|
|
199
264
|
errors.push(`Automation "${name || "(unnamed)"}" missing name/prompt`);
|
|
200
265
|
return null;
|
|
201
266
|
}
|
|
267
|
+
if (typeof d.schedule === "string" && utf8Bytes(d.schedule.trim()) > MAX_SCHEDULE_BYTES) {
|
|
268
|
+
errors.push("Automation " + name + " schedule exceeds " + MAX_SCHEDULE_BYTES + " bytes");
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (d.schedule && typeof d.schedule === "object" && jsonBytes(d.schedule) > MAX_SCHEDULE_BYTES) {
|
|
272
|
+
errors.push("Automation " + name + " schedule exceeds " + MAX_SCHEDULE_BYTES + " bytes");
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
202
275
|
const resolved = resolveScheduleHonest(d.schedule, errors);
|
|
203
276
|
if (!resolved) return null; // 정직 거부 — 스케줄 추측 금지
|
|
204
277
|
// steps[] 는 wire 패리티로 보존만 한다. Desktop 은 stepsToGraph 로
|
|
205
278
|
// 그래프를 합성하지만 터미널 데몬은 prompt_template 단일 실행이다 —
|
|
206
279
|
// 그래프 합성을 위장하지 않는다(비합성은 apply 쪽 영수증에 표기).
|
|
280
|
+
if (Array.isArray(d.steps) && d.steps.length > MAX_STEPS) {
|
|
281
|
+
errors.push("Automation " + name + " exceeds maximum of " + MAX_STEPS + " steps");
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(d.steps) && jsonBytes(d.steps) > MAX_STEPS_BYTES) {
|
|
285
|
+
errors.push("Automation " + name + " steps exceed " + MAX_STEPS_BYTES + " bytes");
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
207
288
|
const steps = Array.isArray(d.steps) && d.steps.length
|
|
208
289
|
? d.steps.filter((s) => !!s && typeof s === "object")
|
|
209
290
|
: undefined;
|
|
@@ -218,6 +299,7 @@ function parseAutomationBlock(text) {
|
|
|
218
299
|
};
|
|
219
300
|
})
|
|
220
301
|
.filter(Boolean);
|
|
302
|
+
}
|
|
221
303
|
} else {
|
|
222
304
|
errors.push("Automation block was not a JSON array");
|
|
223
305
|
}
|
|
@@ -241,6 +323,7 @@ function parseAutomationBlock(text) {
|
|
|
241
323
|
/* ── <<agentlas-ask>> (renderer/lib/ask-question.ts extractQuestions 포팅) ── */
|
|
242
324
|
|
|
243
325
|
function tryParseAsk(body) {
|
|
326
|
+
if (utf8Bytes(body) > MAX_ASK_BODY_BYTES) return null;
|
|
244
327
|
// body 가 ```json 펜스로 감싸진 경우도 허용 (Desktop confirm/index.ts:77-80 동일)
|
|
245
328
|
const stripped = body
|
|
246
329
|
.replace(/^```(?:json)?\s*/i, "")
|
|
@@ -297,7 +380,7 @@ function extractAsks(text) {
|
|
|
297
380
|
break;
|
|
298
381
|
}
|
|
299
382
|
const parsed = tryParseAsk(afterOpen.slice(0, close).trim());
|
|
300
|
-
if (parsed) asks.push(parsed);
|
|
383
|
+
if (parsed && asks.length < MAX_ASKS) asks.push(parsed);
|
|
301
384
|
// 파싱 실패해도 닫힌 펜스는 본문에서 제거 — malformed JSON raw 를 사용자에게 노출하지 않는다.
|
|
302
385
|
remaining = afterOpen.slice(close + ASK_CLOSE.length);
|
|
303
386
|
}
|
|
@@ -328,6 +411,7 @@ function parseReplyFences(text) {
|
|
|
328
411
|
// 2. Delegate
|
|
329
412
|
const del = parseDelegateBlock(working);
|
|
330
413
|
working = del.cleanedText;
|
|
414
|
+
errors.push(...(del.errors || []));
|
|
331
415
|
|
|
332
416
|
// 3. Automation
|
|
333
417
|
const auto = parseAutomationBlock(working);
|
|
@@ -64,6 +64,52 @@ function curatorRuntimeEnv() {
|
|
|
64
64
|
return env;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
const OLLAMA_CURATOR_TIMEOUT_MS = 120_000;
|
|
68
|
+
const OLLAMA_JUDGMENT_TIMEOUT_MS = 60_000;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* API-backed memory decisions run after the main Session turn, so Session's
|
|
72
|
+
* provider abort slot is no longer active. Keep this boundary self-contained:
|
|
73
|
+
* the race releases the turn even if a custom/fake fetch ignores AbortSignal,
|
|
74
|
+
* while the signal lets a real fetch stop immediately on timeout.
|
|
75
|
+
*/
|
|
76
|
+
async function runBoundedOllama(session, system, prompt, operation, timeoutMs) {
|
|
77
|
+
const limit = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
78
|
+
? Number(timeoutMs)
|
|
79
|
+
: OLLAMA_JUDGMENT_TIMEOUT_MS;
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
let timer = null;
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
let timeoutError = null;
|
|
84
|
+
const call = Promise.resolve().then(() => capture.runApi(
|
|
85
|
+
"ollama",
|
|
86
|
+
session.runtime.model,
|
|
87
|
+
system,
|
|
88
|
+
prompt,
|
|
89
|
+
{ signal: controller.signal },
|
|
90
|
+
));
|
|
91
|
+
const deadline = new Promise((_, reject) => {
|
|
92
|
+
timer = setTimeout(() => {
|
|
93
|
+
timedOut = true;
|
|
94
|
+
timeoutError = new Error(`Ollama ${operation} timed out after ${limit}ms.`);
|
|
95
|
+
timeoutError.code = "AGENTLAS_MEMORY_JUDGMENT_TIMEOUT";
|
|
96
|
+
try { controller.abort(timeoutError); } catch { /* already aborted */ }
|
|
97
|
+
reject(timeoutError);
|
|
98
|
+
}, limit);
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([call, deadline]);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (timedOut) throw timeoutError;
|
|
104
|
+
throw error;
|
|
105
|
+
} finally {
|
|
106
|
+
if (timer) clearTimeout(timer);
|
|
107
|
+
if (!controller.signal.aborted) {
|
|
108
|
+
try { controller.abort(new Error(`Ollama ${operation} finished`)); } catch { /* already aborted */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
function ensureGeminiNoToolsPolicy() {
|
|
68
114
|
const dir = curatorRuntimeDir();
|
|
69
115
|
const file = path.join(dir, "gemini-no-tools-policy.toml");
|
|
@@ -101,7 +147,7 @@ async function invokeCurator(session, payload, systemPrompt) {
|
|
|
101
147
|
throw new Error("Memory Curator payload failed the pre-invocation privacy gate");
|
|
102
148
|
}
|
|
103
149
|
if (session.runtime.kind === "ollama") {
|
|
104
|
-
return
|
|
150
|
+
return runBoundedOllama(session, systemPrompt, serialized, "memory curator", OLLAMA_CURATOR_TIMEOUT_MS);
|
|
105
151
|
}
|
|
106
152
|
return capture.captureRuntime(session.runtime.kind, systemPrompt, serialized, {
|
|
107
153
|
cwd: curatorRuntimeDir(),
|
|
@@ -146,7 +192,13 @@ async function resolveSessionTaskSignatures(session, prompt) {
|
|
|
146
192
|
].join("\n");
|
|
147
193
|
let raw;
|
|
148
194
|
if (session.runtime.kind === "ollama") {
|
|
149
|
-
raw = await
|
|
195
|
+
raw = await runBoundedOllama(
|
|
196
|
+
session,
|
|
197
|
+
system,
|
|
198
|
+
String(prompt || ""),
|
|
199
|
+
"task judgment",
|
|
200
|
+
OLLAMA_JUDGMENT_TIMEOUT_MS,
|
|
201
|
+
);
|
|
150
202
|
} else {
|
|
151
203
|
raw = await capture.captureRuntime(session.runtime.kind, system, String(prompt || ""), {
|
|
152
204
|
cwd: curatorRuntimeDir(),
|
|
@@ -183,7 +235,13 @@ async function judgeGlobalMemoryAuthorization(session, promptText) {
|
|
|
183
235
|
].join("\n");
|
|
184
236
|
let raw;
|
|
185
237
|
if (session.runtime.kind === "ollama") {
|
|
186
|
-
raw = await
|
|
238
|
+
raw = await runBoundedOllama(
|
|
239
|
+
session,
|
|
240
|
+
system,
|
|
241
|
+
String(promptText || ""),
|
|
242
|
+
"global-memory judgment",
|
|
243
|
+
OLLAMA_JUDGMENT_TIMEOUT_MS,
|
|
244
|
+
);
|
|
187
245
|
} else {
|
|
188
246
|
raw = await capture.captureRuntime(session.runtime.kind, system, String(promptText || ""), {
|
|
189
247
|
cwd: curatorRuntimeDir(),
|
|
@@ -242,4 +300,5 @@ module.exports = {
|
|
|
242
300
|
beginSessionMemoryTurn,
|
|
243
301
|
completeSessionMemoryTurn,
|
|
244
302
|
resolveSessionTaskSignatures,
|
|
303
|
+
runBoundedOllama,
|
|
245
304
|
};
|
|
@@ -84,7 +84,17 @@ class Orchestrator extends EventEmitter {
|
|
|
84
84
|
|
|
85
85
|
runningCount() {
|
|
86
86
|
let n = 0;
|
|
87
|
-
for (const s of this.sessions.values())
|
|
87
|
+
for (const s of this.sessions.values()) {
|
|
88
|
+
if (s.isBusy()) {
|
|
89
|
+
n += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// kill() fences the session immediately, but native/API cancellation is
|
|
93
|
+
// asynchronous. Keep the slot occupied until Session has released its
|
|
94
|
+
// child or abort controller; otherwise a replacement turn can start
|
|
95
|
+
// while the killed provider is still consuming a process/slot.
|
|
96
|
+
if (s.status === "killed" && (s._child || s._apiAbort)) n += 1;
|
|
97
|
+
}
|
|
88
98
|
return n;
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -31,6 +31,63 @@ const TERMINAL_MEMORY_CORE = [
|
|
|
31
31
|
].join("\n");
|
|
32
32
|
const MEMORY_DETAIL_RE = /\b(?:remember|memory|save this|record this|memory event)\b|기억|메모리|저장해|기록해|남겨/i;
|
|
33
33
|
const CREDENTIAL_INDEX_RE = /\b(?:deploy|release|billing|auth|oauth|credential|api key|secret key|cloud)\b|배포|릴리스|출시|결제|인증|자격 증명|API\s*키|시크릿|클라우드/i;
|
|
34
|
+
const PROJECT_SOUL_MAX_CHARS = 1_800;
|
|
35
|
+
const ONE_STATE_MAX_CHARS = 64 * 1024;
|
|
36
|
+
|
|
37
|
+
function sameFileIdentity(left, right) {
|
|
38
|
+
return left && right && String(left.dev) === String(right.dev) && String(left.ino) === String(right.ino);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read a small local prompt file through one descriptor. The lstat/fstat
|
|
43
|
+
* identity check detects a path swap between the check and open; O_NOFOLLOW
|
|
44
|
+
* rejects symlink aliases, and the second fstat detects a concurrent rewrite.
|
|
45
|
+
* The byte cap is deliberately larger than the character cap for UTF-8 while
|
|
46
|
+
* still bounding the amount retained before decoding and slicing.
|
|
47
|
+
*/
|
|
48
|
+
function readBoundedText(file, maxChars) {
|
|
49
|
+
const charLimit = Math.max(0, Number(maxChars) || 0);
|
|
50
|
+
const byteLimit = Math.max(1, charLimit * 4 + 4);
|
|
51
|
+
const expected = fs.lstatSync(file);
|
|
52
|
+
if (!expected.isFile() || expected.isSymbolicLink() || expected.nlink !== 1) {
|
|
53
|
+
throw new Error(`prompt file is not a single-link regular file: ${file}`);
|
|
54
|
+
}
|
|
55
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
56
|
+
let fd = null;
|
|
57
|
+
try {
|
|
58
|
+
fd = fs.openSync(file, flags);
|
|
59
|
+
const opened = fs.fstatSync(fd);
|
|
60
|
+
if (!opened.isFile() || opened.nlink !== 1 || !sameFileIdentity(expected, opened)) {
|
|
61
|
+
throw new Error(`prompt file identity changed before read: ${file}`);
|
|
62
|
+
}
|
|
63
|
+
const size = Number.isSafeInteger(Number(opened.size)) ? Number(opened.size) : byteLimit;
|
|
64
|
+
const readLimit = Math.min(size, byteLimit);
|
|
65
|
+
const data = Buffer.allocUnsafe(readLimit);
|
|
66
|
+
let offset = 0;
|
|
67
|
+
while (offset < readLimit) {
|
|
68
|
+
const count = fs.readSync(fd, data, offset, readLimit - offset, null);
|
|
69
|
+
if (!count) break;
|
|
70
|
+
offset += count;
|
|
71
|
+
}
|
|
72
|
+
const after = fs.fstatSync(fd);
|
|
73
|
+
const changed = !sameFileIdentity(opened, after)
|
|
74
|
+
|| after.nlink !== 1
|
|
75
|
+
|| Number(after.size) !== Number(opened.size)
|
|
76
|
+
|| (after.mtimeNs != null && opened.mtimeNs != null
|
|
77
|
+
? String(after.mtimeNs) !== String(opened.mtimeNs)
|
|
78
|
+
: after.mtimeMs !== opened.mtimeMs);
|
|
79
|
+
if (changed) throw new Error(`prompt file changed while reading: ${file}`);
|
|
80
|
+
const text = data.subarray(0, offset).toString("utf8");
|
|
81
|
+
return {
|
|
82
|
+
text: text.slice(0, charLimit),
|
|
83
|
+
truncated: offset < Number(opened.size) || text.length > charLimit,
|
|
84
|
+
};
|
|
85
|
+
} finally {
|
|
86
|
+
if (fd != null) {
|
|
87
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
34
91
|
|
|
35
92
|
function approximatePromptTokens(text) {
|
|
36
93
|
return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
|
|
@@ -130,11 +187,10 @@ function cliMemoryContext(db, projectPath, agentId = null, task = "") {
|
|
|
130
187
|
if (projectPath) {
|
|
131
188
|
try {
|
|
132
189
|
const soulPath = path.join(projectPath, arch.memoryDir || ".agentlas", arch.soulFile || "project-soul-memory.md");
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
190
|
+
const bounded = readBoundedText(soulPath, PROJECT_SOUL_MAX_CHARS);
|
|
191
|
+
let s = bounded.text;
|
|
192
|
+
if (bounded.truncated) s += "\n…(truncated)";
|
|
193
|
+
if (s.trim()) sections.push(`### Project memory (${projectPath})\n${s.trim()}`);
|
|
138
194
|
} catch { /* ignore */ }
|
|
139
195
|
const contextSlice = cliProjectContextSlice(projectPath, task);
|
|
140
196
|
if (contextSlice) sections.push(contextSlice);
|
|
@@ -188,13 +244,13 @@ function loadOneDirective() {
|
|
|
188
244
|
try {
|
|
189
245
|
const root = process.env.AGENTLAS_ONE_DIR
|
|
190
246
|
|| require("node:path").join(require("node:os").homedir(), ".agentlas", "one");
|
|
191
|
-
const fs = require("node:fs");
|
|
192
247
|
const path = require("node:path");
|
|
193
|
-
const state = JSON.parse(
|
|
248
|
+
const state = JSON.parse(readBoundedText(path.join(root, "state.json"), ONE_STATE_MAX_CHARS).text);
|
|
194
249
|
if (!state || state.on !== true) return "";
|
|
195
|
-
const
|
|
250
|
+
const bounded = readBoundedText(path.join(root, "directive.md"), ONE_DIRECTIVE_MAX_CHARS);
|
|
251
|
+
const text = bounded.text.trim();
|
|
196
252
|
// 상한을 둔다 — 지시문이 남의 토큰 예산을 잠식하면 안 된다(메모리 예산과 같은 규칙).
|
|
197
|
-
return text
|
|
253
|
+
return text;
|
|
198
254
|
} catch {
|
|
199
255
|
return "";
|
|
200
256
|
}
|
|
@@ -251,4 +307,5 @@ module.exports = {
|
|
|
251
307
|
cliProjectContextSlice,
|
|
252
308
|
augmentSystem,
|
|
253
309
|
loadGlobalConnectionSkill,
|
|
310
|
+
readBoundedText,
|
|
254
311
|
};
|
|
@@ -15,12 +15,44 @@ const nativeHost = require("../agentlas-native-host.cjs");
|
|
|
15
15
|
const permissions = require("../agentlas-permissions.cjs");
|
|
16
16
|
const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
|
|
17
17
|
const { CLI_EXECUTABLE_KINDS } = require("../runtimes/resolve.cjs");
|
|
18
|
+
const ACP_RUNTIME_KINDS = new Set(require("../runtimes/kinds.cjs").ACP_CLI_KINDS);
|
|
18
19
|
const { roleMembers } = require("../runtimes/roles.cjs");
|
|
19
20
|
const { EventSink } = require("./sink.cjs");
|
|
20
21
|
const store = require("./store.cjs");
|
|
21
22
|
const memoryTurn = require("./memory-turn.cjs");
|
|
22
23
|
|
|
23
24
|
const RING_LIMIT = 2000;
|
|
25
|
+
const RING_BYTE_LIMIT = 8 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
function eventBytes(event) {
|
|
28
|
+
try { return Buffer.byteLength(JSON.stringify(event), "utf8"); }
|
|
29
|
+
catch { return 1024; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Provider resume ids belong to the model that created them. Effort and other
|
|
34
|
+
* per-turn controls can safely change on the same thread, but a model switch
|
|
35
|
+
* must start a fresh provider session and let Agentlas history preserve the
|
|
36
|
+
* visible conversation. Otherwise the CLI receives `--model new` together
|
|
37
|
+
* with `--resume old-session` and the dashboard selection is not authoritative.
|
|
38
|
+
*/
|
|
39
|
+
function runtimeSessionFingerprint(runtime, agent, permission) {
|
|
40
|
+
const digest = crypto.createHash("sha256")
|
|
41
|
+
.update("agentlas-terminal-runtime-session.v2\0")
|
|
42
|
+
.update(String((runtime && runtime.kind) || ""))
|
|
43
|
+
.update("\0model\0")
|
|
44
|
+
.update(String((runtime && runtime.model) || ""))
|
|
45
|
+
.update("\0agent\0")
|
|
46
|
+
.update(String((agent && agent.id) || ""))
|
|
47
|
+
.update("\0prompt\0")
|
|
48
|
+
.update(String((agent && agent.systemPrompt) || ""));
|
|
49
|
+
// ACP chooses its session mode at session/new. Unlike native Codex/Claude
|
|
50
|
+
// flags, a later turn cannot safely retrofit that mode onto an old id.
|
|
51
|
+
if (ACP_RUNTIME_KINDS.has(runtime && runtime.kind)) {
|
|
52
|
+
digest.update("\0permission\0").update(permissions.normalize(permission));
|
|
53
|
+
}
|
|
54
|
+
return digest.digest("hex");
|
|
55
|
+
}
|
|
24
56
|
|
|
25
57
|
class Session extends EventEmitter {
|
|
26
58
|
/**
|
|
@@ -48,6 +80,7 @@ class Session extends EventEmitter {
|
|
|
48
80
|
this.queue = [];
|
|
49
81
|
this._child = null;
|
|
50
82
|
this._events = [];
|
|
83
|
+
this._eventBytes = 0;
|
|
51
84
|
this._turnPromise = null;
|
|
52
85
|
// 계약 테스트용 spawn 주입(runNativeTurn의 req.spawn). 프로덕션 경로에선 null.
|
|
53
86
|
this._spawnImpl = opts.spawnImpl || null;
|
|
@@ -73,9 +106,7 @@ class Session extends EventEmitter {
|
|
|
73
106
|
} catch { /* kind 열이 없는 구형 DB — user 취급(레거시 NULL=user 계약) */ }
|
|
74
107
|
}
|
|
75
108
|
|
|
76
|
-
this.fingerprint =
|
|
77
|
-
.update(`${this.runtime.kind}\n${this.agent.id}\n${this.agent.systemPrompt || ""}`)
|
|
78
|
-
.digest("hex");
|
|
109
|
+
this.fingerprint = runtimeSessionFingerprint(this.runtime, this.agent, this.permission);
|
|
79
110
|
this.runtimeSession = store.loadRuntimeSession(this.db, this.chatId, this.runtime.kind, this.fingerprint, this.agent.id);
|
|
80
111
|
|
|
81
112
|
this._sink = new EventSink({
|
|
@@ -91,7 +122,11 @@ class Session extends EventEmitter {
|
|
|
91
122
|
|
|
92
123
|
_record(ev) {
|
|
93
124
|
this._events.push(ev);
|
|
94
|
-
|
|
125
|
+
this._eventBytes += eventBytes(ev);
|
|
126
|
+
while (this._events.length > RING_LIMIT || this._eventBytes > RING_BYTE_LIMIT) {
|
|
127
|
+
const removed = this._events.shift();
|
|
128
|
+
this._eventBytes = Math.max(0, this._eventBytes - eventBytes(removed));
|
|
129
|
+
}
|
|
95
130
|
if (ev.type === "stream-delta") {
|
|
96
131
|
const tail = (this.lastLine + ev.text).split(/\r?\n/).filter((l) => l.trim());
|
|
97
132
|
this.lastLine = tail.length ? tail[tail.length - 1].slice(0, 200) : this.lastLine;
|
|
@@ -154,6 +189,10 @@ class Session extends EventEmitter {
|
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
_nextRecoveryRuntime() {
|
|
192
|
+
// Per-automation exact pins are authoritative. A provider/model failure is
|
|
193
|
+
// surfaced to the run receipt; it must never jump into a Dashboard role
|
|
194
|
+
// pool and make the unattended run use a different provider.
|
|
195
|
+
if (this.runtime.pinAuthoritative) return null;
|
|
157
196
|
const role = this.runtime.role === "worker" ? "worker" : "orchestrator";
|
|
158
197
|
const members = roleMembers(this.db, role);
|
|
159
198
|
const current = members.findIndex((member) =>
|
|
@@ -178,6 +217,10 @@ class Session extends EventEmitter {
|
|
|
178
217
|
}
|
|
179
218
|
|
|
180
219
|
async _runTurn(prompt) {
|
|
220
|
+
const runtimeSessionAtTurnStart = {
|
|
221
|
+
kind: this.runtime.kind,
|
|
222
|
+
id: this.runtimeSession && this.runtimeSession.id ? String(this.runtimeSession.id) : "",
|
|
223
|
+
};
|
|
181
224
|
this.status = "running";
|
|
182
225
|
this.startedAt = Date.now();
|
|
183
226
|
this.lastError = null;
|
|
@@ -304,6 +347,7 @@ class Session extends EventEmitter {
|
|
|
304
347
|
if (turnAbort) req.signal = turnAbort.signal;
|
|
305
348
|
if (this._spawnImpl) req.spawn = this._spawnImpl;
|
|
306
349
|
if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
|
|
350
|
+
let recoveryAbort = null;
|
|
307
351
|
try {
|
|
308
352
|
res = await nativeHost.runNativeTurn(req);
|
|
309
353
|
/*
|
|
@@ -312,16 +356,21 @@ class Session extends EventEmitter {
|
|
|
312
356
|
* 그래서 `!res.text`가 영원히 거짓이 되어, 예비 런타임이 등록돼 있는데도
|
|
313
357
|
* 복구가 한 번도 발화하지 않았다. 실패는 error가 말하고, text는 표시용이다.
|
|
314
358
|
*/
|
|
315
|
-
|
|
359
|
+
// A user kill is terminal. The killed child normally unwinds with an
|
|
360
|
+
// error marker; treating that marker as provider failure would launch
|
|
361
|
+
// the next priority model after the user explicitly pressed stop.
|
|
362
|
+
if (res && res.error && this.status !== "killed") {
|
|
316
363
|
const nextRuntime = this._nextRecoveryRuntime();
|
|
317
364
|
if (nextRuntime) {
|
|
318
365
|
const privateEvidence = [...this._privateRecoveryEvidence, String(res.error)]
|
|
319
366
|
.filter(Boolean).join("\n").slice(0, 12000);
|
|
320
367
|
this.runtime = nextRuntime;
|
|
321
368
|
this.runtimeSession = {};
|
|
322
|
-
this.fingerprint =
|
|
323
|
-
|
|
324
|
-
|
|
369
|
+
this.fingerprint = runtimeSessionFingerprint(nextRuntime, this.agent, this.permission);
|
|
370
|
+
if (ACP_RUNTIME_KINDS.has(nextRuntime.kind)) {
|
|
371
|
+
recoveryAbort = new AbortController();
|
|
372
|
+
this._apiAbort = recoveryAbort;
|
|
373
|
+
}
|
|
325
374
|
res = await nativeHost.runNativeTurn({
|
|
326
375
|
...req,
|
|
327
376
|
kind: nextRuntime.kind,
|
|
@@ -329,6 +378,8 @@ class Session extends EventEmitter {
|
|
|
329
378
|
model: nextRuntime.model,
|
|
330
379
|
effort: nextRuntime.effort,
|
|
331
380
|
session: {},
|
|
381
|
+
sessionFingerprintSeed: this.fingerprint,
|
|
382
|
+
...(recoveryAbort ? { signal: recoveryAbort.signal } : {}),
|
|
332
383
|
prompt: [
|
|
333
384
|
prompt,
|
|
334
385
|
"",
|
|
@@ -342,6 +393,7 @@ class Session extends EventEmitter {
|
|
|
342
393
|
} catch (e) {
|
|
343
394
|
res = { text: "", session: req.session, error: (e && e.message) || String(e) };
|
|
344
395
|
} finally {
|
|
396
|
+
if (recoveryAbort && this._apiAbort === recoveryAbort) this._apiAbort = null;
|
|
345
397
|
if (turnAbort && this._apiAbort === turnAbort) this._apiAbort = null;
|
|
346
398
|
}
|
|
347
399
|
}
|
|
@@ -396,7 +448,15 @@ class Session extends EventEmitter {
|
|
|
396
448
|
if (persistText) store.appendMessage(this.db, this.chatId, "assistant", persistText);
|
|
397
449
|
if (res && res.session && res.session.id) {
|
|
398
450
|
this.runtimeSession = { id: res.session.id };
|
|
399
|
-
store.saveRuntimeSession(
|
|
451
|
+
store.saveRuntimeSession(
|
|
452
|
+
this.db,
|
|
453
|
+
this.chatId,
|
|
454
|
+
this.runtime.kind,
|
|
455
|
+
this.runtimeSession,
|
|
456
|
+
this.fingerprint,
|
|
457
|
+
this.agent.id,
|
|
458
|
+
runtimeSessionAtTurnStart.kind === this.runtime.kind ? runtimeSessionAtTurnStart.id : "",
|
|
459
|
+
);
|
|
400
460
|
}
|
|
401
461
|
if (res && res.usage) this.usage = res.usage;
|
|
402
462
|
|
|
@@ -495,4 +555,4 @@ class Session extends EventEmitter {
|
|
|
495
555
|
}
|
|
496
556
|
}
|
|
497
557
|
|
|
498
|
-
module.exports = { Session };
|
|
558
|
+
module.exports = { Session, runtimeSessionFingerprint };
|