@turmind/halo 2.2.0 → 2.2.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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/1989-3b2faa2731d973c4.js +1 -0
- package/admin-out/_next/static/chunks/app/page-10a4afac9cbc27a2.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/background-dispatch.md +1 -1
- package/bundled-docs/design/cron.md +4 -4
- package/bundled-docs/design/session.md +2 -2
- package/bundled-docs/design/ws.md +7 -5
- package/bundled-docs/dev/api.md +4 -3
- package/bundled-docs/guide/agents.md +1 -1
- package/bundled-docs/requirements/agent.md +1 -1
- package/bundled-docs/requirements/workspace.md +3 -3
- package/dist/index.js +72 -24
- package/package.json +1 -1
- package/templates/skills/cron/SKILL.md +46 -21
- package/templates/skills/cron/manage-cron.py +10 -13
- package/admin-out/_next/static/chunks/1989-dab5d5c24b8f4c9f.js +0 -1
- package/admin-out/_next/static/chunks/app/page-cca81a2ed69100f5.js +0 -1
- /package/admin-out/_next/static/{Rs9tIYSSITPLxv274DU9f → j4QFRrUtg33DNMUIgg4ij}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{Rs9tIYSSITPLxv274DU9f → j4QFRrUtg33DNMUIgg4ij}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1929,7 +1929,7 @@ var init_init = __esm({
|
|
|
1929
1929
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
1930
1930
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
1931
1931
|
];
|
|
1932
|
-
TEMPLATE_VERSION =
|
|
1932
|
+
TEMPLATE_VERSION = 119;
|
|
1933
1933
|
VERSION_FILE = ".template-version";
|
|
1934
1934
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
1935
1935
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -16976,7 +16976,7 @@ async function initObservability(haloProcess = "server") {
|
|
|
16976
16976
|
return true;
|
|
16977
16977
|
}
|
|
16978
16978
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
16979
|
-
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.
|
|
16979
|
+
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.1", haloProcess);
|
|
16980
16980
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
|
|
16981
16981
|
return true;
|
|
16982
16982
|
}
|
|
@@ -28938,6 +28938,23 @@ var init_handler = __esm({
|
|
|
28938
28938
|
});
|
|
28939
28939
|
|
|
28940
28940
|
// ../core/dist/protocol/session-message.js
|
|
28941
|
+
function inferMessageType(msg) {
|
|
28942
|
+
if (msg.type)
|
|
28943
|
+
return msg.type;
|
|
28944
|
+
if (msg.role === "user")
|
|
28945
|
+
return "user";
|
|
28946
|
+
if (msg.role === "assistant")
|
|
28947
|
+
return "assistant";
|
|
28948
|
+
if (msg.toolName)
|
|
28949
|
+
return "tool_call";
|
|
28950
|
+
if (msg.toolOutput !== void 0 && !msg.toolName)
|
|
28951
|
+
return "tool_result";
|
|
28952
|
+
if (msg.usage)
|
|
28953
|
+
return "usage";
|
|
28954
|
+
if (msg.systemPrompt)
|
|
28955
|
+
return "context";
|
|
28956
|
+
return "notification";
|
|
28957
|
+
}
|
|
28941
28958
|
var init_session_message = __esm({
|
|
28942
28959
|
"../core/dist/protocol/session-message.js"() {
|
|
28943
28960
|
"use strict";
|
|
@@ -31566,6 +31583,12 @@ var init_terminal_manager = __esm({
|
|
|
31566
31583
|
|
|
31567
31584
|
// ../server/dist/ws/handler.js
|
|
31568
31585
|
import path78 from "node:path";
|
|
31586
|
+
function adminSnapshotRows(rows) {
|
|
31587
|
+
return rows.filter((m) => {
|
|
31588
|
+
const t2 = inferMessageType(m);
|
|
31589
|
+
return t2 !== "tool_call" && t2 !== "tool_result";
|
|
31590
|
+
});
|
|
31591
|
+
}
|
|
31569
31592
|
function setupWebSocketHandler(deps) {
|
|
31570
31593
|
const { wss: wss2, registry: registry2 } = deps;
|
|
31571
31594
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -31689,8 +31712,10 @@ function setupWebSocketHandler(deps) {
|
|
|
31689
31712
|
}
|
|
31690
31713
|
if (client.subscriptions.has(targetSessionId)) {
|
|
31691
31714
|
const state = getState(client, targetSessionId);
|
|
31692
|
-
const
|
|
31693
|
-
|
|
31715
|
+
const log2 = state?.messageLog ?? [];
|
|
31716
|
+
const deletedIds = log2.filter((m) => m.deleted).map((m) => m.id);
|
|
31717
|
+
const userRows = log2.filter((m) => m.role === "user" && !m.taskId).length;
|
|
31718
|
+
sendJson(ws, { type: "exchange:deleted", sessionId: targetSessionId, deletedIds, userOrdinal: msg.userOrdinal, userRows });
|
|
31694
31719
|
}
|
|
31695
31720
|
}
|
|
31696
31721
|
function createEventListener(client, sessionId) {
|
|
@@ -32074,7 +32099,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
32074
32099
|
const state2 = getState(client2, sid);
|
|
32075
32100
|
const ctxConfig = await sm.getContextConfig(sid);
|
|
32076
32101
|
const running = sm.isSessionRunning(sid);
|
|
32077
|
-
const messages3 = state2 ? running ?
|
|
32102
|
+
const messages3 = state2 ? adminSnapshotRows(running ? state2.messageLog : createSaveSnapshot(state2)) : [];
|
|
32078
32103
|
const detachedSession = sm.getSessionById(sid);
|
|
32079
32104
|
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId: sid, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId, archiveCount: archiveCountFor(client2, sid, detachedSession?.agentId), accessLevel: detachedSession?.accessLevel ?? null } });
|
|
32080
32105
|
if (state2 && state2.contextTokens > 0) {
|
|
@@ -32131,7 +32156,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
32131
32156
|
}
|
|
32132
32157
|
}
|
|
32133
32158
|
const state = sid ? getState(client2, sid) : null;
|
|
32134
|
-
const messages2 = state ?
|
|
32159
|
+
const messages2 = state ? adminSnapshotRows(createSaveSnapshot(state)) : [];
|
|
32135
32160
|
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: sid, maxContextTokens, agentId, archiveCount: sid ? archiveCountFor(client2, sid, agentId) : 0, accessLevel } });
|
|
32136
32161
|
if (state && state.contextTokens > 0) {
|
|
32137
32162
|
sendJson(ws, { type: "chat:usage", contextTokens: state.contextTokens, outputTokens: state.outputTokens, sessionId: sid });
|
|
@@ -32161,6 +32186,7 @@ var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
|
|
|
32161
32186
|
var init_handler2 = __esm({
|
|
32162
32187
|
"../server/dist/ws/handler.js"() {
|
|
32163
32188
|
"use strict";
|
|
32189
|
+
init_session_types();
|
|
32164
32190
|
init_sandbox();
|
|
32165
32191
|
init_ui_log_builder();
|
|
32166
32192
|
init_config();
|
|
@@ -32291,7 +32317,7 @@ function buildCard(workspace, interfaceUrl, caps) {
|
|
|
32291
32317
|
return {
|
|
32292
32318
|
name: raw.name,
|
|
32293
32319
|
description: raw.description,
|
|
32294
|
-
version: typeof raw.version === "string" ? raw.version : "2.2.
|
|
32320
|
+
version: typeof raw.version === "string" ? raw.version : "2.2.1",
|
|
32295
32321
|
supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
|
|
32296
32322
|
capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
|
|
32297
32323
|
...caps.tokenAuth === false ? {} : {
|
|
@@ -34046,6 +34072,8 @@ async function runJob(jobId, triggerKind) {
|
|
|
34046
34072
|
kickPendingEvents(jobId);
|
|
34047
34073
|
return runId;
|
|
34048
34074
|
};
|
|
34075
|
+
if (triggerKind === "manual" && !job.userPrompt)
|
|
34076
|
+
return skip("nothing to run: the job has no prompt, and run-now carries no webhook events");
|
|
34049
34077
|
if (_inflight.has(jobId))
|
|
34050
34078
|
return skip("previous run still in progress");
|
|
34051
34079
|
if (_inflightSessions.has(sessionKeyFor(job.workspacePath, sessionId)))
|
|
@@ -34131,9 +34159,9 @@ async function runJob(jobId, triggerKind) {
|
|
|
34131
34159
|
const events = webhook ? claimPendingEvents(jobId, runId, sessionId) : [];
|
|
34132
34160
|
try {
|
|
34133
34161
|
const texts = events.map((ev) => eventText(ev, job.workspacePath, false));
|
|
34134
|
-
const stamp = host.appendUserMessage(sessionId, [job.userPrompt, ...texts.map((t2) => t2.ui)].join("\n\n"));
|
|
34162
|
+
const stamp = host.appendUserMessage(sessionId, [job.userPrompt, ...texts.map((t2) => t2.ui)].filter(Boolean).join("\n\n"));
|
|
34135
34163
|
stampEvents(events.map((ev) => ev.id), stamp);
|
|
34136
|
-
await host.sendUserMessage(sessionId, [UNATTENDED_PREAMBLE2, job.userPrompt, ...texts.map((t2) => t2.model)].join("\n\n"), void 0, void 0, stamp);
|
|
34164
|
+
await host.sendUserMessage(sessionId, [UNATTENDED_PREAMBLE2, job.userPrompt, ...texts.map((t2) => t2.model)].filter(Boolean).join("\n\n"), void 0, void 0, stamp);
|
|
34137
34165
|
} catch (err) {
|
|
34138
34166
|
untrackRun(runId);
|
|
34139
34167
|
tracked = false;
|
|
@@ -34531,7 +34559,7 @@ function createCronRoutes() {
|
|
|
34531
34559
|
const body = await c.req.json().catch(() => null);
|
|
34532
34560
|
if (!body)
|
|
34533
34561
|
return c.json({ error: "invalid body" }, 400);
|
|
34534
|
-
for (const k of ["workspacePath", "agentId"
|
|
34562
|
+
for (const k of ["workspacePath", "agentId"]) {
|
|
34535
34563
|
if (!body[k] || typeof body[k] !== "string")
|
|
34536
34564
|
return c.json({ error: `${k} required` }, 400);
|
|
34537
34565
|
}
|
|
@@ -34540,6 +34568,11 @@ function createCronRoutes() {
|
|
|
34540
34568
|
const hasSchedule = typeof body.schedule === "string" && body.schedule.trim().length > 0;
|
|
34541
34569
|
const hasRunAt = typeof body.runAt === "number" && Number.isFinite(body.runAt);
|
|
34542
34570
|
const webhook = body.webhook === true;
|
|
34571
|
+
const userPrompt = body.userPrompt ?? "";
|
|
34572
|
+
if (typeof userPrompt !== "string")
|
|
34573
|
+
return c.json({ error: "userPrompt must be a string" }, 400);
|
|
34574
|
+
if (!userPrompt && !webhook)
|
|
34575
|
+
return c.json({ error: "userPrompt required" }, 400);
|
|
34543
34576
|
if (!hasSchedule && !hasRunAt && !webhook)
|
|
34544
34577
|
return c.json({ error: "schedule, runAt or webhook required" }, 400);
|
|
34545
34578
|
if (hasSchedule && hasRunAt)
|
|
@@ -34570,8 +34603,6 @@ function createCronRoutes() {
|
|
|
34570
34603
|
const stateless = body.stateless === true;
|
|
34571
34604
|
if (stateless && session.sessionId)
|
|
34572
34605
|
return c.json({ error: STATELESS_SESSION_ERR }, 400);
|
|
34573
|
-
if (webhook && session.sessionId)
|
|
34574
|
-
return c.json({ error: WEBHOOK_SESSION_ERR }, 400);
|
|
34575
34606
|
const folder = await parseWorkingDir(body.workspacePath, body.workingDir);
|
|
34576
34607
|
if ("error" in folder)
|
|
34577
34608
|
return c.json({ error: folder.error }, 400);
|
|
@@ -34598,7 +34629,7 @@ function createCronRoutes() {
|
|
|
34598
34629
|
label: body.label && body.label.trim().length > 0 ? body.label.trim() : null,
|
|
34599
34630
|
workspacePath: body.workspacePath,
|
|
34600
34631
|
agentId: body.agentId,
|
|
34601
|
-
userPrompt
|
|
34632
|
+
userPrompt,
|
|
34602
34633
|
schedule: hasSchedule ? body.schedule : "",
|
|
34603
34634
|
runAt: hasRunAt ? body.runAt : null,
|
|
34604
34635
|
webhookSecret: webhook ? newWebhookSecret() : null,
|
|
@@ -34694,8 +34725,12 @@ function createCronRoutes() {
|
|
|
34694
34725
|
if (!nextWebhookSecret && nextSchedule.trim().length === 0 && typeof nextRunAt !== "number") {
|
|
34695
34726
|
return c.json({ error: "schedule, runAt or webhook required" }, 400);
|
|
34696
34727
|
}
|
|
34697
|
-
if (
|
|
34698
|
-
return c.json({ error:
|
|
34728
|
+
if (body.userPrompt !== void 0 && typeof body.userPrompt !== "string")
|
|
34729
|
+
return c.json({ error: "userPrompt must be a string" }, 400);
|
|
34730
|
+
const nextPrompt = body.userPrompt ?? existing.userPrompt;
|
|
34731
|
+
if (!nextWebhookSecret && !nextPrompt && (body.userPrompt !== void 0 || existing.webhookSecret !== null)) {
|
|
34732
|
+
return c.json({ error: "userPrompt required (only a webhook job may leave it empty)" }, 400);
|
|
34733
|
+
}
|
|
34699
34734
|
for (const k of ["activeFrom", "activeUntil"]) {
|
|
34700
34735
|
const err = validateActiveBound(k, body[k]);
|
|
34701
34736
|
if (err)
|
|
@@ -34720,7 +34755,7 @@ function createCronRoutes() {
|
|
|
34720
34755
|
if (body.agentId !== void 0)
|
|
34721
34756
|
patch.agentId = body.agentId;
|
|
34722
34757
|
if (body.userPrompt !== void 0)
|
|
34723
|
-
patch.userPrompt =
|
|
34758
|
+
patch.userPrompt = nextPrompt;
|
|
34724
34759
|
if (body.schedule !== void 0 || clearSchedule)
|
|
34725
34760
|
patch.schedule = nextSchedule;
|
|
34726
34761
|
if (body.runAt !== void 0 || clearRunAt)
|
|
@@ -34871,7 +34906,7 @@ function computeNextRun(j) {
|
|
|
34871
34906
|
return null;
|
|
34872
34907
|
}
|
|
34873
34908
|
}
|
|
34874
|
-
var
|
|
34909
|
+
var TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX, STATELESS_SESSION_ERR;
|
|
34875
34910
|
var init_cron = __esm({
|
|
34876
34911
|
"../server/dist/routes/cron.js"() {
|
|
34877
34912
|
"use strict";
|
|
@@ -34881,7 +34916,6 @@ var init_cron = __esm({
|
|
|
34881
34916
|
init_dispatcher();
|
|
34882
34917
|
init_broadcast();
|
|
34883
34918
|
init_session_id();
|
|
34884
|
-
WEBHOOK_SESSION_ERR = "a webhook job runs in its own session (or a new one every run), not a picked sessionId";
|
|
34885
34919
|
TIMEOUT_SEC_MIN = 60;
|
|
34886
34920
|
TIMEOUT_SEC_MAX = 21600;
|
|
34887
34921
|
STATELESS_SESSION_ERR = "stateless (new session every run) and sessionId are mutually exclusive";
|
|
@@ -35381,7 +35415,7 @@ function createCoreChecker(opts) {
|
|
|
35381
35415
|
}
|
|
35382
35416
|
function getCoreStatus(force) {
|
|
35383
35417
|
checker ??= createCoreChecker({
|
|
35384
|
-
haloVersion: "2.2.
|
|
35418
|
+
haloVersion: "2.2.1",
|
|
35385
35419
|
fetchLatest: fetchNpmLatest,
|
|
35386
35420
|
onChange: (status) => broadcast({ type: "updates:changed", scope: "core", status })
|
|
35387
35421
|
});
|
|
@@ -35549,7 +35583,7 @@ function hubBase() {
|
|
|
35549
35583
|
}
|
|
35550
35584
|
function serverChecker() {
|
|
35551
35585
|
checker2 ??= createExtensionsChecker({
|
|
35552
|
-
haloVersion: "2.2.
|
|
35586
|
+
haloVersion: "2.2.1",
|
|
35553
35587
|
installed: () => {
|
|
35554
35588
|
const s = getSnapshot();
|
|
35555
35589
|
return [...s.extensions, ...s.disabled];
|
|
@@ -41920,7 +41954,7 @@ var init_dist2 = __esm({
|
|
|
41920
41954
|
init_git_ssh();
|
|
41921
41955
|
__dirname3 = path93.dirname(fileURLToPath3(import.meta.url));
|
|
41922
41956
|
PORT = config.server.port;
|
|
41923
|
-
HALO_VERSION = "2.2.
|
|
41957
|
+
HALO_VERSION = "2.2.1";
|
|
41924
41958
|
PROJECT_ROOT = findProjectRoot();
|
|
41925
41959
|
HALO_HOME4 = path93.join(homedir25(), ".halo");
|
|
41926
41960
|
GIT_SHA = readGitSha();
|
|
@@ -42073,8 +42107,21 @@ var init_dist2 = __esm({
|
|
|
42073
42107
|
}
|
|
42074
42108
|
FRONTEND_DIR = resolveFrontendDir();
|
|
42075
42109
|
if (!AGENTCORE) {
|
|
42076
|
-
app.use("/*", serveStatic({
|
|
42110
|
+
app.use("/*", serveStatic({
|
|
42111
|
+
root: path93.relative(process.cwd(), FRONTEND_DIR),
|
|
42112
|
+
// Next's `/_next/static/` files are content-hashed (under a buildId dir),
|
|
42113
|
+
// so they can be cached forever. Only on a found file — the SPA fallback
|
|
42114
|
+
// below must stay uncached, or a missing chunk caches index.html as js.
|
|
42115
|
+
// `c.res.headers`, not `c.header`: node-server calls onFound after the
|
|
42116
|
+
// body is built, so only c.res (merged into the result by compose) lands.
|
|
42117
|
+
onFound: (_path, c) => {
|
|
42118
|
+
if (c.req.path.startsWith("/_next/static/"))
|
|
42119
|
+
c.res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
42120
|
+
}
|
|
42121
|
+
}));
|
|
42077
42122
|
app.get("/*", (c) => {
|
|
42123
|
+
if (c.req.path.startsWith("/_next/static/"))
|
|
42124
|
+
return c.notFound();
|
|
42078
42125
|
const indexPath = path93.join(FRONTEND_DIR, "index.html");
|
|
42079
42126
|
try {
|
|
42080
42127
|
const html = fs88.readFileSync(indexPath, "utf-8");
|
|
@@ -42102,6 +42149,7 @@ var init_dist2 = __esm({
|
|
|
42102
42149
|
});
|
|
42103
42150
|
wss = new WebSocketServer5({
|
|
42104
42151
|
noServer: true,
|
|
42152
|
+
perMessageDeflate: { zlibDeflateOptions: { level: 1 }, threshold: 1024 },
|
|
42105
42153
|
verifyClient: (info, callback) => {
|
|
42106
42154
|
const token = getTokenFromCookieHeader(info.req.headers.cookie);
|
|
42107
42155
|
if (isAuthenticated(token)) {
|
|
@@ -43981,7 +44029,7 @@ function runNode(opts) {
|
|
|
43981
44029
|
shellTimeoutMs: config.timeout.shellExec,
|
|
43982
44030
|
os: process.platform,
|
|
43983
44031
|
hostname: os13.hostname(),
|
|
43984
|
-
version: "2.2.
|
|
44032
|
+
version: "2.2.1"
|
|
43985
44033
|
};
|
|
43986
44034
|
const exec2 = createNodeExecutor({ root, base, allowShell }, log2);
|
|
43987
44035
|
const instance = crypto17.randomUUID();
|
|
@@ -44497,7 +44545,7 @@ function installUserUnit(opts, run = runCommand) {
|
|
|
44497
44545
|
}
|
|
44498
44546
|
|
|
44499
44547
|
// src/index.ts
|
|
44500
|
-
var VERSION = "2.2.
|
|
44548
|
+
var VERSION = "2.2.1";
|
|
44501
44549
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
44502
44550
|
|
|
44503
44551
|
Commands:
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cron
|
|
3
|
-
description: Manage
|
|
3
|
+
description: Manage jobs that run a halo agent on a recurring schedule, once (`--run-at`), or per webhook POST from an external system or process (a turn in the job's session or an existing root session); results can go to a chat. Activate to add / list / edit / pause / delete a scheduled, one-shot or webhook-triggered task.
|
|
4
4
|
command: /cron
|
|
5
5
|
requiresAccess: full
|
|
6
6
|
verbs:
|
|
7
|
-
- { name: create, desc: Create a
|
|
8
|
-
- { name: list, desc: List
|
|
7
|
+
- { name: create, desc: Create a job }
|
|
8
|
+
- { name: list, desc: List jobs }
|
|
9
9
|
- { name: update, desc: Edit a job (schedule / prompt / targets) }
|
|
10
10
|
- { name: enable, desc: Resume a paused job }
|
|
11
11
|
- { name: disable, desc: Pause a job }
|
|
@@ -18,8 +18,8 @@ The requested action is **`$1`** (full args: `$ARGUMENTS`); with
|
|
|
18
18
|
natural-language activation, infer it from the request. All verbs run through
|
|
19
19
|
this body — map them onto the helper script's subcommands below.
|
|
20
20
|
|
|
21
|
-
Halo runs a global cron daemon that executes user-defined agent prompts on a schedule
|
|
22
|
-
optionally pushes the resulting text to a chat (telegram / wechat / slack / feishu / wecom). Use the
|
|
21
|
+
Halo runs a global cron daemon that executes user-defined agent prompts on a schedule or on
|
|
22
|
+
webhook events and optionally pushes the resulting text to a chat (telegram / wechat / slack / feishu / wecom). Use the
|
|
23
23
|
`manage-cron.py` helper script (sibling of this file) for every operation — it handles id
|
|
24
24
|
generation, JSON encoding for `targets`, timestamps, and so on, so you don't end up
|
|
25
25
|
handcrafting SQL.
|
|
@@ -91,20 +91,42 @@ of the two switches the job's mode and clears the other; never pass both.
|
|
|
91
91
|
Ambiguous time ("morning")? Ask once for the exact hour.
|
|
92
92
|
|
|
93
93
|
**Webhook trigger (`--webhook`)** — the third mode, instead of `--schedule` /
|
|
94
|
-
`--run-at`: the job
|
|
95
|
-
server
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
secret
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
`--run-at`: the job runs on each HTTP POST to `/cron-hooks/<id>` on the
|
|
95
|
+
server — a notification from an external system or process (GitHub, CI,
|
|
96
|
+
any HTTP sender). Use it for "when X happens, run this".
|
|
97
|
+
- **What you get** — the helper generates the secret and prints
|
|
98
|
+
`webhook: {path, secret}`. Give the user the full URL (the server's
|
|
99
|
+
public origin, i.e. the admin address, + `path`) and the secret once, and
|
|
100
|
+
say the secret is shown again in the admin Triggers tab and `get`. Don't
|
|
101
|
+
echo the secret again in later replies or write it into files.
|
|
102
|
+
- **Session** — the same choices as a scheduled job: the job's own session
|
|
103
|
+
(default), a new one per run (`--stateless`), or an existing root session
|
|
104
|
+
(`--session`, below).
|
|
105
|
+
- **How an event enters the session** — each request body becomes a
|
|
106
|
+
message in the session, marked as data. `--prompt` is optional on a
|
|
107
|
+
webhook job: left out, a run delivers the events alone (and run-now has
|
|
108
|
+
nothing to run). Session idle → a new turn starts.
|
|
109
|
+
A user's turn running in it → the events join that turn at its next step,
|
|
110
|
+
and its reply is what the run pushes.
|
|
111
|
+
Session held by another owner (a relay or A2A task, another job's run) →
|
|
112
|
+
the events stay pending and run once it releases the session. Events that
|
|
113
|
+
arrive while the job's own run is in flight join that run.
|
|
114
|
+
- **Delivery** — the sender authenticates with
|
|
115
|
+
`Authorization: Bearer <secret>`, or as a GitHub webhook with the secret
|
|
116
|
+
as its secret (`X-Hub-Signature-256`). Dedupe key: `X-GitHub-Delivery`
|
|
117
|
+
(GitHub sets it), `Idempotency-Key` or `X-Request-Id`; without one, the
|
|
118
|
+
same body is processed only once per 10 minutes. A repeat of an event
|
|
119
|
+
whose run failed runs again. Responses: `202` accepted · `200` duplicate,
|
|
120
|
+
or GitHub `ping` (no run) · `404` wrong secret or unknown job · `409` job
|
|
121
|
+
disabled · `413` body over 1 MB · `429` 100 events already pending.
|
|
122
|
+
- **Limits** — root sessions only; no active window. The run timeout
|
|
123
|
+
(`--timeout-sec`, below; 60–21600 s) stops the session — on a picked
|
|
124
|
+
session, that is the user's conversation. The body is always data to the agent,
|
|
125
|
+
never instructions: put what to do in `--prompt`. A session that has
|
|
126
|
+
taken webhook events never gets an evolution patch auto-approved (it goes
|
|
127
|
+
to review).
|
|
128
|
+
- On `update`: `--webhook on` switches to it (an existing secret is kept),
|
|
129
|
+
`--schedule …` / `--run-at …` switches away (the secret is dropped).
|
|
108
130
|
|
|
109
131
|
**Max run time (`--timeout-sec`, optional)** — each fire runs inside the
|
|
110
132
|
server as a turn in the job's session; the server stops it after
|
|
@@ -122,7 +144,10 @@ only when the user wants the runs to continue a specific conversation (the
|
|
|
122
144
|
agent then sees that session's history). Rules the user should hear once:
|
|
123
145
|
- the session keeps **its own agent and access level** — `--agent` must
|
|
124
146
|
match an existing session's agent (the helper refuses otherwise);
|
|
125
|
-
- a fire is **skipped** while
|
|
147
|
+
- a scheduled / run-now fire is **skipped** while that session is busy (a
|
|
148
|
+
turn running in it, or held by another owner); a webhook job's accepted
|
|
149
|
+
events are never skipped — they join a user's turn in progress, and wait out another owner
|
|
150
|
+
(see *How an event enters the session* above);
|
|
126
151
|
- if the user chats in that session while a run is in flight, the message
|
|
127
152
|
joins the run, and the run pushes the reply of its last turn.
|
|
128
153
|
|
|
@@ -345,7 +370,7 @@ picks it up within ~10s. Example:
|
|
|
345
370
|
For an active-window change, say when it resumes / stops — read
|
|
346
371
|
`active_from_iso` / `active_until_iso` back with `get <id>` and name the
|
|
347
372
|
first fire on or after it from the schedule (`get` has no computed next-run
|
|
348
|
-
field; the admin
|
|
373
|
+
field; the admin Triggers tab shows it):
|
|
349
374
|
|
|
350
375
|
> Paused `A-share daily brief` over the holiday: it resumes on Oct 8 —
|
|
351
376
|
> next run Oct 8 09:30 (Asia/Shanghai). Nothing is sent before that.
|
|
@@ -224,10 +224,6 @@ def require_v7_columns(conn: sqlite3.Connection):
|
|
|
224
224
|
'once so it migrates cron.db, then retry')
|
|
225
225
|
|
|
226
226
|
|
|
227
|
-
WEBHOOK_SESSION_ERR = ('a --webhook job runs in its own session (or a new one every run, '
|
|
228
|
-
'--stateless), not a picked --session')
|
|
229
|
-
|
|
230
|
-
|
|
231
227
|
def webhook_info(job_id: str, secret: str) -> dict:
|
|
232
228
|
"""What a webhook sender needs. The path goes after the server's public
|
|
233
229
|
origin (the admin URL); the script doesn't know it."""
|
|
@@ -417,10 +413,11 @@ def cmd_get(args):
|
|
|
417
413
|
def cmd_create(args):
|
|
418
414
|
if not args.workspace:
|
|
419
415
|
die('--workspace required')
|
|
420
|
-
if not args.prompt:
|
|
421
|
-
die('--prompt required')
|
|
422
416
|
has_schedule = bool(args.schedule and args.schedule.strip())
|
|
423
417
|
run_at_ms = parse_run_at(args.run_at)
|
|
418
|
+
# A webhook job's prompt is optional (its events are the message).
|
|
419
|
+
if not args.prompt and not args.webhook:
|
|
420
|
+
die('--prompt required (only a --webhook job may leave it out)')
|
|
424
421
|
if not has_schedule and run_at_ms is None and not args.webhook:
|
|
425
422
|
die('--schedule, --run-at or --webhook required')
|
|
426
423
|
if has_schedule and run_at_ms is not None:
|
|
@@ -431,8 +428,6 @@ def cmd_create(args):
|
|
|
431
428
|
die('--run-at must be in the future')
|
|
432
429
|
timeout_sec = parse_timeout_sec(args.timeout_sec)
|
|
433
430
|
session_id = parse_session(args.session)
|
|
434
|
-
if args.webhook and session_id:
|
|
435
|
-
die(WEBHOOK_SESSION_ERR)
|
|
436
431
|
working_dir = parse_dir(args.workspace, args.dir)
|
|
437
432
|
active_from = parse_active_bound('--active-from', args.active_from or '', args.timezone)
|
|
438
433
|
active_until = parse_active_bound('--active-until', args.active_until or '', args.timezone)
|
|
@@ -460,7 +455,7 @@ def cmd_create(args):
|
|
|
460
455
|
'label': args.label,
|
|
461
456
|
'workspace_path': args.workspace,
|
|
462
457
|
'agent_id': args.agent or 'default',
|
|
463
|
-
'user_prompt': args.prompt,
|
|
458
|
+
'user_prompt': args.prompt or '',
|
|
464
459
|
'schedule': args.schedule or '',
|
|
465
460
|
'run_at': run_at_ms,
|
|
466
461
|
'timezone': args.timezone,
|
|
@@ -579,8 +574,10 @@ def cmd_update(args):
|
|
|
579
574
|
merged = {**cur, **dict(sets)}
|
|
580
575
|
if not (merged.get('schedule') or '').strip() and merged.get('run_at') is None and not next_secret:
|
|
581
576
|
die('--schedule, --run-at or --webhook required (this update would leave the job with none)')
|
|
582
|
-
|
|
583
|
-
|
|
577
|
+
# Same rule as the REST PUT: only a webhook job may have an empty prompt —
|
|
578
|
+
# checked when this update touches the prompt or leaves webhook mode.
|
|
579
|
+
if not next_secret and not merged.get('user_prompt') and (args.prompt is not None or cur_secret):
|
|
580
|
+
die('--prompt required (only a webhook job may leave it empty)')
|
|
584
581
|
validate_active_window(
|
|
585
582
|
merged.get('run_at'), merged.get('active_from'), merged.get('active_until'),
|
|
586
583
|
new_until=bool(args.active_until and args.active_until.strip()), webhook=bool(next_secret),
|
|
@@ -732,13 +729,13 @@ def main():
|
|
|
732
729
|
c.add_argument('--label')
|
|
733
730
|
c.add_argument('--workspace', required=True, help='absolute workspace path')
|
|
734
731
|
c.add_argument('--agent', help="agent id (default: 'default')")
|
|
735
|
-
c.add_argument('--prompt',
|
|
732
|
+
c.add_argument('--prompt', help='the task each run is given; optional with --webhook (then the events alone)')
|
|
736
733
|
c.add_argument('--schedule', help='5-field cron expression (recurring); mutually exclusive with --run-at')
|
|
737
734
|
c.add_argument('--run-at', dest='run_at',
|
|
738
735
|
help='one-shot fire time, ISO-8601 (e.g. 2026-05-24T09:00) or unix ms — auto-disables after fire')
|
|
739
736
|
c.add_argument('--webhook', action='store_true',
|
|
740
737
|
help='fire on POST /cron-hooks/<id> instead of a schedule; prints the path + generated secret. '
|
|
741
|
-
'Excludes --schedule / --run-at /
|
|
738
|
+
'Excludes --schedule / --run-at / an active window')
|
|
742
739
|
c.add_argument('--timezone', help='IANA tz, e.g. Asia/Shanghai')
|
|
743
740
|
c.add_argument('--timeout-sec', dest='timeout_sec',
|
|
744
741
|
help='max run time in seconds, 60–21600 (default: unset = 3600)')
|