eve-hub 0.6.70 → 0.7.1
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 +12 -0
- package/kit-modules.json +0 -1
- package/package.json +1 -1
- package/schedules/hub.d.mts +0 -1
- package/schedules/hub.mjs +0 -3
- package/stubs.mjs +1 -4
- package/world.mjs +29 -63
- package/schedules/compact.d.ts +0 -5
- package/schedules/compact.js +0 -18
- package/schedules/rules.mjs +0 -132
- package/schedules/run-compact.mjs +0 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A subagent's spawn call stays running through the parent's next step until its task settles, so a working card keeps its subagent orbs.
|
|
8
|
+
|
|
9
|
+
## 0.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- main's compact schedule is gone: the hub's sweep compacts a session whose turn ended at 130k+ input tokens. The kit no longer has `schedules/compact` or `hub.compact`.
|
|
14
|
+
|
|
3
15
|
## 0.6.70
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/kit-modules.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
|
|
6
6
|
"type": "module",
|
package/schedules/hub.d.mts
CHANGED
|
@@ -14,7 +14,6 @@ export type Hub = {
|
|
|
14
14
|
commit(id: string, withState: { state?: unknown }, posts: Post[]): Promise<unknown>;
|
|
15
15
|
post(id: string, text: string, key?: string): Promise<unknown>;
|
|
16
16
|
track<T>(id: string, fn: () => Promise<T>): Promise<T | undefined>;
|
|
17
|
-
compact(threadId: string, bot: string, by: string, report?: { trigger?: string; reportTo?: string }): Promise<{ status: number; ok: boolean; body: unknown }>;
|
|
18
17
|
dashChecked(prFacts?: Record<string, { head: string | null; ci: string }>): Promise<Ok & { checkedAt: string }>;
|
|
19
18
|
setSummary(path: string, text: string): Promise<unknown>;
|
|
20
19
|
dashClassified(): Promise<Ok & { classifiedAt: string }>;
|
package/schedules/hub.mjs
CHANGED
|
@@ -61,9 +61,6 @@ export function createHub({ base = registryBase(), agent = "main", fetchImpl = f
|
|
|
61
61
|
},
|
|
62
62
|
// A one-off post outside a run (runOnce commits through `commit`).
|
|
63
63
|
post: (id, text, key) => must(`/api/threads/${encodeURIComponent(threadOf(id))}/messages`, { method: "POST", body: JSON.stringify({ author: "system", text, ...(key ? { clientId: key } : {}) }) }),
|
|
64
|
-
// With `report` ({ trigger, reportTo: "<bot>/<schedule>" }) the hub posts
|
|
65
|
-
// this compaction's report lines into that schedule's thread itself.
|
|
66
|
-
compact: (threadId, bot, by, report = {}) => call(`/api/threads/${encodeURIComponent(threadId)}/compact`, { method: "POST", body: JSON.stringify({ agent: bot, by, ...report }) }),
|
|
67
64
|
// The Dash was scanned now (hosted-hub/src/domain/dash.mjs checkedAt), changed or not.
|
|
68
65
|
// `prFacts` ({ "owner/repo#12": { head, ci } }): what the scan's GitHub query said of the PRs reviews are about.
|
|
69
66
|
dashChecked: (prFacts = {}) => must("/api/dash/checked", { method: "POST", body: JSON.stringify({ by: agent, prFacts }) }),
|
package/stubs.mjs
CHANGED
|
@@ -38,7 +38,6 @@ export const MAIN_KIT_FILES = {
|
|
|
38
38
|
"tools/dash_update.ts": ["Set Dash entries: importance, lane, reason and deferred per thread."],
|
|
39
39
|
"tools/apply_tags.ts": ["Retag threads, merge or rename tags: applies a staged proposal or a new batch directly."],
|
|
40
40
|
// Handler schedules: the hub fires them from its minute clock (registry/schedules.mjs).
|
|
41
|
-
"schedules/compact.ts": ["Every 10 minutes: compacts idle sessions once their prompt cache has gone cold, reported in sch-main-compact."],
|
|
42
41
|
"schedules/dash.ts": ["When a bot's turn ends (fired by the hub), and every 8 minutes: the human's PRs, a Dash entry for each thread that changed, and a new summary for each track with news."],
|
|
43
42
|
"schedules/prune.ts": ["Every 10 minutes: one prune pass over what archived or deleted threads leave on disk: worktrees, scratch, their-only uploads, idle bots' traces and workflow runs; unsaved work kept as refs/bundles."],
|
|
44
43
|
};
|
|
@@ -51,12 +50,10 @@ export const MAIN_KIT_FILES = {
|
|
|
51
50
|
// the kit's bash (HostSandbox's root, the daemon's env; eve's own bash runs
|
|
52
51
|
// instead), the browser and the screenshot paths of attach_image/view_image,
|
|
53
52
|
// restart_hub (the hub's own code), memory (the workspace's files), and main's prune schedule (it cleans the hub machine's disk).
|
|
54
|
-
// main's
|
|
55
|
-
// (hosted sessions carry no last-model-call time), and dash has no PR data (its lookup runs `gh`).
|
|
53
|
+
// main's dash schedule stays: off the hub's machine it has no PR data (its lookup runs `gh`).
|
|
56
54
|
export const DEPLOYED_KIT_FILES = new Set([
|
|
57
55
|
"channels/eve.ts",
|
|
58
56
|
"hooks/mention-gate.ts",
|
|
59
|
-
"schedules/compact.ts",
|
|
60
57
|
"schedules/dash.ts",
|
|
61
58
|
"skills/onboarding-profile.ts",
|
|
62
59
|
"tools/onboarding_scripts.ts",
|
package/world.mjs
CHANGED
|
@@ -10925,7 +10925,7 @@ function reduceActivity(turn, { type, data = {} }, at, onCutOff) {
|
|
|
10925
10925
|
changed = trackTaskEvent(turn.card, { type, data }, new Date(at).toISOString()) || changed;
|
|
10926
10926
|
if (changed) turn.taskCard = taskCardView(turn.card);
|
|
10927
10927
|
if (type === "step.started") {
|
|
10928
|
-
for (const tool of turn.tools) if (tool.status === "running" && tool.step !== void 0 && tool.step <= data.stepIndex) {
|
|
10928
|
+
for (const tool of turn.tools) if (!tool.task && tool.status === "running" && tool.step !== void 0 && tool.step <= data.stepIndex) {
|
|
10929
10929
|
tool.status = "interrupted";
|
|
10930
10930
|
changed = true;
|
|
10931
10931
|
}
|
|
@@ -12304,14 +12304,14 @@ function codePointLength(str) {
|
|
|
12304
12304
|
const units = str.length;
|
|
12305
12305
|
if (!highSurrogate.test(str))
|
|
12306
12306
|
return units;
|
|
12307
|
-
let
|
|
12307
|
+
let count = units;
|
|
12308
12308
|
for (let i = 0; i < units - 1; i++) {
|
|
12309
12309
|
if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
|
|
12310
|
-
|
|
12310
|
+
count--;
|
|
12311
12311
|
i++;
|
|
12312
12312
|
}
|
|
12313
12313
|
}
|
|
12314
|
-
return
|
|
12314
|
+
return count;
|
|
12315
12315
|
}
|
|
12316
12316
|
function getLengthableOrigin(input2) {
|
|
12317
12317
|
if (Array.isArray(input2))
|
|
@@ -16694,8 +16694,8 @@ function az_default() {
|
|
|
16694
16694
|
}
|
|
16695
16695
|
|
|
16696
16696
|
// ../../hosted-hub/node_modules/zod/v4/locales/be.js
|
|
16697
|
-
function getBelarusianPlural(
|
|
16698
|
-
const absCount = Math.abs(
|
|
16697
|
+
function getBelarusianPlural(count, one, few, many) {
|
|
16698
|
+
const absCount = Math.abs(count);
|
|
16699
16699
|
const lastDigit = absCount % 10;
|
|
16700
16700
|
const lastTwoDigits = absCount % 100;
|
|
16701
16701
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -19342,8 +19342,8 @@ function hu_default() {
|
|
|
19342
19342
|
}
|
|
19343
19343
|
|
|
19344
19344
|
// ../../hosted-hub/node_modules/zod/v4/locales/hy.js
|
|
19345
|
-
function getArmenianPlural(
|
|
19346
|
-
return Math.abs(
|
|
19345
|
+
function getArmenianPlural(count, one, many) {
|
|
19346
|
+
return Math.abs(count) === 1 ? one : many;
|
|
19347
19347
|
}
|
|
19348
19348
|
function withDefiniteArticle(word) {
|
|
19349
19349
|
if (!word)
|
|
@@ -22081,8 +22081,8 @@ function ro_default() {
|
|
|
22081
22081
|
}
|
|
22082
22082
|
|
|
22083
22083
|
// ../../hosted-hub/node_modules/zod/v4/locales/ru.js
|
|
22084
|
-
function getRussianPlural(
|
|
22085
|
-
const absCount = Math.abs(
|
|
22084
|
+
function getRussianPlural(count, one, few, many) {
|
|
22085
|
+
const absCount = Math.abs(count);
|
|
22086
22086
|
const lastDigit = absCount % 10;
|
|
22087
22087
|
const lastTwoDigits = absCount % 100;
|
|
22088
22088
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -30846,11 +30846,11 @@ var PARTICIPANTS = (thread, except = "''") => `SELECT b.name AS bot FROM bots b,
|
|
|
30846
30846
|
var NOT_IN_BEFORE = `NOT EXISTS (SELECT 1 FROM (${PARTICIPANTS("?", "?")}) WHERE bot = ?)`;
|
|
30847
30847
|
var hasJoins = (threadId2) => !threadId2.startsWith("dm-") && !threadId2.startsWith("sch-");
|
|
30848
30848
|
function routingStatements(messageId2, threadId2, bots, now, { by, joins }, held = {}, steer = false) {
|
|
30849
|
-
const
|
|
30849
|
+
const quiet = !joins || !hasJoins(threadId2);
|
|
30850
30850
|
const fresh = "EXISTS (SELECT 1 FROM messages WHERE id = ?)";
|
|
30851
30851
|
return [
|
|
30852
30852
|
...bots.flatMap((bot) => [
|
|
30853
|
-
...
|
|
30853
|
+
...quiet ? [] : eventStatements(
|
|
30854
30854
|
threadId2,
|
|
30855
30855
|
{ kind: "join", agent: bot, by, message: messageId2 },
|
|
30856
30856
|
`@${bot} joined${by !== bot ? ` (added by @${by})` : ""}`,
|
|
@@ -31113,44 +31113,30 @@ var MOVES = Object.freeze({
|
|
|
31113
31113
|
});
|
|
31114
31114
|
var VERDICTS = Object.freeze(["ship", "fix-first", "rethink"]);
|
|
31115
31115
|
|
|
31116
|
-
// ../../hosted-hub/src/domain/cron.mjs
|
|
31117
|
-
var MINUTE = 6e4;
|
|
31118
|
-
var CATCH_UP_MS = 7 * 24 * 60 * MINUTE;
|
|
31119
|
-
|
|
31120
|
-
// ../../hosted-hub/src/store/schedules.ts
|
|
31121
|
-
var scheduleThreadId = (agent, id) => `sch-${agent}-${id.replaceAll("/", ".")}`;
|
|
31122
|
-
var Info = external_exports.object({ schedules: external_exports.array(external_exports.object({ name: external_exports.string().min(1), cron: external_exports.string().min(1), hasRun: external_exports.boolean() })) });
|
|
31123
|
-
|
|
31124
|
-
// ../../hosted-hub/src/domain/compact-report.mjs
|
|
31125
|
-
var quiet = (text) => String(text).replace(/@/g, "\uFF20");
|
|
31126
|
-
var count = (n) => typeof n === "number" ? n.toLocaleString("en-US") : "?";
|
|
31127
|
-
var link = (thread, id) => `[${quiet(String(thread?.title ?? id).replace(/[[\]]/g, ""))}](${id})`;
|
|
31128
|
-
var nextTry = (agent) => `next try: after ${quiet(agent)}'s next turn there, once idle 10+ min (or at 40 turns) with its prompt cache gone cold`;
|
|
31129
|
-
var compactedLine = (r, thread) => `compacted: ${quiet(r.agent)} on ${link(thread, r.threadId)} \xB7 ${quiet(r.trigger)} \xB7 ${count(r.before)} input tokens before`;
|
|
31130
|
-
var afterLine = (r, thread, after) => `after compaction: ${quiet(r.agent)} on ${link(thread, r.threadId)} \xB7 ${count(r.before)} \u2192 ${count(after)} input tokens`;
|
|
31131
|
-
var failureLine = (r, thread, reason) => `compact failed: ${quiet(r.agent)} on ${link(thread, r.threadId)} \xB7 ${quiet(r.trigger)} \xB7 ${quiet(reason)}; ${nextTry(r.agent)}`;
|
|
31132
|
-
|
|
31133
31116
|
// ../../hosted-hub/src/store/compactions.ts
|
|
31134
31117
|
var ASKED = `(c.attempt_at IS NOT NULL AND c.attempt_at > COALESCE(c.settled_at, 0))`;
|
|
31135
31118
|
var k = (tokens) => `CAST((${tokens} + 500) / 1000 AS INTEGER) || 'k'`;
|
|
31136
31119
|
var compactedText = (after) => `'Context compacted' || CASE WHEN c.before_tokens IS NULL THEN ''
|
|
31137
31120
|
WHEN ${after} IS NULL THEN ' at ' || ${k("c.before_tokens")} || ' input tokens'
|
|
31138
31121
|
ELSE ': ' || ${k("c.before_tokens")} || ' \u2192 ' || ${k(after)} || ' input tokens' END`;
|
|
31139
|
-
|
|
31122
|
+
var COMPACT_WINDOW_MS = 10 * 6e4;
|
|
31123
|
+
var COMPACTABLE_READ = `SELECT s.id, s.thread_id, s.bot, u.input_tokens FROM usage_steps u JOIN sessions s ON s.id = u.session_id
|
|
31124
|
+
WHERE u.at > ? AND u.at <= ? AND u.input_tokens >= ? AND s.ended_at IS NULL
|
|
31125
|
+
AND (u.at, u.sequence, u.step_index) = (SELECT at, sequence, step_index FROM usage_steps WHERE session_id = u.session_id ORDER BY at DESC, sequence DESC, step_index DESC LIMIT 1)
|
|
31126
|
+
AND NOT EXISTS (SELECT 1 FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES})
|
|
31127
|
+
AND NOT EXISTS (SELECT 1 FROM compactions c WHERE c.session_id = s.id AND MAX(COALESCE(c.completed_at, 0), COALESCE(c.attempt_at, 0)) >= u.at)`;
|
|
31128
|
+
function compactionStatements(type, session, event, at, messageId2) {
|
|
31140
31129
|
const before = `COALESCE(?, (SELECT input_tokens FROM usage_steps WHERE session_id = ? ORDER BY at DESC, sequence DESC, step_index DESC LIMIT 1))`;
|
|
31141
31130
|
if (type === "compaction.requested")
|
|
31142
31131
|
return [s(
|
|
31143
|
-
`INSERT INTO compactions (session_id, before_tokens) VALUES (?, ${before}) ON CONFLICT (session_id) DO UPDATE SET before_tokens = excluded.before_tokens
|
|
31144
|
-
report_phase = CASE WHEN report_phase = 'asked' THEN 'requested' ELSE report_phase END`,
|
|
31132
|
+
`INSERT INTO compactions (session_id, before_tokens) VALUES (?, ${before}) ON CONFLICT (session_id) DO UPDATE SET before_tokens = excluded.before_tokens`,
|
|
31145
31133
|
session.id,
|
|
31146
31134
|
typeof event.usageInputTokens === "number" ? event.usageInputTokens : null,
|
|
31147
31135
|
session.id
|
|
31148
31136
|
)];
|
|
31149
31137
|
if (type === "session.waiting")
|
|
31150
31138
|
return [
|
|
31151
|
-
// eve took the request and went idle with no checkpoint: it kept the history (the reason is in the bot's log).
|
|
31152
|
-
// session.waiting from before eve took the request (the previous turn's, folded late) is not that: only 'requested' counts.
|
|
31153
|
-
...reportLine(report, session, at, `${messageId2}:waiting`, (r) => failureLine(...r, `eve ended it without a checkpoint and kept the history (the reason is in ${session.bot}'s log)`), `report_phase = 'requested'`, null),
|
|
31139
|
+
// eve took the request and went idle with no checkpoint: it kept the history (the reason is in the bot's log).
|
|
31154
31140
|
s("UPDATE compactions SET settled_at = ? WHERE session_id = ? AND attempt_at IS NOT NULL AND attempt_at > COALESCE(settled_at, 0)", at, session.id)
|
|
31155
31141
|
];
|
|
31156
31142
|
return [
|
|
@@ -31172,23 +31158,7 @@ function compactionStatements(type, session, event, at, messageId2, report = nul
|
|
|
31172
31158
|
session.thread,
|
|
31173
31159
|
session.bot,
|
|
31174
31160
|
messageId2
|
|
31175
|
-
)
|
|
31176
|
-
...reportLine(report, session, at, `${messageId2}:completed`, (r) => compactedLine(...r), `report_phase IN ('asked', 'requested', 'unanswered')`, "compacted")
|
|
31177
|
-
];
|
|
31178
|
-
}
|
|
31179
|
-
var reportThread = (to) => {
|
|
31180
|
-
const [bot, ...id] = to.split("/");
|
|
31181
|
-
return scheduleThreadId(bot, id.join("/"));
|
|
31182
|
-
};
|
|
31183
|
-
var lineOf = (report, session) => [{ agent: session.bot, threadId: session.thread, trigger: report.trigger, before: report.before }, { title: report.title }];
|
|
31184
|
-
function reportLine(report, session, at, id, text, when, next) {
|
|
31185
|
-
if (!report) return [];
|
|
31186
|
-
const thread = reportThread(report.to), rowId = derivedMessageId(id, "report"), has = `EXISTS (SELECT 1 FROM compactions WHERE session_id = ? AND ${when})`;
|
|
31187
|
-
return [
|
|
31188
|
-
s(`INSERT INTO log (thread_id,type,data,at) SELECT ?, 'thread.created', ?, ? WHERE ${has} AND NOT EXISTS (SELECT 1 FROM threads WHERE id = ?)`, thread, JSON.stringify({ threadId: thread }), at, session.id, thread),
|
|
31189
|
-
s(`INSERT OR IGNORE INTO threads (id,title,created_at) SELECT ?, ?, ? WHERE ${has}`, thread, report.to, at, session.id),
|
|
31190
|
-
...eventStatements(thread, { kind: "compaction-report", agent: session.bot }, text(lineOf(report, session)), at, rowId, { sql: has, args: [session.id] }),
|
|
31191
|
-
s(`UPDATE compactions SET report_phase = ? WHERE session_id = ? AND ${when}`, next, session.id)
|
|
31161
|
+
)
|
|
31192
31162
|
];
|
|
31193
31163
|
}
|
|
31194
31164
|
function compactedSizeStatements(sessionId2, inputTokens) {
|
|
@@ -31199,7 +31169,6 @@ function compactedSizeStatements(sessionId2, inputTokens) {
|
|
|
31199
31169
|
s(`UPDATE compactions SET after_tokens = ? WHERE session_id = ? AND ${waiting("")}`, inputTokens, sessionId2)
|
|
31200
31170
|
];
|
|
31201
31171
|
}
|
|
31202
|
-
var reportAfterStatements = (report, session, inputTokens, at, id) => reportLine(report, session, at, `${id}:after`, ([r, thread]) => afterLine(r, thread, inputTokens), `report_phase = 'compacted' AND completed_at IS NOT NULL AND after_tokens IS NULL`, null);
|
|
31203
31172
|
|
|
31204
31173
|
// ../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/util.js
|
|
31205
31174
|
var util_exports2 = {};
|
|
@@ -31846,14 +31815,14 @@ function codePointLength2(str) {
|
|
|
31846
31815
|
const units = str.length;
|
|
31847
31816
|
if (!highSurrogate2.test(str))
|
|
31848
31817
|
return units;
|
|
31849
|
-
let
|
|
31818
|
+
let count = units;
|
|
31850
31819
|
for (let i = 0; i < units - 1; i++) {
|
|
31851
31820
|
if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
|
|
31852
|
-
|
|
31821
|
+
count--;
|
|
31853
31822
|
i++;
|
|
31854
31823
|
}
|
|
31855
31824
|
}
|
|
31856
|
-
return
|
|
31825
|
+
return count;
|
|
31857
31826
|
}
|
|
31858
31827
|
function getLengthableOrigin2(input2) {
|
|
31859
31828
|
if (Array.isArray(input2))
|
|
@@ -37924,9 +37893,7 @@ var FOLD_STATE_SQL = `SELECT * FROM (WITH sess AS (SELECT * FROM sessions WHERE
|
|
|
37924
37893
|
(SELECT json_object('person', person, 'names', json(names)) FROM human_notify) AS notify,
|
|
37925
37894
|
(SELECT json_object('status', t.status, 'id', t.id, 'text', t.reply_text, 'lastStep', json(t.last_step), 'activity', json(t.activity),
|
|
37926
37895
|
'trigger', ${triggerOf("t")}) FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES}
|
|
37927
|
-
ORDER BY t.started_at DESC LIMIT 1) AS open_turn
|
|
37928
|
-
(SELECT json_object('to', c.report_to, 'trigger', c.report_trigger, 'before', c.report_before, 'title', (SELECT title FROM threads WHERE id = s.thread_id))
|
|
37929
|
-
FROM compactions c WHERE c.session_id = s.id AND c.report_to IS NOT NULL) AS report
|
|
37896
|
+
ORDER BY t.started_at DESC LIMIT 1) AS open_turn
|
|
37930
37897
|
FROM sess s LEFT JOIN session_folds f ON f.session_id = s.id)`;
|
|
37931
37898
|
var TURN_END_SQL = `SELECT * FROM (WITH sess AS (SELECT * FROM sessions WHERE id = ?),
|
|
37932
37899
|
open_turn AS (SELECT t.id FROM turns t WHERE t.session_id = (SELECT id FROM sess) AND t.status IN ${OPEN_TURN_STATUSES} ORDER BY t.started_at DESC LIMIT 1),
|
|
@@ -37958,8 +37925,7 @@ function foldState(row) {
|
|
|
37958
37925
|
lastPost: row.last_post == null ? null : String(row.last_post),
|
|
37959
37926
|
notify: row.notify ? JSON.parse(String(row.notify)) : null,
|
|
37960
37927
|
foldedIndex: Number(row.folded_index),
|
|
37961
|
-
open: row.open_turn ? JSON.parse(String(row.open_turn)) : null
|
|
37962
|
-
report: row.report ? JSON.parse(String(row.report)) : null
|
|
37928
|
+
open: row.open_turn ? JSON.parse(String(row.open_turn)) : null
|
|
37963
37929
|
};
|
|
37964
37930
|
}
|
|
37965
37931
|
var REVOKED_TURN = `EXISTS (SELECT 1 FROM turn_deliveries td JOIN deliveries d ON d.id = td.delivery_id
|
|
@@ -38017,7 +37983,7 @@ function fold(state, chunks, now = Date.now()) {
|
|
|
38017
37983
|
log("request.updated", { requestIds: (data.resolutions ?? []).map((r) => r.requestId) });
|
|
38018
37984
|
}
|
|
38019
37985
|
if (event.type === "compaction.requested" || event.type === "compaction.completed" || event.type === "session.waiting")
|
|
38020
|
-
stmts.push(...compactionStatements(event.type, session, data, at, derivedMessageId(session.id, index, eventIndex)
|
|
37986
|
+
stmts.push(...compactionStatements(event.type, session, data, at, derivedMessageId(session.id, index, eventIndex)));
|
|
38021
37987
|
if (event.type === "turn.started") {
|
|
38022
37988
|
if (!data.turnId) throw new Error("turn.started must name eve turnId");
|
|
38023
37989
|
flush();
|
|
@@ -38079,7 +38045,7 @@ function fold(state, chunks, now = Date.now()) {
|
|
|
38079
38045
|
case "step.completed": {
|
|
38080
38046
|
if (data.sequence === void 0 || data.stepIndex === void 0) break;
|
|
38081
38047
|
stmts.push(s(`${STEP_INSERT} VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT step_model FROM sessions WHERE id = ?))`, ...stepValues(session.id, { sequence: data.sequence, stepIndex: data.stepIndex, at: new Date(at).toISOString(), usage: data.usage ?? {} }), open3.id, session.id));
|
|
38082
|
-
if (typeof data.usage?.inputTokens === "number") stmts.push(...
|
|
38048
|
+
if (typeof data.usage?.inputTokens === "number") stmts.push(...compactedSizeStatements(session.id, data.usage.inputTokens));
|
|
38083
38049
|
break;
|
|
38084
38050
|
}
|
|
38085
38051
|
case "message.appended": {
|
package/schedules/compact.d.ts
DELETED
package/schedules/compact.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// main's compact schedule: every 10 minutes, compacts idle sessions whose
|
|
2
|
-
// prompt cache has gone cold (last model call past their model's cache TTL
|
|
3
|
-
// + 1 min, measured per model by the hub; a warm one never is) once idle at
|
|
4
|
-
// least 10 min with >= 100k input tokens, or after 40 turns, through the hub
|
|
5
|
-
// (POST /api/threads/:id/compact), which refuses if a
|
|
6
|
-
// turn or a delivery got there first. Reports each compaction, its before →
|
|
7
|
-
// after tokens and any failure in sch-main-compact without tagging anyone,
|
|
8
|
-
// so main is never woken. Rules in ./rules.mjs, the run in ./run-compact.mjs.
|
|
9
|
-
import { defineSchedule } from "eve/schedules";
|
|
10
|
-
import { createHub } from "./hub.mjs";
|
|
11
|
-
import { runCompact } from "./run-compact.mjs";
|
|
12
|
-
export default defineSchedule({
|
|
13
|
-
cron: "*/10 * * * *",
|
|
14
|
-
async run() {
|
|
15
|
-
const hub = createHub();
|
|
16
|
-
await hub.track("compact", () => runCompact(hub));
|
|
17
|
-
},
|
|
18
|
-
});
|
package/schedules/rules.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// The decisions behind main's compact schedule (schedules/compact.ts), pure:
|
|
2
|
-
// each takes the registry's /api/state snapshot (`snap`), the schedule's
|
|
3
|
-
// and the time. No I/O here; schedules/hub.mjs does that. The report lines
|
|
4
|
-
// for the compactions themselves are the hub's (hosted-hub/src/domain/compact-report.mjs).
|
|
5
|
-
//
|
|
6
|
-
// Plain JS on purpose: the schedule (TypeScript, bundled by eve) and the
|
|
7
|
-
// tests (plain node) import the same module.
|
|
8
|
-
|
|
9
|
-
const MIN = 60_000;
|
|
10
|
-
|
|
11
|
-
const parse = (iso) => {
|
|
12
|
-
const ms = Date.parse(iso ?? "");
|
|
13
|
-
return Number.isFinite(ms) ? ms : null;
|
|
14
|
-
};
|
|
15
|
-
const isBusy = (status) => status === "working" || status === "waiting";
|
|
16
|
-
const clock = (ms) => {
|
|
17
|
-
const s = Math.max(0, Math.floor(ms / 1000));
|
|
18
|
-
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
19
|
-
};
|
|
20
|
-
const count = (n) => (typeof n === "number" ? n.toLocaleString("en-US") : "?");
|
|
21
|
-
// The report never tags anyone: an "@" from a title or an error can't
|
|
22
|
-
// become a mention (and wake a bot).
|
|
23
|
-
const quiet = (text) => String(text).replace(/@/g, "@");
|
|
24
|
-
const link = (threads, id) => `[${quiet(String(threads?.[id]?.title ?? id).replace(/[[\]]/g, ""))}](${id})`;
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// How long a model's prompt cache stays warm after a model call.
|
|
28
|
-
//
|
|
29
|
-
// The Gateway sends Anthropic models `caching: "auto"` with no `ttl`: 5 min,
|
|
30
|
-
// reset by every read. OpenAI keeps prefixes best-effort, longer (measured
|
|
31
|
-
// warm past 10 min, cold by 30-60). Anything else: assume Anthropic's 5 min.
|
|
32
|
-
// Once a model has `minSamples` observations (the hub's `cacheGaps[model]`:
|
|
33
|
-
// [gapMs, hitRatio] for model calls >= 1 min after the session's previous
|
|
34
|
-
// one), its TTL is measured instead, from the gap buckets with at least
|
|
35
|
-
// `minBucketSamples` samples (the rest say nothing), each warm if its median
|
|
36
|
-
// hit ratio is >= 0.5:
|
|
37
|
-
// - the cache is warm up to at least the top of the longest warm bucket;
|
|
38
|
-
// - it has expired by the bottom of the first cold bucket above that (a
|
|
39
|
-
// shorter-gap miss never overrides a longer-gap hit);
|
|
40
|
-
// - the provider default, kept within those bounds. So warm samples alone
|
|
41
|
-
// can extend the TTL but never shorten it: only a cold bucket can.
|
|
42
|
-
// The median, not the mean: a cold call on a long shared prefix (system
|
|
43
|
-
// prompt, tools) still reads part of it.
|
|
44
|
-
export const CACHE_TTL_DEFAULTS = Object.freeze({ anthropic: 5 * MIN, openai: 30 * MIN, unknown: 5 * MIN });
|
|
45
|
-
export const CACHE_GAP_EDGES = Object.freeze([1, 3, 5, 10, 30, 60].map((m) => m * MIN));
|
|
46
|
-
export const CACHE_TTL_OPTIONS = Object.freeze({ minSamples: 20, minBucketSamples: 3, warmRatio: 0.5 });
|
|
47
|
-
|
|
48
|
-
const providerOf = (modelId) => String(modelId ?? "").split("/")[0];
|
|
49
|
-
export const defaultCacheTtlMs = (modelId, defaults = CACHE_TTL_DEFAULTS) => defaults[providerOf(modelId)] ?? defaults.unknown;
|
|
50
|
-
const median = (xs) => {
|
|
51
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
52
|
-
const m = s.length >> 1;
|
|
53
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// { ttlMs, measured, samples }
|
|
57
|
-
export function cacheTtl(modelId, samples, options = {}) {
|
|
58
|
-
const { minSamples, minBucketSamples, warmRatio, defaults = CACHE_TTL_DEFAULTS } = { ...CACHE_TTL_OPTIONS, ...options };
|
|
59
|
-
const fallback = { ttlMs: defaultCacheTtlMs(modelId, defaults), measured: false, samples: samples?.length ?? 0 };
|
|
60
|
-
if (!Array.isArray(samples) || samples.length < minSamples) return fallback;
|
|
61
|
-
let warmTop = 0; // warm at least this long
|
|
62
|
-
const cold = []; // bottoms of the cold buckets
|
|
63
|
-
for (let i = 0; i < CACHE_GAP_EDGES.length; i++) {
|
|
64
|
-
const lo = CACHE_GAP_EDGES[i];
|
|
65
|
-
const hi = CACHE_GAP_EDGES[i + 1] ?? Infinity;
|
|
66
|
-
const ratios = samples.filter(([gap, ratio]) => gap >= lo && gap < hi && Number.isFinite(ratio)).map(([, ratio]) => ratio);
|
|
67
|
-
if (ratios.length < minBucketSamples) continue;
|
|
68
|
-
if (median(ratios) >= warmRatio) warmTop = Math.max(warmTop, Number.isFinite(hi) ? hi : lo);
|
|
69
|
-
else cold.push(lo);
|
|
70
|
-
}
|
|
71
|
-
const coldAbove = cold.filter((lo) => lo >= warmTop);
|
|
72
|
-
const expiredBy = coldAbove.length ? Math.min(...coldAbove) : Infinity; // cold from here on
|
|
73
|
-
if (warmTop === 0 && cold.length === 0) return fallback;
|
|
74
|
-
const ttlMs = Math.min(Math.max(fallback.ttlMs, warmTop), expiredBy);
|
|
75
|
-
return { ttlMs, measured: true, samples: samples.length };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const span = (ms) => (ms % MIN === 0 ? `${ms / MIN}m` : clock(ms));
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Which sessions to compact: only those whose prompt cache has gone cold.
|
|
82
|
-
//
|
|
83
|
-
// eve's compaction call doesn't reuse the session's cached prefix (its own
|
|
84
|
-
// system prompt, no tools, the transcript re-rendered), so it pays full input
|
|
85
|
-
// price warm or cold. A warm session's next turn reads its context at the
|
|
86
|
-
// cached rate, so compacting it costs more than it saves; a cold one's next
|
|
87
|
-
// turn re-writes the whole context anyway, so a smaller one is cheaper.
|
|
88
|
-
//
|
|
89
|
-
// An idle session is compacted only once its latest model call is more than
|
|
90
|
-
// its model's cache TTL (cacheTtl, from the hub's cacheGaps) plus
|
|
91
|
-
// `coldMarginMs` ago (a warm one never is), and either
|
|
92
|
-
// - it has been idle at least `idleMinMs` and its latest model call was at
|
|
93
|
-
// least `minTokens` input tokens. Per (bot, thread) session: `lastEventAt`
|
|
94
|
-
// moves only on that session's own eve events, so another bot working in
|
|
95
|
-
// the same thread doesn't hold it back; or
|
|
96
|
-
// - it has had `maxTurns` turns since its last compaction and has been idle
|
|
97
|
-
// at least `turnsIdleMs`;
|
|
98
|
-
// and it has made a model call since its last compaction, or since the last
|
|
99
|
-
// attempt at one (a failed one isn't retried until the session has moved on;
|
|
100
|
-
// a compacted one doesn't trigger again on later ticks until its next turn).
|
|
101
|
-
// Only bots whose process is up. The hub refuses (409) if a turn or a
|
|
102
|
-
// delivery got there first.
|
|
103
|
-
export const COMPACT_DEFAULTS = Object.freeze({ idleMinMs: 10 * MIN, minTokens: 100_000, maxTurns: 40, turnsIdleMs: 1 * MIN, coldMarginMs: 1 * MIN });
|
|
104
|
-
|
|
105
|
-
export function compactCandidates(snap, nowMs, options = {}) {
|
|
106
|
-
const { idleMinMs, minTokens, maxTurns, turnsIdleMs, coldMarginMs, ttl: ttlOptions } = { ...COMPACT_DEFAULTS, ...options };
|
|
107
|
-
const up = new Map((snap?.agents ?? []).map((a) => [a.name, !a.process || a.process.status === "up"]));
|
|
108
|
-
const out = [];
|
|
109
|
-
for (const v of Object.values(snap?.sessions ?? {})) {
|
|
110
|
-
if (!v.sessionId || !up.get(v.agent) || v.status !== "idle" || v.compacting || v.suspect) continue;
|
|
111
|
-
const thread = snap.threads?.[v.threadId];
|
|
112
|
-
if (!thread || thread.archived || isBusy(thread.status?.[v.agent])) continue;
|
|
113
|
-
const lastStep = parse(v.lastStepAt);
|
|
114
|
-
const lastTry = Math.max(parse(v.compactedAt) ?? 0, parse(v.compactAttemptAt) ?? 0);
|
|
115
|
-
if (lastStep === null || lastStep <= lastTry) continue;
|
|
116
|
-
const { ttlMs } = cacheTtl(v.lastModelId, snap.cacheGaps?.[v.lastModelId], ttlOptions);
|
|
117
|
-
const sinceStepMs = nowMs - lastStep;
|
|
118
|
-
if (sinceStepMs < ttlMs + coldMarginMs) continue; // still warm: never compacted
|
|
119
|
-
const idleMs = nowMs - (parse(v.lastEventAt) ?? nowMs);
|
|
120
|
-
const cold = `cold: idle ${clock(sinceStepMs)} > ttl ${span(ttlMs)}`;
|
|
121
|
-
const turns = (v.sessionTurns ?? 0) - (v.turnsAtCompaction ?? 0);
|
|
122
|
-
let trigger = null;
|
|
123
|
-
if (idleMs >= idleMinMs && (v.lastInputTokens ?? 0) >= minTokens) trigger = `${cold} · ${Math.round(v.lastInputTokens / 1000)}k tokens`;
|
|
124
|
-
else if (turns >= maxTurns && idleMs >= turnsIdleMs) trigger = `${cold} · ${turns} turns`;
|
|
125
|
-
if (trigger) out.push({ threadId: v.threadId, agent: v.agent, sessionId: v.sessionId, trigger, before: v.lastInputTokens ?? null });
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// A failure the hub didn't report itself (it refused before asking eve).
|
|
131
|
-
export const failureLine = (c, threads, reason) =>
|
|
132
|
-
`compact failed: ${quiet(c.agent)} on ${link(threads, c.threadId)} · ${c.trigger} · ${quiet(reason)}; next try: after ${quiet(c.agent)}'s next turn there, once idle 10+ min (or at 40 turns) with its prompt cache gone cold`;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// One run of main's compact schedule (schedules/compact.ts): ask the hub to
|
|
2
|
-
// compact each candidate, with the report going to sch-main-compact. The
|
|
3
|
-
// hub records each attempt durably before it asks eve and posts the
|
|
4
|
-
// report's lines itself (compacted, before → after, failures), so this run
|
|
5
|
-
// keeps no state. Only a refusal the hub didn't report (it never got as far
|
|
6
|
-
// as asking eve) gets a line from here.
|
|
7
|
-
import { compactCandidates, failureLine } from "./rules.mjs";
|
|
8
|
-
|
|
9
|
-
export async function runCompact(hub, { now = Date.now(), options } = {}) {
|
|
10
|
-
const snap = await hub.snapshot();
|
|
11
|
-
const lines = [];
|
|
12
|
-
for (const c of compactCandidates(snap, now, options)) {
|
|
13
|
-
let r;
|
|
14
|
-
try {
|
|
15
|
-
r = await hub.compact(c.threadId, c.agent, "schedule main/compact", { trigger: c.trigger, reportTo: "main/compact" });
|
|
16
|
-
} catch (error) {
|
|
17
|
-
r = { status: 0, body: { error: String(error?.message ?? error) } };
|
|
18
|
-
}
|
|
19
|
-
if (r.status === 202 || r.status === 409 || r.body?.reported) continue; // 409: a turn or a delivery got there first
|
|
20
|
-
const line = failureLine(c, snap.threads, `the hub refused: ${r.status || "unreachable"} ${r.body?.error ?? r.body?.status ?? ""}`.trim());
|
|
21
|
-
lines.push(line);
|
|
22
|
-
await hub.post("compact", line).catch(() => {});
|
|
23
|
-
}
|
|
24
|
-
return { lines };
|
|
25
|
-
}
|