@turmind/halo 1.1.2 → 1.1.4
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/212-70bd2971392bde84.js +1 -0
- package/admin-out/_next/static/chunks/app/page-71c576815b4a30d9.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/agentcore.md +35 -0
- package/bundled-docs/design/cron.md +3 -3
- package/bundled-docs/design/goal-mode.md +1 -0
- package/bundled-docs/design/session.md +3 -0
- package/bundled-docs/dev/api.md +2 -2
- package/dist/index.js +207 -17
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +28 -0
- package/templates/skills/cron/SKILL.md +8 -0
- package/templates/skills/cron/manage-cron.py +27 -3
- package/templates/skills/skill/SKILL.md +1 -1
- package/templates/skills/web-search/SKILL.md +44 -0
- package/templates/skills/web-search/search.py +137 -0
- package/templates/skills/workspace/share.md +1 -1
- package/templates/skills/workspace/stage.py +1 -1
- package/admin-out/_next/static/chunks/212-8ac9dfe9c24e512c.js +0 -1
- package/admin-out/_next/static/chunks/app/page-2076ec04359bb395.js +0 -1
- package/templates/skills/nova-web-search/SKILL.md +0 -65
- /package/admin-out/_next/static/{bZcoAqHBQyOvzwS5c84EJ → PuVQROsIREQUt1b6EFWJR}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{bZcoAqHBQyOvzwS5c84EJ → PuVQROsIREQUt1b6EFWJR}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -2339,6 +2339,13 @@ function ensureHaloHome(haloHome2) {
|
|
|
2339
2339
|
fs7.rmSync(staleWsSkill, { recursive: true, force: true });
|
|
2340
2340
|
console.log("[Init] Removed stale `ws` skill (renamed to `workspace`)");
|
|
2341
2341
|
}
|
|
2342
|
+
for (const staleId of ["nova-web-search", "openai-web-search"]) {
|
|
2343
|
+
const staleDir = path8.join(globalDir2, "skills", staleId);
|
|
2344
|
+
if (fs7.existsSync(staleDir)) {
|
|
2345
|
+
fs7.rmSync(staleDir, { recursive: true, force: true });
|
|
2346
|
+
console.log(`[Init] Removed stale \`${staleId}\` skill (merged into \`web-search\`)`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2342
2349
|
const docsSrc = resolveDocsSource();
|
|
2343
2350
|
if (!docsSrc) {
|
|
2344
2351
|
console.log(`[Init] No docs source found \u2014 skipping platform docs seed. Searched: ${DOCS_SRC_CANDIDATES.join(", ")}`);
|
|
@@ -2400,7 +2407,7 @@ var init_init = __esm({
|
|
|
2400
2407
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2401
2408
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2402
2409
|
];
|
|
2403
|
-
TEMPLATE_VERSION =
|
|
2410
|
+
TEMPLATE_VERSION = 52;
|
|
2404
2411
|
VERSION_FILE = ".template-version";
|
|
2405
2412
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2406
2413
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -2434,7 +2441,7 @@ var init_init = __esm({
|
|
|
2434
2441
|
// Capability skills, model-invoked only (user-invocable: false — no slash
|
|
2435
2442
|
// command). Preinstalled because they're broadly useful out of the box.
|
|
2436
2443
|
"aws-knowledge",
|
|
2437
|
-
"
|
|
2444
|
+
"web-search",
|
|
2438
2445
|
// The agent's visual "face": teaches it that `.halo/canvas/self.html`
|
|
2439
2446
|
// (seeded per-workspace, see ensureWorkspaceHalo) is a live self-portrait
|
|
2440
2447
|
// it can drive in real time by emitting `<<<SHOW: …js… >>>`, which the admin
|
|
@@ -3324,7 +3331,12 @@ async function deliverGoalRound(host, worker) {
|
|
|
3324
3331
|
const activeChildren = db.select({ id: agentSessions.id }).from(agentSessions).where(and2(eq2(agentSessions.parentId, worker.id), isNull(agentSessions.stoppedAt), isNull(agentSessions.archivedAt))).all();
|
|
3325
3332
|
if (activeChildren.length > 0 || worker.messageQueue.length > 0)
|
|
3326
3333
|
return;
|
|
3327
|
-
|
|
3334
|
+
let report = worker.finalOutput || worker.output || "(no output)";
|
|
3335
|
+
if (worker.turnError) {
|
|
3336
|
+
report = `[WORKER ABORTED: this round was terminated by an unrecoverable error, NOT a normal wrap-up. Error: ${worker.turnError}. The text below is a partial trace of the aborted turn \u2014 judge this as an interrupted round, do not score it as a completed result. Re-dispatch with query_session to let the worker resume.]
|
|
3337
|
+
|
|
3338
|
+
${report}`;
|
|
3339
|
+
}
|
|
3328
3340
|
const caps = state.caps;
|
|
3329
3341
|
const currentSpecHash = hashFileOrNull(goalSpecPath(host.workspaceRoot, state.goalId));
|
|
3330
3342
|
if (!currentSpecHash || currentSpecHash !== state.specHash) {
|
|
@@ -5127,6 +5139,17 @@ var init_session_ui_store = __esm({
|
|
|
5127
5139
|
/** Last activity (event reduced / view built) per uiStates key — the idle
|
|
5128
5140
|
* sweep's eviction clock. Invariant: every uiStates key has an entry. */
|
|
5129
5141
|
uiStateTouched = /* @__PURE__ */ new Map();
|
|
5142
|
+
/** Roots whose in-memory UIState holds local mutations (event reduced in,
|
|
5143
|
+
* notification appended, log replaced) **not yet persisted** — marked on
|
|
5144
|
+
* every mutation, cleared once persistUIState lands the snapshot. A state
|
|
5145
|
+
* not in this set adds nothing to what's on disk: either it's a pure disk
|
|
5146
|
+
* seed (built to *view* a session another process may be driving — a cron
|
|
5147
|
+
* `halo cli` child shares the workspace's session files and keeps appending
|
|
5148
|
+
* after the seed), or its mutations were already flushed. The WS
|
|
5149
|
+
* detach/switch saves gate on `isUIStateDirty`: writing a clean state back
|
|
5150
|
+
* would overwrite the other process's newer messages with this process's
|
|
5151
|
+
* frozen snapshot (the cron-session UI-log truncation incident). */
|
|
5152
|
+
uiStateDirty = /* @__PURE__ */ new Set();
|
|
5130
5153
|
constructor(host) {
|
|
5131
5154
|
this.host = host;
|
|
5132
5155
|
this.db = host.getDb();
|
|
@@ -5223,6 +5246,7 @@ var init_session_ui_store = __esm({
|
|
|
5223
5246
|
reduceIntoUIState(rootId, event) {
|
|
5224
5247
|
try {
|
|
5225
5248
|
const state = this.ensureUIState(rootId);
|
|
5249
|
+
this.uiStateDirty.add(rootId);
|
|
5226
5250
|
const result = applyEvent(state, event);
|
|
5227
5251
|
if (result.subSessionSave) {
|
|
5228
5252
|
this.persistSubSession(state, rootId, result.subSessionSave);
|
|
@@ -5319,6 +5343,7 @@ var init_session_ui_store = __esm({
|
|
|
5319
5343
|
archiveCount: archive?.count,
|
|
5320
5344
|
archivedUserDelta: archive?.userDelta
|
|
5321
5345
|
});
|
|
5346
|
+
this.uiStateDirty.delete(rootId);
|
|
5322
5347
|
} catch (err) {
|
|
5323
5348
|
console.error(`[SessionUIStore] persistUIState failed for ${rootId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5324
5349
|
}
|
|
@@ -5438,6 +5463,14 @@ var init_session_ui_store = __esm({
|
|
|
5438
5463
|
getCachedUIState(rootSessionId) {
|
|
5439
5464
|
return this.uiStates.get(rootSessionId) ?? null;
|
|
5440
5465
|
}
|
|
5466
|
+
/** Whether the cached UIState holds local mutations not yet persisted.
|
|
5467
|
+
* False for a state that was only seeded from disk (view of a session
|
|
5468
|
+
* another process drives) or whose mutations already flushed — writing
|
|
5469
|
+
* such a state back is at best a no-op and at worst overwrites a fresher
|
|
5470
|
+
* file (see uiStateDirty). */
|
|
5471
|
+
isUIStateDirty(rootSessionId) {
|
|
5472
|
+
return this.uiStateDirty.has(rootSessionId);
|
|
5473
|
+
}
|
|
5441
5474
|
/**
|
|
5442
5475
|
* Get the UIState for a session, restoring it from disk if it isn't
|
|
5443
5476
|
* in memory yet. Returns null only when the session id doesn't exist
|
|
@@ -5470,6 +5503,7 @@ var init_session_ui_store = __esm({
|
|
|
5470
5503
|
timestamp: Date.now(),
|
|
5471
5504
|
agentName
|
|
5472
5505
|
});
|
|
5506
|
+
this.uiStateDirty.add(rootId);
|
|
5473
5507
|
this.persistUIState(rootId, state);
|
|
5474
5508
|
}
|
|
5475
5509
|
/**
|
|
@@ -5483,6 +5517,7 @@ var init_session_ui_store = __esm({
|
|
|
5483
5517
|
state.streamBuffer = "";
|
|
5484
5518
|
state.turnToolCalls = [];
|
|
5485
5519
|
state.turnContentBlocks = [];
|
|
5520
|
+
this.uiStateDirty.add(rootId);
|
|
5486
5521
|
this.persistUIState(rootId, state);
|
|
5487
5522
|
}
|
|
5488
5523
|
/**
|
|
@@ -5504,6 +5539,7 @@ var init_session_ui_store = __esm({
|
|
|
5504
5539
|
this.uiStates.delete(sessionId);
|
|
5505
5540
|
this.uiStateProjectPaths.delete(sessionId);
|
|
5506
5541
|
this.uiStateTouched.delete(sessionId);
|
|
5542
|
+
this.uiStateDirty.delete(sessionId);
|
|
5507
5543
|
}
|
|
5508
5544
|
/**
|
|
5509
5545
|
* Evict UIStates whose tree has been idle past the TTL. This is THE eviction
|
|
@@ -5560,8 +5596,10 @@ var init_session_ui_store = __esm({
|
|
|
5560
5596
|
* session evolving elsewhere would freeze on the first snapshot.
|
|
5561
5597
|
*/
|
|
5562
5598
|
prepareForView(sessionId, selfDriven) {
|
|
5563
|
-
if (!selfDriven)
|
|
5599
|
+
if (!selfDriven) {
|
|
5564
5600
|
this.uiStates.delete(sessionId);
|
|
5601
|
+
this.uiStateDirty.delete(sessionId);
|
|
5602
|
+
}
|
|
5565
5603
|
this.uiStateProjectPaths.set(sessionId, this.host.workspaceRoot);
|
|
5566
5604
|
return this.ensureUIState(sessionId);
|
|
5567
5605
|
}
|
|
@@ -5571,6 +5609,7 @@ var init_session_ui_store = __esm({
|
|
|
5571
5609
|
this.uiStates.delete(id);
|
|
5572
5610
|
this.uiStateProjectPaths.delete(id);
|
|
5573
5611
|
this.uiStateTouched.delete(id);
|
|
5612
|
+
this.uiStateDirty.delete(id);
|
|
5574
5613
|
const timer = this.persistTimers.get(id);
|
|
5575
5614
|
if (timer) {
|
|
5576
5615
|
clearTimeout(timer);
|
|
@@ -10392,6 +10431,7 @@ var init_session_manager = __esm({
|
|
|
10392
10431
|
draftReset,
|
|
10393
10432
|
output: "",
|
|
10394
10433
|
finalOutput: "",
|
|
10434
|
+
turnError: null,
|
|
10395
10435
|
promise: null,
|
|
10396
10436
|
abortController: null,
|
|
10397
10437
|
messageQueue: [],
|
|
@@ -10695,6 +10735,13 @@ var init_session_manager = __esm({
|
|
|
10695
10735
|
getCachedUIState(rootSessionId) {
|
|
10696
10736
|
return this.uiStore.getCachedUIState(rootSessionId);
|
|
10697
10737
|
}
|
|
10738
|
+
/** Whether a root's cached UIState holds local mutations not yet persisted.
|
|
10739
|
+
* The WS save paths gate on this — a clean state (pure disk seed, or
|
|
10740
|
+
* already flushed) must not be written back over a file that another
|
|
10741
|
+
* process (cron `halo cli` child) may have advanced since. */
|
|
10742
|
+
isUIStateDirty(rootSessionId) {
|
|
10743
|
+
return this.uiStore.isUIStateDirty(rootSessionId);
|
|
10744
|
+
}
|
|
10698
10745
|
getUIState(rootSessionId) {
|
|
10699
10746
|
return this.uiStore.getUIState(rootSessionId);
|
|
10700
10747
|
}
|
|
@@ -10802,6 +10849,7 @@ var init_session_manager = __esm({
|
|
|
10802
10849
|
const maxRetries = config.agent.maxRetries;
|
|
10803
10850
|
session.output = "";
|
|
10804
10851
|
session.finalOutput = "";
|
|
10852
|
+
session.turnError = null;
|
|
10805
10853
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
10806
10854
|
session.abortController = new AbortController();
|
|
10807
10855
|
const signal = session.abortController.signal;
|
|
@@ -10858,6 +10906,7 @@ var init_session_manager = __esm({
|
|
|
10858
10906
|
if (msg.includes("insufficient balance") || msg.includes("suspended") || msg.includes("invalid api key") || msg.includes("Invalid API Key") || msg.includes("Unauthorized") || msg.includes("authentication")) {
|
|
10859
10907
|
console.error(`[SessionManager] Session ${session.id} account error: ${msg}`);
|
|
10860
10908
|
this.emitEvent(session.id, { type: "error", error: msg, agentName: session.agentName, taskId: session.parentId ? session.id : void 0 });
|
|
10909
|
+
session.turnError = msg;
|
|
10861
10910
|
resultText = `Error: ${msg}`;
|
|
10862
10911
|
break;
|
|
10863
10912
|
}
|
|
@@ -10883,7 +10932,7 @@ var init_session_manager = __esm({
|
|
|
10883
10932
|
continue;
|
|
10884
10933
|
}
|
|
10885
10934
|
}
|
|
10886
|
-
if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
|
|
10935
|
+
if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("http2 request did not get a response") || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
|
|
10887
10936
|
if (attempt + 1 < maxRetries) {
|
|
10888
10937
|
const delay = 1e3 * Math.pow(2, attempt) + Math.random() * 500;
|
|
10889
10938
|
console.debug(`[SessionManager] Session ${session.id} transient network error, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries}): ${msg}`);
|
|
@@ -10926,6 +10975,7 @@ var init_session_manager = __esm({
|
|
|
10926
10975
|
const errDetail = [errName, httpStatus ? `HTTP ${httpStatus}` : ""].filter(Boolean).join(" ");
|
|
10927
10976
|
console.error(`[SessionManager] Session ${session.id} error (attempt ${attempt + 1}/${maxRetries})${errDetail ? ` [${errDetail}]` : ""}: ${msg}`);
|
|
10928
10977
|
this.emitEvent(session.id, { type: "error", error: msg, agentName: session.agentName, taskId: session.parentId ? session.id : void 0 });
|
|
10978
|
+
session.turnError = errName ? `${errName}: ${msg}` : msg;
|
|
10929
10979
|
resultText = `Error: ${errName ? `${errName}: ` : ""}${msg}`;
|
|
10930
10980
|
break;
|
|
10931
10981
|
}
|
|
@@ -10991,7 +11041,12 @@ var init_session_manager = __esm({
|
|
|
10991
11041
|
return;
|
|
10992
11042
|
}
|
|
10993
11043
|
this.db.update(agentSessions).set({ stoppedAt: Date.now() }).where(eq6(agentSessions.id, session.id)).run();
|
|
10994
|
-
|
|
11044
|
+
let result = session.finalOutput || session.output || "(no output)";
|
|
11045
|
+
if (session.turnError) {
|
|
11046
|
+
result = `[SUB-AGENT ABORTED: the last turn was terminated by an unrecoverable error, NOT completed. Error: ${session.turnError}. The text below is a partial trace of the aborted turn \u2014 do not treat it as a finished result. Re-dispatch with query_session("${session.id}", ...) to let it resume.]
|
|
11047
|
+
|
|
11048
|
+
${result}`;
|
|
11049
|
+
}
|
|
10995
11050
|
console.debug(`[SessionManager] Auto-report: ${session.id} \u2192 parent ${session.parentId} \u2014 result: ${result.slice(0, 150)}`);
|
|
10996
11051
|
const reportCap = config.limits.autoReportMax;
|
|
10997
11052
|
const truncatedReport = result.length > reportCap ? result.slice(0, reportCap) + `
|
|
@@ -20492,14 +20547,75 @@ var init_commands3 = __esm({
|
|
|
20492
20547
|
import { Hono as Hono13 } from "hono";
|
|
20493
20548
|
import path52 from "node:path";
|
|
20494
20549
|
import fs44 from "node:fs";
|
|
20550
|
+
function sanitizeRuntimeId(runtimeSessionId) {
|
|
20551
|
+
return runtimeSessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 80);
|
|
20552
|
+
}
|
|
20495
20553
|
function haloSessionId(runtimeSessionId) {
|
|
20496
|
-
return `agentcore_${runtimeSessionId
|
|
20554
|
+
return `agentcore_${sanitizeRuntimeId(runtimeSessionId)}`;
|
|
20497
20555
|
}
|
|
20498
20556
|
function userWorkspace(base, runtimeSessionId) {
|
|
20499
|
-
const dir = path52.join(base, "users", runtimeSessionId
|
|
20557
|
+
const dir = path52.join(base, "users", sanitizeRuntimeId(runtimeSessionId));
|
|
20500
20558
|
fs44.mkdirSync(dir, { recursive: true });
|
|
20501
20559
|
return dir;
|
|
20502
20560
|
}
|
|
20561
|
+
function acceptChunk(uploads, msg) {
|
|
20562
|
+
const uploadId = msg.uploadId;
|
|
20563
|
+
const seq = msg.seq;
|
|
20564
|
+
const total = msg.total;
|
|
20565
|
+
const mimeType = msg.mimeType;
|
|
20566
|
+
const data = msg.data;
|
|
20567
|
+
if (typeof uploadId !== "string" || !uploadId || uploadId.length > 64)
|
|
20568
|
+
return "image_chunk: uploadId must be a non-empty string (\u226464 chars)";
|
|
20569
|
+
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 0)
|
|
20570
|
+
return "image_chunk: seq must be a non-negative integer";
|
|
20571
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 1 || total > MAX_CHUNKS_PER_UPLOAD)
|
|
20572
|
+
return `image_chunk: total must be 1..${MAX_CHUNKS_PER_UPLOAD}`;
|
|
20573
|
+
if (typeof mimeType !== "string" || typeof data !== "string" || !data)
|
|
20574
|
+
return "image_chunk: string mimeType and non-empty string data required";
|
|
20575
|
+
if (seq >= total)
|
|
20576
|
+
return "image_chunk: seq out of range";
|
|
20577
|
+
let up = uploads.get(uploadId);
|
|
20578
|
+
if (!up) {
|
|
20579
|
+
if (uploads.size >= MAX_PENDING_UPLOADS)
|
|
20580
|
+
return `too many pending uploads (max ${MAX_PENDING_UPLOADS} per connection)`;
|
|
20581
|
+
up = { chunks: new Array(total), received: 0, mimeType, size: 0 };
|
|
20582
|
+
uploads.set(uploadId, up);
|
|
20583
|
+
}
|
|
20584
|
+
if (up.chunks.length !== total)
|
|
20585
|
+
return "image_chunk: total mismatch across chunks of one uploadId";
|
|
20586
|
+
if (up.chunks[seq] !== void 0)
|
|
20587
|
+
return null;
|
|
20588
|
+
if (up.size + data.length > MAX_IMAGE_BASE64_LENGTH) {
|
|
20589
|
+
uploads.delete(uploadId);
|
|
20590
|
+
return "image too large (max 8MB base64 per image)";
|
|
20591
|
+
}
|
|
20592
|
+
up.chunks[seq] = data;
|
|
20593
|
+
up.received++;
|
|
20594
|
+
up.size += data.length;
|
|
20595
|
+
return null;
|
|
20596
|
+
}
|
|
20597
|
+
function claimImageRefs(uploads, raw) {
|
|
20598
|
+
if (raw === void 0)
|
|
20599
|
+
return [];
|
|
20600
|
+
if (!Array.isArray(raw))
|
|
20601
|
+
return { error: "imageRefs must be an array" };
|
|
20602
|
+
if (raw.length > MAX_IMAGES_PER_MESSAGE)
|
|
20603
|
+
return { error: `too many images (max ${MAX_IMAGES_PER_MESSAGE} per message)` };
|
|
20604
|
+
const images = [];
|
|
20605
|
+
for (const ref of raw) {
|
|
20606
|
+
if (typeof ref !== "string")
|
|
20607
|
+
return { error: "imageRefs entries must be strings" };
|
|
20608
|
+
const up = uploads.get(ref);
|
|
20609
|
+
if (!up)
|
|
20610
|
+
return { error: `unknown uploadId: ${ref}` };
|
|
20611
|
+
if (up.received !== up.chunks.length)
|
|
20612
|
+
return { error: `upload ${ref} incomplete (${up.received}/${up.chunks.length} chunks)` };
|
|
20613
|
+
images.push({ data: up.chunks.join(""), mimeType: up.mimeType });
|
|
20614
|
+
}
|
|
20615
|
+
for (const ref of raw)
|
|
20616
|
+
uploads.delete(ref);
|
|
20617
|
+
return images;
|
|
20618
|
+
}
|
|
20503
20619
|
async function ensureSession(sm, workspace, sessionId) {
|
|
20504
20620
|
if (sm.getSessionById(sessionId))
|
|
20505
20621
|
return;
|
|
@@ -20638,6 +20754,7 @@ function setupAgentCoreWebSocket(deps) {
|
|
|
20638
20754
|
ready ??= ensureSession(sm, userWs, sessionId).then(() => attachListener());
|
|
20639
20755
|
return ready;
|
|
20640
20756
|
};
|
|
20757
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
20641
20758
|
ws.on("message", (raw) => {
|
|
20642
20759
|
let msg;
|
|
20643
20760
|
try {
|
|
@@ -20663,8 +20780,20 @@ function setupAgentCoreWebSocket(deps) {
|
|
|
20663
20780
|
});
|
|
20664
20781
|
return;
|
|
20665
20782
|
}
|
|
20783
|
+
if (msg.type === "image_chunk") {
|
|
20784
|
+
const err = acceptChunk(uploads, msg);
|
|
20785
|
+
if (err)
|
|
20786
|
+
send({ type: "error", error: err });
|
|
20787
|
+
return;
|
|
20788
|
+
}
|
|
20666
20789
|
const text5 = typeof msg.inputText === "string" ? msg.inputText.trim() : "";
|
|
20667
|
-
|
|
20790
|
+
const claimed = claimImageRefs(uploads, msg.imageRefs);
|
|
20791
|
+
if (!Array.isArray(claimed)) {
|
|
20792
|
+
send({ type: "error", error: claimed.error });
|
|
20793
|
+
return;
|
|
20794
|
+
}
|
|
20795
|
+
const images = claimed;
|
|
20796
|
+
if (!text5 && images.length === 0) {
|
|
20668
20797
|
return;
|
|
20669
20798
|
}
|
|
20670
20799
|
if (text5.startsWith("/")) {
|
|
@@ -20722,10 +20851,29 @@ function setupAgentCoreWebSocket(deps) {
|
|
|
20722
20851
|
void (async () => {
|
|
20723
20852
|
try {
|
|
20724
20853
|
await ensureOnce();
|
|
20725
|
-
|
|
20854
|
+
const notes = [];
|
|
20855
|
+
for (const img of images) {
|
|
20856
|
+
try {
|
|
20857
|
+
const savedPath = await saveInboundMedia({
|
|
20858
|
+
workspacePath: userWs,
|
|
20859
|
+
accountId: sanitizeRuntimeId(runtimeSessionId),
|
|
20860
|
+
channel: "agentcore",
|
|
20861
|
+
buffer: Buffer.from(img.data, "base64"),
|
|
20862
|
+
kind: "image",
|
|
20863
|
+
mimeType: img.mimeType
|
|
20864
|
+
});
|
|
20865
|
+
notes.push(`[\u56FE\u7247\u5DF2\u4FDD\u5B58: ${savedPath}]`);
|
|
20866
|
+
} catch (err) {
|
|
20867
|
+
console.log(`[AgentCore] image save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20868
|
+
notes.push("[\u56FE\u7247]");
|
|
20869
|
+
}
|
|
20870
|
+
}
|
|
20871
|
+
const fullText = notes.length > 0 ? text5 ? `${text5}
|
|
20872
|
+
${notes.join("\n")}` : notes.join("\n") : text5;
|
|
20873
|
+
sm.appendUserMessage(sessionId, fullText);
|
|
20726
20874
|
const result = await sm.sendUserMessage(sessionId, `[channel: agentcore]
|
|
20727
20875
|
|
|
20728
|
-
${
|
|
20876
|
+
${fullText}`, images.length > 0 ? images : void 0);
|
|
20729
20877
|
if (result === "queued")
|
|
20730
20878
|
send({ type: "queued" });
|
|
20731
20879
|
} catch (err) {
|
|
@@ -20736,19 +20884,25 @@ ${text5}`);
|
|
|
20736
20884
|
ws.on("close", () => {
|
|
20737
20885
|
unsubscribe?.();
|
|
20738
20886
|
unsubscribe = null;
|
|
20887
|
+
uploads.clear();
|
|
20739
20888
|
activeOverrides.delete(runtimeSessionId);
|
|
20740
20889
|
});
|
|
20741
20890
|
console.log(`[AgentCore] ws connected \u2014 session ${sessionId}`);
|
|
20742
20891
|
});
|
|
20743
20892
|
}
|
|
20744
|
-
var SESSION_HEADER, activeOverrides;
|
|
20893
|
+
var SESSION_HEADER, MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_BASE64_LENGTH, MAX_PENDING_UPLOADS, MAX_CHUNKS_PER_UPLOAD, activeOverrides;
|
|
20745
20894
|
var init_agentcore = __esm({
|
|
20746
20895
|
"packages/server/dist/routes/agentcore.js"() {
|
|
20747
20896
|
"use strict";
|
|
20748
20897
|
init_commands2();
|
|
20749
20898
|
init_ui_log_builder();
|
|
20750
20899
|
init_session_types();
|
|
20900
|
+
init_media_store();
|
|
20751
20901
|
SESSION_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id";
|
|
20902
|
+
MAX_IMAGES_PER_MESSAGE = 4;
|
|
20903
|
+
MAX_IMAGE_BASE64_LENGTH = 8 * 1024 * 1024;
|
|
20904
|
+
MAX_PENDING_UPLOADS = 4;
|
|
20905
|
+
MAX_CHUNKS_PER_UPLOAD = 256;
|
|
20752
20906
|
activeOverrides = /* @__PURE__ */ new Map();
|
|
20753
20907
|
}
|
|
20754
20908
|
});
|
|
@@ -21453,6 +21607,8 @@ function setupWebSocketHandler(deps) {
|
|
|
21453
21607
|
const state = getState(client);
|
|
21454
21608
|
if (!state)
|
|
21455
21609
|
return;
|
|
21610
|
+
if (!client.sessionManager.isUIStateDirty(client.sessionId))
|
|
21611
|
+
return;
|
|
21456
21612
|
const projectPath = resolveProjectPath2(client.projectId);
|
|
21457
21613
|
const snapshot = createSaveSnapshot(state);
|
|
21458
21614
|
if (snapshot.length === 0)
|
|
@@ -22065,6 +22221,9 @@ function createCronDb(globalDir2) {
|
|
|
22065
22221
|
if (!cols.some((c) => c.name === "run_at")) {
|
|
22066
22222
|
sqlite.exec(`ALTER TABLE cron_jobs ADD COLUMN run_at INTEGER`);
|
|
22067
22223
|
}
|
|
22224
|
+
if (!cols.some((c) => c.name === "timeout_sec")) {
|
|
22225
|
+
sqlite.exec(`ALTER TABLE cron_jobs ADD COLUMN timeout_sec INTEGER`);
|
|
22226
|
+
}
|
|
22068
22227
|
const runCols = sqlite.prepare(`PRAGMA table_info(cron_runs)`).all();
|
|
22069
22228
|
if (!runCols.some((c) => c.name === "pid")) {
|
|
22070
22229
|
sqlite.exec(`ALTER TABLE cron_runs ADD COLUMN pid INTEGER`);
|
|
@@ -22105,6 +22264,10 @@ var init_cron_db = __esm({
|
|
|
22105
22264
|
runAt: integer4("run_at"),
|
|
22106
22265
|
/** IANA timezone name; null = host timezone. */
|
|
22107
22266
|
timezone: text4("timezone"),
|
|
22267
|
+
/** Max seconds a single cron-fired cli may run before the runner kills
|
|
22268
|
+
* it. NULL = default 3600 (runner.ts CLI_TIMEOUT_SEC). Range 60–21600,
|
|
22269
|
+
* enforced at the write points (REST routes / skill helper). */
|
|
22270
|
+
timeoutSec: integer4("timeout_sec"),
|
|
22108
22271
|
/** JSON array of `{channelType, accountId}` records. Cron output (final
|
|
22109
22272
|
* assistant text) is dispatched to each one. Empty array = log only. */
|
|
22110
22273
|
targets: text4("targets").notNull().default("[]"),
|
|
@@ -22154,6 +22317,7 @@ CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
|
22154
22317
|
schedule TEXT NOT NULL,
|
|
22155
22318
|
run_at INTEGER,
|
|
22156
22319
|
timezone TEXT,
|
|
22320
|
+
timeout_sec INTEGER,
|
|
22157
22321
|
targets TEXT NOT NULL DEFAULT '[]',
|
|
22158
22322
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
22159
22323
|
last_run_status TEXT,
|
|
@@ -22587,6 +22751,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
22587
22751
|
const sessionId = `cron-${job.id}`;
|
|
22588
22752
|
const args = ["cli", "-a", job.agentId, "-s", sessionId, "-w", job.workspacePath];
|
|
22589
22753
|
const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
|
|
22754
|
+
const timeoutSec = job.timeoutSec ?? CLI_TIMEOUT_SEC;
|
|
22590
22755
|
const teeStream = fs48.createWriteStream(logPath, { flags: "a" });
|
|
22591
22756
|
teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
|
|
22592
22757
|
`);
|
|
@@ -22617,7 +22782,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
22617
22782
|
const killTimer = setTimeout(() => {
|
|
22618
22783
|
timedOut = true;
|
|
22619
22784
|
teeStream.write(`
|
|
22620
|
-
[runner] timeout ${
|
|
22785
|
+
[runner] timeout ${timeoutSec}s \u2014 SIGTERM, grace ${KILL_GRACE_SEC}s
|
|
22621
22786
|
`);
|
|
22622
22787
|
try {
|
|
22623
22788
|
child.kill("SIGTERM");
|
|
@@ -22629,7 +22794,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
22629
22794
|
if (child.pid)
|
|
22630
22795
|
killTreeHard(child.pid);
|
|
22631
22796
|
}, KILL_GRACE_SEC * 1e3);
|
|
22632
|
-
},
|
|
22797
|
+
}, timeoutSec * 1e3);
|
|
22633
22798
|
child.stdout?.on("data", (chunk) => {
|
|
22634
22799
|
stdout += chunk.toString("utf-8");
|
|
22635
22800
|
teeStream.write(chunk);
|
|
@@ -22665,7 +22830,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
22665
22830
|
let failureReason = null;
|
|
22666
22831
|
if (result.exitCode === 124) {
|
|
22667
22832
|
status = "timeout";
|
|
22668
|
-
failureReason = `cli exceeded ${
|
|
22833
|
+
failureReason = `cli exceeded ${timeoutSec}s timeout`;
|
|
22669
22834
|
} else if (result.exitCode !== 0) {
|
|
22670
22835
|
status = "failed";
|
|
22671
22836
|
failureReason = stderr.slice(-500).trim() || `cli exited ${result.exitCode}`;
|
|
@@ -22813,6 +22978,15 @@ function validateSchedule(expr) {
|
|
|
22813
22978
|
return err instanceof Error ? err.message : String(err);
|
|
22814
22979
|
}
|
|
22815
22980
|
}
|
|
22981
|
+
function validateTimeoutSec(v) {
|
|
22982
|
+
if (typeof v !== "number" || !Number.isInteger(v)) {
|
|
22983
|
+
return "timeoutSec must be an integer (seconds)";
|
|
22984
|
+
}
|
|
22985
|
+
if (v < TIMEOUT_SEC_MIN || v > TIMEOUT_SEC_MAX) {
|
|
22986
|
+
return `timeoutSec must be between ${TIMEOUT_SEC_MIN} and ${TIMEOUT_SEC_MAX}`;
|
|
22987
|
+
}
|
|
22988
|
+
return null;
|
|
22989
|
+
}
|
|
22816
22990
|
function createCronRoutes() {
|
|
22817
22991
|
const router = new Hono14();
|
|
22818
22992
|
router.get("/cron/jobs", async (c) => {
|
|
@@ -22856,6 +23030,11 @@ function createCronRoutes() {
|
|
|
22856
23030
|
if (hasRunAt && body.runAt <= Date.now()) {
|
|
22857
23031
|
return c.json({ error: "runAt must be in the future" }, 400);
|
|
22858
23032
|
}
|
|
23033
|
+
if (body.timeoutSec !== void 0 && body.timeoutSec !== null) {
|
|
23034
|
+
const err = validateTimeoutSec(body.timeoutSec);
|
|
23035
|
+
if (err)
|
|
23036
|
+
return c.json({ error: err }, 400);
|
|
23037
|
+
}
|
|
22859
23038
|
const id = newJobId();
|
|
22860
23039
|
const now = Date.now();
|
|
22861
23040
|
getCronDb().insert(cronJobs).values({
|
|
@@ -22867,6 +23046,7 @@ function createCronRoutes() {
|
|
|
22867
23046
|
schedule: hasSchedule ? body.schedule : "",
|
|
22868
23047
|
runAt: hasRunAt ? body.runAt : null,
|
|
22869
23048
|
timezone: body.timezone ?? null,
|
|
23049
|
+
timeoutSec: body.timeoutSec ?? null,
|
|
22870
23050
|
targets: JSON.stringify(body.targets ?? []),
|
|
22871
23051
|
enabled: body.enabled === false ? 0 : 1,
|
|
22872
23052
|
lastRunStatus: null,
|
|
@@ -22903,6 +23083,11 @@ function createCronRoutes() {
|
|
|
22903
23083
|
if (body.runAt <= Date.now())
|
|
22904
23084
|
return c.json({ error: "runAt must be in the future" }, 400);
|
|
22905
23085
|
}
|
|
23086
|
+
if (body.timeoutSec !== void 0 && body.timeoutSec !== null) {
|
|
23087
|
+
const err = validateTimeoutSec(body.timeoutSec);
|
|
23088
|
+
if (err)
|
|
23089
|
+
return c.json({ error: err }, 400);
|
|
23090
|
+
}
|
|
22906
23091
|
const hasSchedule = typeof body.schedule === "string" && body.schedule.trim().length > 0;
|
|
22907
23092
|
const hasRunAt = typeof body.runAt === "number";
|
|
22908
23093
|
if (hasSchedule && hasRunAt) {
|
|
@@ -22930,6 +23115,8 @@ function createCronRoutes() {
|
|
|
22930
23115
|
patch.runAt = nextRunAt;
|
|
22931
23116
|
if (body.timezone !== void 0)
|
|
22932
23117
|
patch.timezone = body.timezone || null;
|
|
23118
|
+
if (body.timeoutSec !== void 0)
|
|
23119
|
+
patch.timeoutSec = body.timeoutSec;
|
|
22933
23120
|
if (body.targets !== void 0)
|
|
22934
23121
|
patch.targets = JSON.stringify(body.targets);
|
|
22935
23122
|
if (body.enabled !== void 0)
|
|
@@ -23045,6 +23232,7 @@ function computeNextRun(j) {
|
|
|
23045
23232
|
return null;
|
|
23046
23233
|
}
|
|
23047
23234
|
}
|
|
23235
|
+
var TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX;
|
|
23048
23236
|
var init_cron = __esm({
|
|
23049
23237
|
"packages/server/dist/routes/cron.js"() {
|
|
23050
23238
|
"use strict";
|
|
@@ -23052,6 +23240,8 @@ var init_cron = __esm({
|
|
|
23052
23240
|
init_runner();
|
|
23053
23241
|
init_dispatcher();
|
|
23054
23242
|
init_broadcast();
|
|
23243
|
+
TIMEOUT_SEC_MIN = 60;
|
|
23244
|
+
TIMEOUT_SEC_MAX = 21600;
|
|
23055
23245
|
}
|
|
23056
23246
|
});
|
|
23057
23247
|
|
|
@@ -28954,7 +29144,7 @@ var init_dist2 = __esm({
|
|
|
28954
29144
|
init_git_ssh();
|
|
28955
29145
|
__dirname3 = path64.dirname(fileURLToPath3(import.meta.url));
|
|
28956
29146
|
PORT = config.server.port;
|
|
28957
|
-
HALO_VERSION = "1.1.
|
|
29147
|
+
HALO_VERSION = "1.1.4";
|
|
28958
29148
|
PROJECT_ROOT = findProjectRoot();
|
|
28959
29149
|
HALO_HOME4 = path64.join(homedir24(), ".halo");
|
|
28960
29150
|
GIT_SHA = readGitSha();
|
|
@@ -29856,7 +30046,7 @@ import path65 from "node:path";
|
|
|
29856
30046
|
import fs58 from "node:fs";
|
|
29857
30047
|
import { homedir as homedir25 } from "node:os";
|
|
29858
30048
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
29859
|
-
var VERSION = "1.1.
|
|
30049
|
+
var VERSION = "1.1.4";
|
|
29860
30050
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
29861
30051
|
|
|
29862
30052
|
Commands:
|
package/package.json
CHANGED
|
@@ -35,6 +35,34 @@ secrets:
|
|
|
35
35
|
# thinking.default → effort selected when Thinking is on (adaptive)
|
|
36
36
|
# thinking.defaultBudgetTokens → budget selected when Thinking is on (manual)
|
|
37
37
|
models:
|
|
38
|
+
- id: global.anthropic.claude-fable-5-1
|
|
39
|
+
displayName: Claude Fable 5.1
|
|
40
|
+
maxOutputTokens: 128000
|
|
41
|
+
contextWindow: 1000000
|
|
42
|
+
capabilities:
|
|
43
|
+
image: true
|
|
44
|
+
video: false
|
|
45
|
+
audio: false
|
|
46
|
+
promptCaching:
|
|
47
|
+
default: 1h
|
|
48
|
+
ttlPresets:
|
|
49
|
+
- { value: 5m, label: 5min }
|
|
50
|
+
- { value: 1h, label: 1hour }
|
|
51
|
+
thinking:
|
|
52
|
+
# Fable 5.1 — Mythos-class flagship refresh. Adaptive effort set
|
|
53
|
+
# confirmed by an invalid-value probe against Bedrock (2026-09-02):
|
|
54
|
+
# low / medium / high / xhigh / max, same as Fable 5. 1M context,
|
|
55
|
+
# 128K output. Smoke-tested on invoke (xhigh).
|
|
56
|
+
mode: adaptive
|
|
57
|
+
defaultEnabled: true
|
|
58
|
+
default: high # flagship-of-flagships — high by default; xhigh/max on demand
|
|
59
|
+
effortPresets:
|
|
60
|
+
- { value: low, label: Low }
|
|
61
|
+
- { value: medium, label: Medium }
|
|
62
|
+
- { value: high, label: High }
|
|
63
|
+
- { value: xhigh, label: XHigh }
|
|
64
|
+
- { value: max, label: Max }
|
|
65
|
+
|
|
38
66
|
- id: global.anthropic.claude-fable-5
|
|
39
67
|
displayName: Claude Fable 5
|
|
40
68
|
maxOutputTokens: 128000
|
|
@@ -53,6 +53,7 @@ Sensible defaults if user didn't say:
|
|
|
53
53
|
| timezone | leave unset (host time) |
|
|
54
54
|
| targets | none (log only — the run shows in admin Cron tab) |
|
|
55
55
|
| label | summarize from prompt + schedule |
|
|
56
|
+
| timeout | leave unset (3600s) — see `--timeout-sec` below |
|
|
56
57
|
|
|
57
58
|
### 2. Translate the schedule
|
|
58
59
|
|
|
@@ -78,6 +79,13 @@ today" or "send the report next Monday morning".
|
|
|
78
79
|
|
|
79
80
|
Ambiguous time ("morning")? Ask once for the exact hour.
|
|
80
81
|
|
|
82
|
+
**Max run time (`--timeout-sec`, optional)** — each fire runs as a cli child
|
|
83
|
+
that the server kills after `timeout_sec` seconds (integer, 60–21600). Unset
|
|
84
|
+
= default 3600. Only set it when the user says so: a quick push job that
|
|
85
|
+
should fail fast ("kill it after 5 minutes" → `--timeout-sec 300`), or a
|
|
86
|
+
heavy analysis run that needs more than an hour (`--timeout-sec 7200`). On
|
|
87
|
+
`update`, pass `--timeout-sec ""` to clear back to the default.
|
|
88
|
+
|
|
81
89
|
### 3. Channels (only when delivery is wanted)
|
|
82
90
|
|
|
83
91
|
Run `channels` first to see what's available:
|