agentlife 2.6.23 → 2.6.25
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/dist/index.js +1174 -670
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// index.ts
|
|
5
|
-
import { homedir as
|
|
6
|
-
import * as
|
|
5
|
+
import { homedir as homedir15 } from "node:os";
|
|
6
|
+
import * as path18 from "node:path";
|
|
7
7
|
import { existsSync as existsSync7 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// db.ts
|
|
@@ -424,7 +424,8 @@ function getOrCreateHistoryDb(state) {
|
|
|
424
424
|
agentId TEXT NOT NULL,
|
|
425
425
|
filename TEXT NOT NULL,
|
|
426
426
|
content TEXT NOT NULL,
|
|
427
|
-
createdAt INTEGER NOT NULL
|
|
427
|
+
createdAt INTEGER NOT NULL,
|
|
428
|
+
note TEXT
|
|
428
429
|
);
|
|
429
430
|
CREATE INDEX IF NOT EXISTS idx_bv_agent ON bootstrap_versions(agentId, filename, createdAt DESC);
|
|
430
431
|
|
|
@@ -449,6 +450,9 @@ function getOrCreateHistoryDb(state) {
|
|
|
449
450
|
);
|
|
450
451
|
CREATE INDEX IF NOT EXISTS idx_pqc_createdAt ON pending_quality_checks(createdAt);
|
|
451
452
|
`);
|
|
453
|
+
try {
|
|
454
|
+
state.historyDb.exec(`ALTER TABLE bootstrap_versions ADD COLUMN note TEXT`);
|
|
455
|
+
} catch {}
|
|
452
456
|
SurfaceIndex.createSchema(state.historyDb);
|
|
453
457
|
FollowupQueue.createSchema(state.historyDb);
|
|
454
458
|
const retentionMs = 90 * 24 * 60 * 60 * 1000;
|
|
@@ -942,11 +946,13 @@ ${SIGNAL_PROTOCOL}
|
|
|
942
946
|
|
|
943
947
|
A widget is a **goal** — a persistent objective that lives on the dashboard until met or abandoned. Face = current state. \`detail:\` = full picture. \`context:\` = lookup keys.
|
|
944
948
|
|
|
949
|
+
**The \`goal:\` directive is mandatory on every widget push, including loading stubs.** Pushes without a goal are rejected with \`widget '<id>' missing 'goal:'\`. There is no warn-only grace period — declare it on the very first push.
|
|
950
|
+
|
|
945
951
|
**Goal ≠ action.** If the action you performed already fulfills it, it's a completed task, not a goal — don't push a widget.
|
|
946
952
|
|
|
947
|
-
**Goal scope — three tests:**
|
|
953
|
+
**Goal scope — three tests, run on every push:**
|
|
948
954
|
1. **Duration:** can it live across turns/sessions? If fulfilled the turn it's created, too narrow.
|
|
949
|
-
2. **Consolidation:**
|
|
955
|
+
2. **Consolidation:** scan live widgets. If any live goal differs from yours by ONLY a parameter dimension (a value that varies between instances but does not change the goal's purpose), they belong on ONE widget with that dimension as widget state. Update or consolidate (delete the parallel widgets, push the consolidated one) before adding to the proliferation.
|
|
950
956
|
3. **Followup:** can it drive a meaningful followup cycle? If "no, work is done," find the higher-level objective.
|
|
951
957
|
|
|
952
958
|
**Goal validation:** could you have written this goal text BEFORE doing the work? If yes, it's a template — the goal must be derived from your findings. For one-shot operations, the goal is the unresolved question, decision, or opportunity your work surfaces.
|
|
@@ -955,17 +961,30 @@ A widget is a **goal** — a persistent objective that lives on the dashboard un
|
|
|
955
961
|
|
|
956
962
|
### surfaceId
|
|
957
963
|
|
|
958
|
-
|
|
964
|
+
A surfaceId is bound to a goal at creation and lives until that goal is met or abandoned. EVERY push toward an active goal — progress, findings, interventions, mid-goal questions — updates the SAME surfaceId.
|
|
965
|
+
|
|
966
|
+
Before minting a new surfaceId, scan Current Dashboard State:
|
|
967
|
+
- If a live widget's goal subsumes what you're about to push, update that surfaceId.
|
|
968
|
+
- If you need to ask the user something mid-goal, push a transient \`input\` surface (input bar, never the dashboard).
|
|
969
|
+
- If one finding affects multiple live goals, push it on the goal with the nearest deadline or the most recent user activity. Never split a finding across widgets.
|
|
970
|
+
|
|
971
|
+
A new dashboard surfaceId is reserved for a goal whose success conditions are not satisfied by any active widget — typically when the parent goal is complete (delete it in the same turn) or the user has opened a domain unrelated to anything alive on the dashboard. Naming encodes the owning agent and the goal's persistent intent — never a date, parameter value, or moment-specific intervention.
|
|
972
|
+
|
|
973
|
+
Goal complete: delete the surface. The deletion plus dismiss notification IS the closure — do not push a completion / acknowledgment widget.
|
|
959
974
|
|
|
960
975
|
### followup — the next step
|
|
961
976
|
|
|
962
|
-
\`followup:\` is
|
|
977
|
+
\`followup:\` is the next concrete step toward the goal. Platform schedules a cron from it; push → cron; update → replaced; delete → cleared. Never create crons directly. Every live widget MUST have one — orphans are a quality error.
|
|
978
|
+
|
|
979
|
+
The followup is a hypothesis derived from THIS cycle's findings — a check whose outcome would change your next move. Every followup must:
|
|
980
|
+
- Name a signal observable at the next fire that does not exist now (a value crossing a threshold, an absence persisting, a deadline elapsing, an action taken or not).
|
|
981
|
+
- State the threshold or condition that, if observed, would change your next action.
|
|
982
|
+
- Imply what you'll do under EACH outcome — never use a generic data operation (query / recompute / update) as a branch.
|
|
983
|
+
- Use a delay that matches when the answer can change. If nothing material can change before then, the delay is wrong.
|
|
963
984
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
- **Must advance the goal.** "Check and delete if stale" is a template, not a step. If there's no meaningful next step, the goal is wrong — fix the goal.
|
|
968
|
-
- **On fire:** query fresh data, evaluate progress, update the widget, set the next followup. Goal met or abandoned → delete the widget.
|
|
985
|
+
If you produce a followup whose residue (digits and parameter values stripped) matches a prior followup on this surface, the platform flags it as templated and the agent receives a [QUALITY ERROR]. Re-derive from the cycle's findings, or — if the situation has not materially changed — delete the widget. A stagnant goal is not a goal.
|
|
986
|
+
|
|
987
|
+
**On fire:** query fresh data → identify what changed since last fire → update the widget face/detail with the change → write the next followup as the next hypothesis. Goal met or abandoned → delete the widget. Do NOT carry forward the previous followup's procedure with new parameters.
|
|
969
988
|
|
|
970
989
|
### Guided mode — input surfaces
|
|
971
990
|
|
|
@@ -986,9 +1005,11 @@ Button shapes and dispatch format for input surfaces are in **Signal Protocol**
|
|
|
986
1005
|
|
|
987
1006
|
**Goal met → delete immediately.** \`delete <surfaceId>\`. No cleanup followups, no passive expiry.
|
|
988
1007
|
|
|
989
|
-
**Action fulfills goal → delete the source widget in the same turn.**
|
|
1008
|
+
**Action fulfills goal → delete the source widget in the same turn.** Only a complete goal yields a successor surfaceId. An action that advances the goal but doesn't complete it stays on the same surfaceId — update the face, don't mint.
|
|
990
1009
|
|
|
991
|
-
**One widget, one goal
|
|
1010
|
+
**One widget, one goal — symmetric rule.**
|
|
1011
|
+
- Same surfaceId → goal text MUST NOT change (platform rejects with QUALITY ERROR \`goal_changed\`).
|
|
1012
|
+
- Same goal → MUST stay on the same surfaceId. A refinement, sub-step, or intervention toward an existing live goal is not a new goal. The dashboard is one widget per active goal, not one widget per moment.
|
|
992
1013
|
|
|
993
1014
|
**User-initiated dismiss** (\`[system:dismiss-requested] surfaceId=<id>\`): you have ~30s to push an \`input\` surface with 2-3 contextual options + "Remove it" last. If the user picks an alternative, act on it and the dismiss is cancelled. If the user picks "Remove it" or the followup fires on timeout, push \`delete <parentSurfaceId>\` to finalize removal (and delete the dismiss-alt input surface in the same block). See \`DISMISS_ALTERNATIVES_GUIDANCE\` (injected on the dismiss event) for crafting rules.
|
|
994
1015
|
|
|
@@ -1068,6 +1089,45 @@ The platform tracks your health: \`healthy\` → \`watch\` → \`review\`.
|
|
|
1068
1089
|
- Max 3 bootstrap rewrites per 7 days — the platform blocks excess rewrites.
|
|
1069
1090
|
- 48h post-rewrite validation: if dismiss rate increases >20 points or followup effectiveness drops >25 points, the rewrite is flagged as regression and Supervisor is notified for rollback.
|
|
1070
1091
|
- If your metrics have drifted significantly from baseline, further rewrites are flagged. Stabilize before rewriting again.
|
|
1092
|
+
|
|
1093
|
+
### Annotating Refinements (\`agentlife.evolutions.note\`)
|
|
1094
|
+
|
|
1095
|
+
When a rewrite meaningfully changes how you behave for the user, immediately call \`agentlife.evolutions.note\` with a one-line plain-language summary. The note attaches to the snapshot of the file taken before your rewrite and surfaces in the user's Refinements rail on your agent detail page. The user reads it to understand what you changed and why.
|
|
1096
|
+
|
|
1097
|
+
Params: \`{ agentId, filename, note }\`. Filename must be one of \`AGENTS.md\`, \`USER.md\`, \`SOUL.md\`, \`TOOLS.md\`, \`IDENTITY.md\`. Note is capped at 280 characters.
|
|
1098
|
+
|
|
1099
|
+
Notes must:
|
|
1100
|
+
- Be written for the user, not for you. Plain language only — no internal terms like signal weights, drift heuristics, or rule names. Describe the user-visible behavior change.
|
|
1101
|
+
- Name the concrete behavior shift and the trigger that prompted it. The user wants to know what's different now and why you decided to change it.
|
|
1102
|
+
- Be skipped entirely for housekeeping rewrites (formatting, dead-rule cleanup, comment edits). Silent rewrites are still snapshotted for audit but won't surface in the rail. Don't pad the rail with noise — the bar is "the user would notice this change in their next interaction with me".
|
|
1103
|
+
|
|
1104
|
+
### Handling \`user_reverted\`
|
|
1105
|
+
|
|
1106
|
+
When you receive a \`[system] User reverted ...\` message on this session, the user has rolled back one of your bootstrap files to a prior version. Their judgment is authoritative — they preferred the older version to whatever you wrote.
|
|
1107
|
+
|
|
1108
|
+
1. Read the current file (now the restored older content) and recall what you changed in the rewrite they undid.
|
|
1109
|
+
2. Treat this as a strong signal that the change you made was wrong for this user, even if your metrics looked fine.
|
|
1110
|
+
3. Do not re-rewrite the same change. If a similar refinement is genuinely needed later, it must take a different shape — different framing, different trigger, different scope.
|
|
1111
|
+
4. If the message includes a \`User note\`, weight it heavily — it's the qualitative reason the metrics didn't capture.
|
|
1112
|
+
5. Respond with a short text summary of what you understood and what you'll do differently. Terminate with \`NO_REPLY\`.
|
|
1113
|
+
|
|
1114
|
+
Do not call \`agentlife.evolutions.restore\` from here — restores are a user action, never an agent action.
|
|
1115
|
+
|
|
1116
|
+
### Handling \`Rewrite regression detected\` (Supervisor only)
|
|
1117
|
+
|
|
1118
|
+
When a \`[system] Rewrite regression detected for {agentId}/{filename}\` message arrives, the platform's 48h validation has determined that a recent rewrite worsened the affected agent's metrics. The user does not yet know.
|
|
1119
|
+
|
|
1120
|
+
Your job in this internal session is to relay a structured handoff to the affected agent's DIRECT session so it can surface the regression to the user as a widget. Do NOT push a widget yourself — this internal session has no Widget DSL catalog, and the affected agent owns its own surfaces.
|
|
1121
|
+
|
|
1122
|
+
1. Parse the message: extract \`agentId\`, \`filename\`, the before/after dismiss rate and followup percentage.
|
|
1123
|
+
2. Look up the most recent snapshot's \`versionId\` for that file via \`agentlife.evolutions.list { agentId, filename, limit: 1 }\` — this is the version the user would restore.
|
|
1124
|
+
3. Send a structured handoff to the affected agent's direct session via \`sessions_send\` with \`sessionKey="agent:{agentId}:agentlife:direct:operator"\` and a message of the exact form:
|
|
1125
|
+
\`\`\`
|
|
1126
|
+
[system] regression-revert filename={filename} versionId={N} dismissBefore={X} dismissAfter={Y} followupBefore={Z} followupAfter={W}
|
|
1127
|
+
\`\`\`
|
|
1128
|
+
4. Use \`timeoutSeconds=0\` (fire-and-forget). Then \`NO_REPLY\`.
|
|
1129
|
+
|
|
1130
|
+
Do not push your own widget, do not summarize the regression to the user, do not modify the affected agent's files. Your role here is the relay only — the affected agent decides how to frame the regression for its user.
|
|
1071
1131
|
`;
|
|
1072
1132
|
var WIDGET_DSL_GUIDANCE = `## WidgetDSL — How to Build Widgets
|
|
1073
1133
|
|
|
@@ -1124,11 +1184,16 @@ followup: +<duration> "<next concrete step — what to check, what data to query
|
|
|
1124
1184
|
context: <JSON — lookup keys: DB tables, date ranges, phase, config>
|
|
1125
1185
|
\`\`\`
|
|
1126
1186
|
|
|
1187
|
+
\`goal:\` is **mandatory on every widget push** — the platform rejects pushes
|
|
1188
|
+
without one ("widget '<id>' missing 'goal:'"). The only exemption is a
|
|
1189
|
+
re-push that omits \`goal:\` while the existing widget already has one in
|
|
1190
|
+
the index — the previous goal is preserved (data-model refresh pattern).
|
|
1191
|
+
|
|
1127
1192
|
### Two-Phase Push — Loading Then Final
|
|
1128
1193
|
|
|
1129
|
-
Phase 1 — your FIRST tool call on every turn. Before any search, fetch, exec, or db call, push a widget with \`state=loading\` in the header.
|
|
1194
|
+
Phase 1 — your FIRST tool call on every turn. Before any search, fetch, exec, or db call, push a widget with \`state=loading\` in the header. **The loading push must already include \`goal:\`** — the user's request IS the goal, and they need to read your intent while you're working, not after. \`state=loading\` controls only the animated-border affordance; it doesn't relax the goal requirement.
|
|
1130
1195
|
|
|
1131
|
-
Phase 2 — your LAST tool call. Same surfaceId, without \`state=loading\`, with the real result (face,
|
|
1196
|
+
Phase 2 — your LAST tool call. Same surfaceId, without \`state=loading\`, with the real result (face, detail, followup, context). \`goal:\` carries over automatically when omitted in a re-push, but you can re-state it if your understanding sharpened during the work.
|
|
1132
1197
|
|
|
1133
1198
|
A loading push without a final push = perpetual spinner.
|
|
1134
1199
|
|
|
@@ -1207,6 +1272,41 @@ The platform provides two storage systems. Do NOT use OpenClaw memory files (mem
|
|
|
1207
1272
|
- OpenClaw memory files (memory_search, memory_get, memory/*.md)
|
|
1208
1273
|
- Workspace files for state
|
|
1209
1274
|
- Session history as knowledge
|
|
1275
|
+
|
|
1276
|
+
### Handling \`[system] regression-revert\`
|
|
1277
|
+
|
|
1278
|
+
The Supervisor relays this when the platform's 48h post-rewrite validation determines that a recent rewrite of one of YOUR bootstrap files worsened your metrics. The user does not yet know.
|
|
1279
|
+
|
|
1280
|
+
Message shape:
|
|
1281
|
+
\`\`\`
|
|
1282
|
+
[system] regression-revert filename={filename} versionId={N} dismissBefore={X} dismissAfter={Y} followupBefore={Z} followupAfter={W}
|
|
1283
|
+
\`\`\`
|
|
1284
|
+
|
|
1285
|
+
Push exactly one widget for this — same surfaceId is reserved for this purpose, never reuse it for other goals:
|
|
1286
|
+
|
|
1287
|
+
\`\`\`
|
|
1288
|
+
surface {agentId}-regression-revert size=m priority=high
|
|
1289
|
+
card
|
|
1290
|
+
column
|
|
1291
|
+
text "I think a recent change made things worse" h3
|
|
1292
|
+
row distribute=spaceBetween
|
|
1293
|
+
metric "Dismiss" "{X}% → {Y}%"
|
|
1294
|
+
metric "Followups" "{Z}% → {W}%"
|
|
1295
|
+
divider
|
|
1296
|
+
text "I rewrote {filename} and the numbers got worse, not better. Revert returns to the prior version. Keep dismisses this notice and lets me try a different approach next time." body
|
|
1297
|
+
row distribute=spaceBetween
|
|
1298
|
+
button "Revert" action=revert primary
|
|
1299
|
+
button "Keep" action=keep
|
|
1300
|
+
goal: Decide whether to revert the recent {filename} rewrite based on the user's judgment, since metrics suggest it was worse.
|
|
1301
|
+
detail:
|
|
1302
|
+
Plain-language summary of what changed in your last rewrite of {filename}, in user-facing terms (no signal-weight or metric jargon). Include the dismiss-rate and followup-effectiveness deltas. Do not blame the user.
|
|
1303
|
+
followup: +24h "If still alive, the user has neither reverted nor explicitly kept. Re-evaluate metrics — if they've recovered, delete this widget; if still degraded, keep it but do not nag."
|
|
1304
|
+
context: {"filename":"{filename}","versionId":{N}}
|
|
1305
|
+
\`\`\`
|
|
1306
|
+
|
|
1307
|
+
Set the version into \`context:\` so the action handler can read it back. When you receive \`[action:revert]\` on this surface: call \`agentlife.evolutions.restore { versionId, userNote: null }\` (the user already gave their judgment by clicking — no extra note needed), then delete the surface in the same turn. When you receive \`[action:keep]\`: just delete the surface — no platform call needed; the regression flag remains in the audit trail but you stop asking.
|
|
1308
|
+
|
|
1309
|
+
Do not push a regression-revert widget without receiving the \`[system] regression-revert\` message — it must be triggered by the platform, never speculative.
|
|
1210
1310
|
`;
|
|
1211
1311
|
var DISMISS_ALTERNATIVES_GUIDANCE = `### Crafting Dismiss Alternatives
|
|
1212
1312
|
|
|
@@ -1935,11 +2035,6 @@ function matchMetadata(block, key) {
|
|
|
1935
2035
|
}
|
|
1936
2036
|
return v || null;
|
|
1937
2037
|
}
|
|
1938
|
-
function isLoadingPush(rawDsl) {
|
|
1939
|
-
const header = rawDsl.split(`
|
|
1940
|
-
`, 1)[0] ?? "";
|
|
1941
|
-
return /\bstate=loading\b/.test(header);
|
|
1942
|
-
}
|
|
1943
2038
|
function hasDetailContent(rawDsl) {
|
|
1944
2039
|
const lines = rawDsl.split(`
|
|
1945
2040
|
`);
|
|
@@ -2341,183 +2436,21 @@ function broadcastInput(message, sessionKey) {
|
|
|
2341
2436
|
return;
|
|
2342
2437
|
broadcastRef("plugin.agentlife.input", { message, sessionKey, timestamp: Date.now() });
|
|
2343
2438
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const autoDeletedInputIds = [];
|
|
2355
|
-
if (opts.evictStaleInputs !== false) {
|
|
2356
|
-
const incomingInputIds = new Set;
|
|
2357
|
-
for (const b of blocks) {
|
|
2358
|
-
if (b.kind === "push" && b.isInput)
|
|
2359
|
-
incomingInputIds.add(b.surfaceId);
|
|
2360
|
-
}
|
|
2361
|
-
if (incomingInputIds.size > 0) {
|
|
2362
|
-
autoDeletedInputIds.push(...state.transientSurfaces.takeStaleInputs(incomingInputIds));
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
for (const block of blocks) {
|
|
2366
|
-
if (block.kind === "delete") {
|
|
2367
|
-
const result2 = await handleDelete(state, block);
|
|
2368
|
-
results.push(result2);
|
|
2369
|
-
continue;
|
|
2370
|
-
}
|
|
2371
|
-
const result = await handlePush(state, block, opts);
|
|
2372
|
-
results.push(result);
|
|
2373
|
-
}
|
|
2374
|
-
broadcastSurface(dsl);
|
|
2375
|
-
for (const sid of autoDeletedInputIds)
|
|
2376
|
-
broadcastDelete(sid);
|
|
2377
|
-
return results;
|
|
2378
|
-
}
|
|
2379
|
-
async function handleDelete(state, block) {
|
|
2380
|
-
const sid = block.surfaceId;
|
|
2381
|
-
const agentId = state.surfaceIndex.get(sid)?.agentId ?? state.transientSurfaces.get(sid)?.agentId ?? null;
|
|
2382
|
-
state.transientSurfaces.delete(sid);
|
|
2383
|
-
const indexed = state.surfaceIndex.get(sid);
|
|
2384
|
-
if (indexed) {
|
|
2385
|
-
try {
|
|
2386
|
-
await state.fileSurfaceStorage.delete(indexed.agentId, sid);
|
|
2387
|
-
} catch (err) {
|
|
2388
|
-
console.warn("[push] file delete failed for %s: %s", sid, err?.message);
|
|
2389
|
-
}
|
|
2390
|
-
state.surfaceIndex.delete(sid);
|
|
2391
|
-
}
|
|
2392
|
-
state.followupQueue.cancel(sid);
|
|
2393
|
-
recordSurfaceEvent(state, sid, "deleted", undefined, agentId ?? undefined);
|
|
2394
|
-
broadcastDelete(sid);
|
|
2395
|
-
return {
|
|
2396
|
-
surfaceId: sid,
|
|
2397
|
-
kind: "delete",
|
|
2398
|
-
isNew: false,
|
|
2399
|
-
isTransient: false,
|
|
2400
|
-
followupChanged: false,
|
|
2401
|
-
followupRemoved: true,
|
|
2402
|
-
goalChanged: null,
|
|
2403
|
-
contextDroppedFields: [],
|
|
2404
|
-
validation: emptyValidation()
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
async function handlePush(state, block, opts) {
|
|
2408
|
-
const now = Date.now();
|
|
2409
|
-
const validation = validateBlockDsl(block.rawDsl);
|
|
2410
|
-
if (block.isTransient) {
|
|
2411
|
-
const existing2 = state.transientSurfaces.get(block.surfaceId);
|
|
2412
|
-
state.transientSurfaces.set(opts.agentId, block, opts.sessionKey ?? null);
|
|
2413
|
-
recordSurfaceEvent(state, block.surfaceId, existing2 ? "updated" : "created", block.rawDsl, opts.agentId);
|
|
2414
|
-
return {
|
|
2415
|
-
surfaceId: block.surfaceId,
|
|
2416
|
-
kind: "push",
|
|
2417
|
-
isNew: !existing2,
|
|
2418
|
-
isTransient: true,
|
|
2419
|
-
followupChanged: false,
|
|
2420
|
-
followupRemoved: false,
|
|
2421
|
-
goalChanged: null,
|
|
2422
|
-
contextDroppedFields: [],
|
|
2423
|
-
validation
|
|
2424
|
-
};
|
|
2425
|
-
}
|
|
2426
|
-
const existing = state.surfaceIndex.get(block.surfaceId);
|
|
2427
|
-
const filePath = await state.fileSurfaceStorage.write(opts.agentId, block.surfaceId, block.rawDsl);
|
|
2428
|
-
const resolvedContext = block.context ?? existing?.context ?? null;
|
|
2429
|
-
const contextDroppedFields = block.context && existing?.context ? Object.keys(existing.context).filter((k) => !(k in block.context)) : [];
|
|
2430
|
-
const resolvedGoal = block.goal ?? existing?.goal ?? null;
|
|
2431
|
-
const goalChanged = existing?.goal && block.goal && existing.goal !== block.goal ? { from: existing.goal, to: block.goal } : null;
|
|
2432
|
-
const newState = "active";
|
|
2433
|
-
const nextEntry = {
|
|
2434
|
-
surfaceId: block.surfaceId,
|
|
2435
|
-
agentId: existing?.agentId ?? opts.agentId,
|
|
2436
|
-
path: filePath,
|
|
2437
|
-
rawDsl: block.rawDsl,
|
|
2438
|
-
state: newState,
|
|
2439
|
-
isOverlay: false,
|
|
2440
|
-
dashboardVisible: existing?.dashboardVisible ?? true,
|
|
2441
|
-
createdAt: existing?.createdAt ?? now,
|
|
2442
|
-
updatedAt: now,
|
|
2443
|
-
expiredSince: null,
|
|
2444
|
-
originSessionKey: opts.sessionKey ?? existing?.originSessionKey ?? null,
|
|
2445
|
-
goal: resolvedGoal,
|
|
2446
|
-
context: resolvedContext,
|
|
2447
|
-
followup: block.followup,
|
|
2448
|
-
chainRoot: block.chainRoot,
|
|
2449
|
-
chainParent: block.chainParent,
|
|
2450
|
-
chainPosition: block.chainPosition,
|
|
2451
|
-
chainStatus: block.chainStatus,
|
|
2452
|
-
parseError: null
|
|
2453
|
-
};
|
|
2454
|
-
try {
|
|
2455
|
-
state.surfaceIndex.upsert(nextEntry);
|
|
2456
|
-
} catch (err) {
|
|
2457
|
-
console.warn("[push] index upsert failed for %s: %s", block.surfaceId, err?.message);
|
|
2458
|
-
}
|
|
2459
|
-
const oldFollowup = existing?.followup ?? null;
|
|
2460
|
-
const newFollowup = block.followup ?? null;
|
|
2461
|
-
const followupChanged = oldFollowup !== newFollowup;
|
|
2462
|
-
const followupRemoved = !!oldFollowup && !newFollowup;
|
|
2463
|
-
try {
|
|
2464
|
-
if (followupRemoved || !newFollowup) {
|
|
2465
|
-
state.followupQueue.cancel(block.surfaceId);
|
|
2466
|
-
} else if (followupChanged || !existing) {
|
|
2467
|
-
const spec = parseFollowupSpec(newFollowup);
|
|
2468
|
-
if (spec) {
|
|
2469
|
-
state.followupQueue.upsert({
|
|
2470
|
-
surfaceId: block.surfaceId,
|
|
2471
|
-
agentId: nextEntry.agentId,
|
|
2472
|
-
fireAt: now + spec.timeOffsetMs,
|
|
2473
|
-
instruction: spec.message,
|
|
2474
|
-
createdAt: now
|
|
2475
|
-
});
|
|
2476
|
-
} else {
|
|
2477
|
-
console.warn("[push] unparseable followup on %s: %s", block.surfaceId, newFollowup);
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
} catch (err) {
|
|
2481
|
-
console.warn("[push] followup queue update failed for %s: %s", block.surfaceId, err?.message);
|
|
2482
|
-
}
|
|
2483
|
-
recordSurfaceEvent(state, block.surfaceId, existing ? "updated" : "created", block.rawDsl, nextEntry.agentId);
|
|
2484
|
-
return {
|
|
2485
|
-
surfaceId: block.surfaceId,
|
|
2486
|
-
kind: "push",
|
|
2487
|
-
isNew: !existing,
|
|
2488
|
-
isTransient: false,
|
|
2489
|
-
followupChanged,
|
|
2490
|
-
followupRemoved,
|
|
2491
|
-
goalChanged,
|
|
2492
|
-
contextDroppedFields,
|
|
2493
|
-
validation
|
|
2494
|
-
};
|
|
2495
|
-
}
|
|
2496
|
-
function emptyValidation() {
|
|
2497
|
-
return {
|
|
2498
|
-
unknownKeywords: [],
|
|
2499
|
-
metadataWithoutColon: [],
|
|
2500
|
-
missingCardStructure: false,
|
|
2501
|
-
invalidInputActions: [],
|
|
2502
|
-
unknownAttrs: [],
|
|
2503
|
-
invalidEnumValues: []
|
|
2504
|
-
};
|
|
2439
|
+
function broadcastNotification(event, surfaceId, title, body) {
|
|
2440
|
+
if (!broadcastRef)
|
|
2441
|
+
return;
|
|
2442
|
+
broadcastRef("plugin.agentlife.notification", {
|
|
2443
|
+
event,
|
|
2444
|
+
surfaceId,
|
|
2445
|
+
title,
|
|
2446
|
+
body,
|
|
2447
|
+
timestamp: Date.now()
|
|
2448
|
+
});
|
|
2505
2449
|
}
|
|
2506
2450
|
|
|
2507
|
-
//
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
const cfg = runtime.config.loadConfig();
|
|
2511
|
-
const list = cfg?.agents?.list ?? [];
|
|
2512
|
-
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2513
|
-
}
|
|
2514
|
-
function isOnboarding(_state, runtime) {
|
|
2515
|
-
return userAgentIds(runtime).length === 0;
|
|
2516
|
-
}
|
|
2517
|
-
function snapshotOnboarding(state, runtime) {
|
|
2518
|
-
const count = userAgentIds(runtime).length;
|
|
2519
|
-
return { isActive: count === 0, userAgentCount: count };
|
|
2520
|
-
}
|
|
2451
|
+
// dashboard-state.ts
|
|
2452
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2453
|
+
import { homedir as homedir3 } from "node:os";
|
|
2521
2454
|
|
|
2522
2455
|
// dsl/expiry.ts
|
|
2523
2456
|
var DEFAULT_CEILING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -2671,235 +2604,30 @@ function indexedToView(rawDsl, e) {
|
|
|
2671
2604
|
};
|
|
2672
2605
|
}
|
|
2673
2606
|
|
|
2674
|
-
//
|
|
2675
|
-
function
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2607
|
+
// dashboard-state.ts
|
|
2608
|
+
function extractTitleAndDetail(meta) {
|
|
2609
|
+
let title = null;
|
|
2610
|
+
let detail = null;
|
|
2611
|
+
const fullText = meta.lines.join(`
|
|
2612
|
+
`);
|
|
2613
|
+
const titleMatch = fullText.match(/text\s+"([^"]+)"\s+h[34]/);
|
|
2614
|
+
if (titleMatch)
|
|
2615
|
+
title = titleMatch[1];
|
|
2616
|
+
if (!title) {
|
|
2617
|
+
const bodyMatch = fullText.match(/text\s+"([^"]+)"/);
|
|
2618
|
+
if (bodyMatch)
|
|
2619
|
+
title = bodyMatch[1];
|
|
2680
2620
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
Each turn:
|
|
2693
|
-
1. Decide: do you have enough signal? Minimum = name/persona hint + 2–3 concrete life areas. Don't drag past 4 turns.
|
|
2694
|
-
2. Not enough → \`agentlife_push\` the SAME surfaceId \`welcome-input\` with:
|
|
2695
|
-
- h3 question generated from what the user just said
|
|
2696
|
-
- 2–4 multichoice buttons \`action=choice\`, options derived from the user's own words (NEVER hardcoded domain lists)
|
|
2697
|
-
- \`textfield placeholder="Something else…"\` escape hatch
|
|
2698
|
-
- updated \`context: {"phase":"welcome","turn":N+1,"answers":[...]}\`
|
|
2699
|
-
- goal + followup unchanged
|
|
2700
|
-
Respond \`done\`.
|
|
2701
|
-
3. Enough → create the generalist and hand off the dashboard:
|
|
2702
|
-
- Workspace under \`$HOME/.openclaw/workspace-{id}\` (id = slugified short name or "me")
|
|
2703
|
-
- \`AGENTS.md\` — generalist scope covering mentioned domains; Data Schema must include a \`domain TEXT NOT NULL\` column on every user-data table so future split by domain is trivial; add self-evolution clause ("when activity_log rows for one domain exceed a natural threshold, push a split-suggest widget")
|
|
2704
|
-
- \`SOUL.md\`, \`IDENTITY.md\`, \`USER.md\` (user's own words), \`HEARTBEAT.md\` (empty)
|
|
2705
|
-
- \`exec openclaw gateway call agentlife.createAgent --params '{...}'\` with \`tools: {profile:"full", alsoAllow:["agentlife_push"]}\`. Plugin auto-deletes welcome + welcome-input on success.
|
|
2706
|
-
- **Final step, same turn, before \`done\`:** push ONE actionable first widget for the newly-created agent — an input prompt ("Log your first meal", "Enter today's weight"), a starter metric, or an inviting CTA that owns a surfaceId like \`{id}-today\` / \`{id}-start\`. This is what the user sees when onboarding ends; it MUST invite interaction, not just announce existence. Then delete your \`{domain}-building\` loading widget. Then emit \`done\`.
|
|
2707
|
-
`;
|
|
2708
|
-
function renderWelcomeWidget(state, log) {
|
|
2709
|
-
const welcomeDsl = [
|
|
2710
|
-
`surface ${WELCOME_SURFACE_ID} size=m`,
|
|
2711
|
-
` card`,
|
|
2712
|
-
` column`,
|
|
2713
|
-
` text "\uD83D\uDC4B Welcome to Agent Life" h3`,
|
|
2714
|
-
` text "Answer below and I'll build your first agent from it." body`,
|
|
2715
|
-
` divider`,
|
|
2716
|
-
` badge "Setup" color=#6366F1 outlined`,
|
|
2717
|
-
`goal: Welcome the user and anchor the onboarding flow`,
|
|
2718
|
-
`followup: +24h "If still zero agents, push an encouraging nudge."`,
|
|
2719
|
-
`context: {"phase":"welcome","autoRendered":true}`
|
|
2720
|
-
].join(`
|
|
2721
|
-
`);
|
|
2722
|
-
pushDsl(state, welcomeDsl, { agentId: SYSTEM_AGENT_ID }).then(() => log(`[render-widgets] rendered ${WELCOME_SURFACE_ID}`)).catch((e) => console.warn("[render-widgets] welcome push failed: %s", e?.message));
|
|
2723
|
-
const welcomeInputDsl = [
|
|
2724
|
-
`surface ${WELCOME_INPUT_SURFACE_ID} input`,
|
|
2725
|
-
` card`,
|
|
2726
|
-
` column`,
|
|
2727
|
-
` text "What do you want to track or improve?" h3`,
|
|
2728
|
-
` text "One line. Anything — meals, money, a project, a habit." body`,
|
|
2729
|
-
` textfield placeholder="Type anything…"`,
|
|
2730
|
-
`goal: Onboard the user to their first agent via a guided interview`,
|
|
2731
|
-
`followup: +30m "If unanswered, replace the question with a gentler prompt."`,
|
|
2732
|
-
`context: {"phase":"welcome","turn":1,"answers":[]}`
|
|
2733
|
-
].join(`
|
|
2734
|
-
`);
|
|
2735
|
-
pushDsl(state, welcomeInputDsl, { agentId: SYSTEM_AGENT_ID, sessionKey: ONBOARDING_SESSION_KEY }).then(() => log(`[render-widgets] rendered ${WELCOME_INPUT_SURFACE_ID} (onboarding session)`)).catch((e) => console.warn("[render-widgets] welcome-input push failed: %s", e?.message));
|
|
2736
|
-
}
|
|
2737
|
-
function deleteWelcomeWidget(state, log) {
|
|
2738
|
-
for (const id of [WELCOME_SURFACE_ID, WELCOME_INPUT_SURFACE_ID]) {
|
|
2739
|
-
if (!hasSurface(state, id))
|
|
2740
|
-
continue;
|
|
2741
|
-
const view = getSurface(state, id);
|
|
2742
|
-
if (view && !view.isTransient && view.path) {
|
|
2743
|
-
try {
|
|
2744
|
-
fs4.unlinkSync(view.path);
|
|
2745
|
-
} catch {}
|
|
2746
|
-
}
|
|
2747
|
-
state.surfaceIndex?.delete(id);
|
|
2748
|
-
state.transientSurfaces.delete(id);
|
|
2749
|
-
state.followupQueue?.cancel(id);
|
|
2750
|
-
purgeSurfaceHistory(state, id);
|
|
2751
|
-
broadcastDelete(id);
|
|
2752
|
-
log(`[render-widgets] deleted ${id}`);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
// services/surfaces-init.ts
|
|
2757
|
-
import * as path5 from "node:path";
|
|
2758
|
-
|
|
2759
|
-
// storage/reconciler.ts
|
|
2760
|
-
var defaultLogger = {
|
|
2761
|
-
log: (...args) => console.log(...args),
|
|
2762
|
-
warn: (...args) => console.warn(...args)
|
|
2763
|
-
};
|
|
2764
|
-
function reconcile(params) {
|
|
2765
|
-
const { storage, index, queue, knownAgents } = params;
|
|
2766
|
-
const log = params.logger ?? defaultLogger;
|
|
2767
|
-
const result = {
|
|
2768
|
-
indexed: 0,
|
|
2769
|
-
parseErrors: 0,
|
|
2770
|
-
orphanFiles: 0,
|
|
2771
|
-
staleIndexRowsDropped: 0,
|
|
2772
|
-
followupsScheduled: 0,
|
|
2773
|
-
staleQueueRowsCancelled: 0
|
|
2774
|
-
};
|
|
2775
|
-
const { surfaces, orphans } = storage.listAll(knownAgents);
|
|
2776
|
-
if (orphans.length > 0) {
|
|
2777
|
-
result.orphanFiles = orphans.length;
|
|
2778
|
-
const byAgent = new Map;
|
|
2779
|
-
for (const o of orphans)
|
|
2780
|
-
byAgent.set(o.agentId, (byAgent.get(o.agentId) ?? 0) + 1);
|
|
2781
|
-
for (const [agentId, count] of byAgent) {
|
|
2782
|
-
log.warn(`[reconcile] orphan workspace: ${agentId} (${count} file(s)) — agent not in agents.list, skipping`);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
const foundSids = new Set;
|
|
2786
|
-
for (const file of surfaces) {
|
|
2787
|
-
let block = null;
|
|
2788
|
-
let parseError = null;
|
|
2789
|
-
try {
|
|
2790
|
-
const parsed = parseBlock(file.dsl);
|
|
2791
|
-
if (!parsed) {
|
|
2792
|
-
parseError = "no surface header";
|
|
2793
|
-
} else if (parsed.kind === "delete") {
|
|
2794
|
-
parseError = "file contains only a delete directive";
|
|
2795
|
-
} else {
|
|
2796
|
-
block = parsed;
|
|
2797
|
-
}
|
|
2798
|
-
} catch (err) {
|
|
2799
|
-
parseError = err?.message ?? String(err);
|
|
2800
|
-
}
|
|
2801
|
-
if (parseError || !block) {
|
|
2802
|
-
index.upsertParseError({
|
|
2803
|
-
surfaceId: file.surfaceId,
|
|
2804
|
-
agentId: file.agentId,
|
|
2805
|
-
path: file.path,
|
|
2806
|
-
rawDsl: file.dsl,
|
|
2807
|
-
error: parseError ?? "unknown parse error"
|
|
2808
|
-
});
|
|
2809
|
-
foundSids.add(file.surfaceId);
|
|
2810
|
-
result.parseErrors++;
|
|
2811
|
-
log.warn(`[reconcile] parse error in ${file.surfaceId}: ${parseError}`);
|
|
2812
|
-
continue;
|
|
2813
|
-
}
|
|
2814
|
-
const now = Date.now();
|
|
2815
|
-
const existing = index.get(file.surfaceId);
|
|
2816
|
-
const entry = {
|
|
2817
|
-
surfaceId: block.surfaceId,
|
|
2818
|
-
agentId: file.agentId,
|
|
2819
|
-
path: file.path,
|
|
2820
|
-
rawDsl: block.rawDsl,
|
|
2821
|
-
state: existing?.state ?? "active",
|
|
2822
|
-
isOverlay: false,
|
|
2823
|
-
dashboardVisible: existing?.dashboardVisible ?? true,
|
|
2824
|
-
createdAt: existing?.createdAt ?? now,
|
|
2825
|
-
updatedAt: existing?.updatedAt ?? now,
|
|
2826
|
-
expiredSince: existing?.expiredSince ?? null,
|
|
2827
|
-
originSessionKey: existing?.originSessionKey ?? null,
|
|
2828
|
-
goal: block.goal,
|
|
2829
|
-
context: block.context,
|
|
2830
|
-
followup: block.followup,
|
|
2831
|
-
chainRoot: block.chainRoot,
|
|
2832
|
-
chainParent: block.chainParent,
|
|
2833
|
-
chainPosition: block.chainPosition,
|
|
2834
|
-
chainStatus: block.chainStatus,
|
|
2835
|
-
parseError: null
|
|
2836
|
-
};
|
|
2837
|
-
index.upsert(entry);
|
|
2838
|
-
foundSids.add(file.surfaceId);
|
|
2839
|
-
result.indexed++;
|
|
2840
|
-
if (block.followup) {
|
|
2841
|
-
const spec = parseFollowupSpec(block.followup);
|
|
2842
|
-
if (spec) {
|
|
2843
|
-
const pending2 = queue.nextPending(file.surfaceId);
|
|
2844
|
-
if (!pending2) {
|
|
2845
|
-
queue.upsert({
|
|
2846
|
-
surfaceId: file.surfaceId,
|
|
2847
|
-
agentId: file.agentId,
|
|
2848
|
-
fireAt: now + spec.timeOffsetMs,
|
|
2849
|
-
instruction: spec.message,
|
|
2850
|
-
createdAt: now
|
|
2851
|
-
});
|
|
2852
|
-
result.followupsScheduled++;
|
|
2853
|
-
}
|
|
2854
|
-
} else {
|
|
2855
|
-
log.warn(`[reconcile] unparseable followup on ${file.surfaceId}: ${block.followup}`);
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
for (const sid of index.keys()) {
|
|
2860
|
-
if (foundSids.has(sid))
|
|
2861
|
-
continue;
|
|
2862
|
-
index.delete(sid);
|
|
2863
|
-
queue.cancel(sid);
|
|
2864
|
-
result.staleIndexRowsDropped++;
|
|
2865
|
-
}
|
|
2866
|
-
const pending = params.queue.listPending();
|
|
2867
|
-
for (const row of pending) {
|
|
2868
|
-
if (!foundSids.has(row.surfaceId)) {
|
|
2869
|
-
queue.cancel(row.surfaceId);
|
|
2870
|
-
result.staleQueueRowsCancelled++;
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
log.log(`[reconcile] indexed=${result.indexed} parseErrors=${result.parseErrors} ` + `orphanFiles=${result.orphanFiles} staleIndexRows=${result.staleIndexRowsDropped} ` + `followupsScheduled=${result.followupsScheduled} staleQueueRows=${result.staleQueueRowsCancelled}`);
|
|
2874
|
-
return result;
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
// dashboard-state.ts
|
|
2878
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2879
|
-
import { homedir as homedir3 } from "node:os";
|
|
2880
|
-
function extractTitleAndDetail(meta) {
|
|
2881
|
-
let title = null;
|
|
2882
|
-
let detail = null;
|
|
2883
|
-
const fullText = meta.lines.join(`
|
|
2884
|
-
`);
|
|
2885
|
-
const titleMatch = fullText.match(/text\s+"([^"]+)"\s+h[34]/);
|
|
2886
|
-
if (titleMatch)
|
|
2887
|
-
title = titleMatch[1];
|
|
2888
|
-
if (!title) {
|
|
2889
|
-
const bodyMatch = fullText.match(/text\s+"([^"]+)"/);
|
|
2890
|
-
if (bodyMatch)
|
|
2891
|
-
title = bodyMatch[1];
|
|
2892
|
-
}
|
|
2893
|
-
const inlineDetail = fullText.match(/^\s*detail:\s*(.+)/m);
|
|
2894
|
-
if (inlineDetail && inlineDetail[1].trim().length > 0) {
|
|
2895
|
-
detail = inlineDetail[1].trim().replace(/^"|"$/g, "");
|
|
2896
|
-
} else {
|
|
2897
|
-
const detailBlockMatch = fullText.match(/^\s*detail:\s*\n((?:\s+.+\n?)+)/m);
|
|
2898
|
-
if (detailBlockMatch) {
|
|
2899
|
-
detail = detailBlockMatch[1].replace(/^ {2,4}/gm, "").trim();
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
return { title, detail };
|
|
2621
|
+
const inlineDetail = fullText.match(/^\s*detail:\s*(.+)/m);
|
|
2622
|
+
if (inlineDetail && inlineDetail[1].trim().length > 0) {
|
|
2623
|
+
detail = inlineDetail[1].trim().replace(/^"|"$/g, "");
|
|
2624
|
+
} else {
|
|
2625
|
+
const detailBlockMatch = fullText.match(/^\s*detail:\s*\n((?:\s+.+\n?)+)/m);
|
|
2626
|
+
if (detailBlockMatch) {
|
|
2627
|
+
detail = detailBlockMatch[1].replace(/^ {2,4}/gm, "").trim();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return { title, detail };
|
|
2903
2631
|
}
|
|
2904
2632
|
function buildDashboardStateContext(state, agentId) {
|
|
2905
2633
|
if (!state.surfaceIndex)
|
|
@@ -3234,6 +2962,29 @@ async function sweepExpiredSurfaces(params) {
|
|
|
3234
2962
|
|
|
3235
2963
|
// followup.ts
|
|
3236
2964
|
var pendingDebounces = new Map;
|
|
2965
|
+
function lookupRecentFiredFollowups(state, surfaceId, limit = 5) {
|
|
2966
|
+
try {
|
|
2967
|
+
const db = getOrCreateHistoryDb(state);
|
|
2968
|
+
const rows = db.prepare("SELECT instruction FROM followup_queue WHERE surfaceId = ? AND status = 'fired' ORDER BY createdAt DESC LIMIT ?").all(surfaceId, limit);
|
|
2969
|
+
return rows.map((r) => r.instruction);
|
|
2970
|
+
} catch {
|
|
2971
|
+
return [];
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function lookupPreviousFiredFollowup(state, surfaceId) {
|
|
2975
|
+
const recent = lookupRecentFiredFollowups(state, surfaceId, 1);
|
|
2976
|
+
return recent[0] ?? null;
|
|
2977
|
+
}
|
|
2978
|
+
function followupResidue(message) {
|
|
2979
|
+
return message.toLowerCase().replace(/\d{4}-\d{2}-\d{2}/g, "").replace(/\d+/g, "").replace(/\s+/g, " ").trim();
|
|
2980
|
+
}
|
|
2981
|
+
function isTemplatedFollowup(prev, next) {
|
|
2982
|
+
const a = followupResidue(prev);
|
|
2983
|
+
const b = followupResidue(next);
|
|
2984
|
+
if (!a || !b)
|
|
2985
|
+
return false;
|
|
2986
|
+
return a === b;
|
|
2987
|
+
}
|
|
3237
2988
|
var pollerInterval = null;
|
|
3238
2989
|
var startupPollTimeout = null;
|
|
3239
2990
|
var sweepCounter = 0;
|
|
@@ -3273,6 +3024,19 @@ function scheduleFollowup(state, surfaceId, followupRaw, agentId) {
|
|
|
3273
3024
|
console.warn("[agentlife:followup] parse failed for %s: %s", surfaceId, followupRaw);
|
|
3274
3025
|
return;
|
|
3275
3026
|
}
|
|
3027
|
+
const previous = lookupPreviousFiredFollowup(state, surfaceId);
|
|
3028
|
+
if (previous && isTemplatedFollowup(previous, parsed.message)) {
|
|
3029
|
+
const owner = agentId ?? getSurface(state, surfaceId)?.agentId ?? null;
|
|
3030
|
+
recordActivity(state, "quality_warning", null, owner, {
|
|
3031
|
+
data: JSON.stringify({
|
|
3032
|
+
issue: "followup_templated",
|
|
3033
|
+
surfaceId,
|
|
3034
|
+
previous,
|
|
3035
|
+
next: parsed.message
|
|
3036
|
+
})
|
|
3037
|
+
});
|
|
3038
|
+
console.warn("[agentlife:followup] templated followup detected for %s — same residue as previous fire", surfaceId);
|
|
3039
|
+
}
|
|
3276
3040
|
const existing = pendingDebounces.get(surfaceId);
|
|
3277
3041
|
if (existing)
|
|
3278
3042
|
clearTimeout(existing);
|
|
@@ -3341,17 +3105,25 @@ function rescheduleFailedFollowup(state, row, error) {
|
|
|
3341
3105
|
console.error("[agentlife:followup] reschedule failed for %s: %s", row.surfaceId, e?.message);
|
|
3342
3106
|
}
|
|
3343
3107
|
}
|
|
3344
|
-
function
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
const view = getSurface(state, surfaceId);
|
|
3348
|
-
const goalText = view?.goal ?? (view ? extractTitleAndDetail({ lines: view.lines }).title : null);
|
|
3108
|
+
function buildFireInstruction(state, row) {
|
|
3109
|
+
const view2 = getSurface(state, row.surfaceId);
|
|
3110
|
+
const goalText = view2?.goal ?? (view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null);
|
|
3349
3111
|
const goalLabel = goalText ? ` (goal: "${goalText}")` : "";
|
|
3350
|
-
const
|
|
3351
|
-
|
|
3352
|
-
|
|
3112
|
+
const recentFires = lookupRecentFiredFollowups(state, row.surfaceId, 5);
|
|
3113
|
+
const prior = recentFires.filter((m) => m !== row.instruction);
|
|
3114
|
+
const priorBlock = prior.length > 0 ? `
|
|
3115
|
+
Prior followups on this surface — your next must not match any of these with parameters rotated:
|
|
3116
|
+
${prior.slice(0, 3).map((m) => ` • "${m}"`).join(`
|
|
3117
|
+
`)}` : "";
|
|
3118
|
+
return [
|
|
3119
|
+
`Widget followup for ${row.surfaceId}${goalLabel}: ${row.instruction}`,
|
|
3120
|
+
`Stay on this surfaceId — update it, push a transient input surface, or delete it. New dashboard surfaceId only if this goal is complete and a successor begins (delete in the same turn).${priorBlock}`
|
|
3353
3121
|
].join(`
|
|
3354
3122
|
`);
|
|
3123
|
+
}
|
|
3124
|
+
function executeSchedule(state, surfaceId, parsed, agentId) {
|
|
3125
|
+
if (!state.followupQueue)
|
|
3126
|
+
return;
|
|
3355
3127
|
const fireAt = Date.now() + parsed.timeOffsetMs;
|
|
3356
3128
|
const owner = agentId ?? view?.agentId ?? null;
|
|
3357
3129
|
if (!owner) {
|
|
@@ -3378,8 +3150,8 @@ function pollFollowups(state) {
|
|
|
3378
3150
|
try {
|
|
3379
3151
|
const due = state.followupQueue.drainDue(Date.now());
|
|
3380
3152
|
for (const row of due) {
|
|
3381
|
-
const
|
|
3382
|
-
if (!
|
|
3153
|
+
const view2 = getSurface(state, row.surfaceId);
|
|
3154
|
+
if (!view2 || view2.state === "dismissed") {
|
|
3383
3155
|
console.log("[agentlife:followup] skipped %s — surface gone or dismissed", row.surfaceId);
|
|
3384
3156
|
continue;
|
|
3385
3157
|
}
|
|
@@ -3390,7 +3162,8 @@ function pollFollowups(state) {
|
|
|
3390
3162
|
}
|
|
3391
3163
|
const sessionKey = buildAgentSessionKey(agentId);
|
|
3392
3164
|
const idempotencyKey = `followup-${row.surfaceId}-${Date.now()}`;
|
|
3393
|
-
const
|
|
3165
|
+
const wrapped = buildFireInstruction(state, row);
|
|
3166
|
+
const chatParams = JSON.stringify({ sessionKey, message: `[system] ${wrapped}`, idempotencyKey });
|
|
3394
3167
|
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 60000 }).then((result) => {
|
|
3395
3168
|
console.log("[agentlife:followup] chat.send for %s: code=%s", row.surfaceId, result?.code ?? "?");
|
|
3396
3169
|
}).catch((e) => {
|
|
@@ -3419,6 +3192,451 @@ function pollFollowups(state) {
|
|
|
3419
3192
|
}
|
|
3420
3193
|
}
|
|
3421
3194
|
|
|
3195
|
+
// push.ts
|
|
3196
|
+
function tokenizeGoal(goal) {
|
|
3197
|
+
return new Set(goal.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= 3));
|
|
3198
|
+
}
|
|
3199
|
+
function goalSimilarity(a, b) {
|
|
3200
|
+
const ta = tokenizeGoal(a);
|
|
3201
|
+
const tb = tokenizeGoal(b);
|
|
3202
|
+
if (ta.size === 0 || tb.size === 0)
|
|
3203
|
+
return 0;
|
|
3204
|
+
let inter = 0;
|
|
3205
|
+
for (const w of ta)
|
|
3206
|
+
if (tb.has(w))
|
|
3207
|
+
inter++;
|
|
3208
|
+
const union = ta.size + tb.size - inter;
|
|
3209
|
+
return union === 0 ? 0 : inter / union;
|
|
3210
|
+
}
|
|
3211
|
+
var SIBLING_SIMILARITY_THRESHOLD = 0.75;
|
|
3212
|
+
var SIBLING_MIN_SHARED_WORDS = 5;
|
|
3213
|
+
function findSiblingSurfaces(state, surfaceId, goal, agentId) {
|
|
3214
|
+
if (!goal || !state.surfaceIndex)
|
|
3215
|
+
return [];
|
|
3216
|
+
const live = state.surfaceIndex.list({ agentId, state: "active" });
|
|
3217
|
+
const sharedFloor = SIBLING_MIN_SHARED_WORDS;
|
|
3218
|
+
const out = [];
|
|
3219
|
+
const myTokens = tokenizeGoal(goal);
|
|
3220
|
+
for (const e of live) {
|
|
3221
|
+
if (e.surfaceId === surfaceId || !e.goal)
|
|
3222
|
+
continue;
|
|
3223
|
+
const sim = goalSimilarity(goal, e.goal);
|
|
3224
|
+
if (sim < SIBLING_SIMILARITY_THRESHOLD)
|
|
3225
|
+
continue;
|
|
3226
|
+
const otherTokens = tokenizeGoal(e.goal);
|
|
3227
|
+
let shared = 0;
|
|
3228
|
+
for (const w of myTokens)
|
|
3229
|
+
if (otherTokens.has(w))
|
|
3230
|
+
shared++;
|
|
3231
|
+
if (shared < sharedFloor)
|
|
3232
|
+
continue;
|
|
3233
|
+
out.push({ surfaceId: e.surfaceId, goal: e.goal, similarity: Math.round(sim * 100) / 100 });
|
|
3234
|
+
}
|
|
3235
|
+
return out;
|
|
3236
|
+
}
|
|
3237
|
+
async function pushDsl(state, dsl, opts) {
|
|
3238
|
+
if (!state.surfaceIndex || !state.followupQueue || !state.transientSurfaces || !state.fileSurfaceStorage) {
|
|
3239
|
+
throw new Error("push.ts: plugin state not initialized");
|
|
3240
|
+
}
|
|
3241
|
+
const blocks = parseDsl(dsl);
|
|
3242
|
+
if (blocks.length === 0)
|
|
3243
|
+
return [];
|
|
3244
|
+
const results = [];
|
|
3245
|
+
const autoDeletedInputIds = [];
|
|
3246
|
+
if (opts.evictStaleInputs !== false) {
|
|
3247
|
+
const incomingInputIds = new Set;
|
|
3248
|
+
for (const b of blocks) {
|
|
3249
|
+
if (b.kind === "push" && b.isInput)
|
|
3250
|
+
incomingInputIds.add(b.surfaceId);
|
|
3251
|
+
}
|
|
3252
|
+
if (incomingInputIds.size > 0) {
|
|
3253
|
+
autoDeletedInputIds.push(...state.transientSurfaces.takeStaleInputs(incomingInputIds));
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
for (const block of blocks) {
|
|
3257
|
+
if (block.kind === "delete") {
|
|
3258
|
+
const result2 = await handleDelete(state, block);
|
|
3259
|
+
results.push(result2);
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
const result = await handlePush(state, block, opts);
|
|
3263
|
+
results.push(result);
|
|
3264
|
+
}
|
|
3265
|
+
broadcastSurface(dsl);
|
|
3266
|
+
for (const sid of autoDeletedInputIds)
|
|
3267
|
+
broadcastDelete(sid);
|
|
3268
|
+
return results;
|
|
3269
|
+
}
|
|
3270
|
+
async function handleDelete(state, block) {
|
|
3271
|
+
const sid = block.surfaceId;
|
|
3272
|
+
const agentId = state.surfaceIndex.get(sid)?.agentId ?? state.transientSurfaces.get(sid)?.agentId ?? null;
|
|
3273
|
+
state.transientSurfaces.delete(sid);
|
|
3274
|
+
const indexed = state.surfaceIndex.get(sid);
|
|
3275
|
+
if (indexed) {
|
|
3276
|
+
try {
|
|
3277
|
+
await state.fileSurfaceStorage.delete(indexed.agentId, sid);
|
|
3278
|
+
} catch (err) {
|
|
3279
|
+
console.warn("[push] file delete failed for %s: %s", sid, err?.message);
|
|
3280
|
+
}
|
|
3281
|
+
state.surfaceIndex.delete(sid);
|
|
3282
|
+
}
|
|
3283
|
+
state.followupQueue.cancel(sid);
|
|
3284
|
+
recordSurfaceEvent(state, sid, "deleted", undefined, agentId ?? undefined);
|
|
3285
|
+
broadcastDelete(sid);
|
|
3286
|
+
return {
|
|
3287
|
+
surfaceId: sid,
|
|
3288
|
+
kind: "delete",
|
|
3289
|
+
isNew: false,
|
|
3290
|
+
isTransient: false,
|
|
3291
|
+
isLoading: false,
|
|
3292
|
+
followupChanged: false,
|
|
3293
|
+
followupRemoved: true,
|
|
3294
|
+
followupTemplated: null,
|
|
3295
|
+
siblingSurfaces: [],
|
|
3296
|
+
goalChanged: null,
|
|
3297
|
+
contextDroppedFields: [],
|
|
3298
|
+
validation: emptyValidation()
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
async function handlePush(state, block, opts) {
|
|
3302
|
+
const now = Date.now();
|
|
3303
|
+
const validation = validateBlockDsl(block.rawDsl);
|
|
3304
|
+
if (block.isTransient) {
|
|
3305
|
+
const existing2 = state.transientSurfaces.get(block.surfaceId);
|
|
3306
|
+
state.transientSurfaces.set(opts.agentId, block, opts.sessionKey ?? null);
|
|
3307
|
+
recordSurfaceEvent(state, block.surfaceId, existing2 ? "updated" : "created", block.rawDsl, opts.agentId);
|
|
3308
|
+
return {
|
|
3309
|
+
surfaceId: block.surfaceId,
|
|
3310
|
+
kind: "push",
|
|
3311
|
+
isNew: !existing2,
|
|
3312
|
+
isTransient: true,
|
|
3313
|
+
isLoading: block.declaredState === "loading",
|
|
3314
|
+
followupChanged: false,
|
|
3315
|
+
followupRemoved: false,
|
|
3316
|
+
followupTemplated: null,
|
|
3317
|
+
siblingSurfaces: [],
|
|
3318
|
+
goalChanged: null,
|
|
3319
|
+
contextDroppedFields: [],
|
|
3320
|
+
validation
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
const existing = state.surfaceIndex.get(block.surfaceId);
|
|
3324
|
+
if (!block.goal && !existing?.goal) {
|
|
3325
|
+
throw new Error(`widget '${block.surfaceId}' missing 'goal:' — every widget must declare ` + `a goal in its DSL header (e.g. 'goal: track today\\'s weather'). ` + `Without one the dashboard can't rank or iterate on this widget.`);
|
|
3326
|
+
}
|
|
3327
|
+
const filePath = await state.fileSurfaceStorage.write(opts.agentId, block.surfaceId, block.rawDsl);
|
|
3328
|
+
const resolvedContext = block.context ?? existing?.context ?? null;
|
|
3329
|
+
const contextDroppedFields = block.context && existing?.context ? Object.keys(existing.context).filter((k) => !(k in block.context)) : [];
|
|
3330
|
+
const resolvedGoal = block.goal ?? existing?.goal ?? null;
|
|
3331
|
+
const goalChanged = existing?.goal && block.goal && existing.goal !== block.goal ? { from: existing.goal, to: block.goal } : null;
|
|
3332
|
+
const newState = "active";
|
|
3333
|
+
const nextEntry = {
|
|
3334
|
+
surfaceId: block.surfaceId,
|
|
3335
|
+
agentId: existing?.agentId ?? opts.agentId,
|
|
3336
|
+
path: filePath,
|
|
3337
|
+
rawDsl: block.rawDsl,
|
|
3338
|
+
state: newState,
|
|
3339
|
+
isOverlay: false,
|
|
3340
|
+
dashboardVisible: existing?.dashboardVisible ?? true,
|
|
3341
|
+
createdAt: existing?.createdAt ?? now,
|
|
3342
|
+
updatedAt: now,
|
|
3343
|
+
expiredSince: null,
|
|
3344
|
+
originSessionKey: opts.sessionKey ?? existing?.originSessionKey ?? null,
|
|
3345
|
+
goal: resolvedGoal,
|
|
3346
|
+
context: resolvedContext,
|
|
3347
|
+
followup: block.followup,
|
|
3348
|
+
chainRoot: block.chainRoot,
|
|
3349
|
+
chainParent: block.chainParent,
|
|
3350
|
+
chainPosition: block.chainPosition,
|
|
3351
|
+
chainStatus: block.chainStatus,
|
|
3352
|
+
parseError: null
|
|
3353
|
+
};
|
|
3354
|
+
try {
|
|
3355
|
+
state.surfaceIndex.upsert(nextEntry);
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
console.warn("[push] index upsert failed for %s: %s", block.surfaceId, err?.message);
|
|
3358
|
+
}
|
|
3359
|
+
const oldFollowup = existing?.followup ?? null;
|
|
3360
|
+
const newFollowup = block.followup ?? null;
|
|
3361
|
+
const followupChanged = oldFollowup !== newFollowup;
|
|
3362
|
+
const followupRemoved = !!oldFollowup && !newFollowup;
|
|
3363
|
+
let followupTemplated = null;
|
|
3364
|
+
if (newFollowup) {
|
|
3365
|
+
const newSpec = parseFollowupSpec(newFollowup);
|
|
3366
|
+
if (newSpec) {
|
|
3367
|
+
const recent = lookupRecentFiredFollowups(state, block.surfaceId, 5);
|
|
3368
|
+
const match = recent.find((r) => isTemplatedFollowup(r, newSpec.message));
|
|
3369
|
+
if (match) {
|
|
3370
|
+
followupTemplated = { previous: match, next: newSpec.message };
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
try {
|
|
3375
|
+
if (followupRemoved || !newFollowup) {
|
|
3376
|
+
state.followupQueue.cancel(block.surfaceId);
|
|
3377
|
+
} else {
|
|
3378
|
+
const spec = parseFollowupSpec(newFollowup);
|
|
3379
|
+
if (spec) {
|
|
3380
|
+
state.followupQueue.upsert({
|
|
3381
|
+
surfaceId: block.surfaceId,
|
|
3382
|
+
agentId: nextEntry.agentId,
|
|
3383
|
+
fireAt: now + spec.timeOffsetMs,
|
|
3384
|
+
instruction: spec.message,
|
|
3385
|
+
createdAt: now
|
|
3386
|
+
});
|
|
3387
|
+
} else {
|
|
3388
|
+
console.warn("[push] unparseable followup on %s: %s", block.surfaceId, newFollowup);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
} catch (err) {
|
|
3392
|
+
console.warn("[push] followup queue update failed for %s: %s", block.surfaceId, err?.message);
|
|
3393
|
+
}
|
|
3394
|
+
recordSurfaceEvent(state, block.surfaceId, existing ? "updated" : "created", block.rawDsl, nextEntry.agentId);
|
|
3395
|
+
const siblingSurfaces = findSiblingSurfaces(state, block.surfaceId, nextEntry.goal ?? null, nextEntry.agentId);
|
|
3396
|
+
return {
|
|
3397
|
+
surfaceId: block.surfaceId,
|
|
3398
|
+
kind: "push",
|
|
3399
|
+
isNew: !existing,
|
|
3400
|
+
isTransient: false,
|
|
3401
|
+
isLoading: block.declaredState === "loading",
|
|
3402
|
+
followupChanged,
|
|
3403
|
+
followupRemoved,
|
|
3404
|
+
followupTemplated,
|
|
3405
|
+
siblingSurfaces,
|
|
3406
|
+
goalChanged,
|
|
3407
|
+
contextDroppedFields,
|
|
3408
|
+
validation
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
function emptyValidation() {
|
|
3412
|
+
return {
|
|
3413
|
+
unknownKeywords: [],
|
|
3414
|
+
metadataWithoutColon: [],
|
|
3415
|
+
missingCardStructure: false,
|
|
3416
|
+
invalidInputActions: [],
|
|
3417
|
+
unknownAttrs: [],
|
|
3418
|
+
invalidEnumValues: []
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// onboarding.ts
|
|
3423
|
+
var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
3424
|
+
function userAgentIds(runtime) {
|
|
3425
|
+
const cfg = runtime.config.loadConfig();
|
|
3426
|
+
const list = cfg?.agents?.list ?? [];
|
|
3427
|
+
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
3428
|
+
}
|
|
3429
|
+
function isOnboarding(_state, runtime) {
|
|
3430
|
+
return userAgentIds(runtime).length === 0;
|
|
3431
|
+
}
|
|
3432
|
+
function snapshotOnboarding(state, runtime) {
|
|
3433
|
+
const count = userAgentIds(runtime).length;
|
|
3434
|
+
return { isActive: count === 0, userAgentCount: count };
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// render-widgets.ts
|
|
3438
|
+
function renderAllAgentWidgets(state, runtime, log) {
|
|
3439
|
+
if (isOnboarding(state, runtime)) {
|
|
3440
|
+
log("[render-widgets] onboarding active — rendering welcome widget");
|
|
3441
|
+
renderWelcomeWidget(state, log);
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
log("[render-widgets] onboarding complete — no plugin-pushed widgets; specialists own their surfaces");
|
|
3445
|
+
}
|
|
3446
|
+
var WELCOME_SURFACE_ID = "welcome";
|
|
3447
|
+
var WELCOME_INPUT_SURFACE_ID = "welcome-input";
|
|
3448
|
+
var ONBOARDING_SESSION_KEY = "agent:agentlife-builder:agentlife:direct:onboarding";
|
|
3449
|
+
var ONBOARDING_PLAYBOOK = `## Zero-agent onboarding
|
|
3450
|
+
|
|
3451
|
+
You are agent-builder running a one-time onboarding for a fresh user (no agents yet). The plugin already pushed a \`welcome-input\` surface; the user's reply in this session is a tap or text on that surface.
|
|
3452
|
+
|
|
3453
|
+
Run a 2–4 turn guided interview, then create ONE **generalist** agent.
|
|
3454
|
+
|
|
3455
|
+
Each turn:
|
|
3456
|
+
1. Decide: do you have enough signal? Minimum = name/persona hint + 2–3 concrete life areas. Don't drag past 4 turns.
|
|
3457
|
+
2. Not enough → \`agentlife_push\` the SAME surfaceId \`welcome-input\` with:
|
|
3458
|
+
- h3 question generated from what the user just said
|
|
3459
|
+
- 2–4 multichoice buttons \`action=choice\`, options derived from the user's own words (NEVER hardcoded domain lists)
|
|
3460
|
+
- \`textfield placeholder="Something else…"\` escape hatch
|
|
3461
|
+
- updated \`context: {"phase":"welcome","turn":N+1,"answers":[...]}\`
|
|
3462
|
+
- goal + followup unchanged
|
|
3463
|
+
Respond \`done\`.
|
|
3464
|
+
3. Enough → create the generalist and hand off the dashboard:
|
|
3465
|
+
- Workspace under \`$HOME/.openclaw/workspace-{id}\` (id = slugified short name or "me")
|
|
3466
|
+
- \`AGENTS.md\` — generalist scope covering mentioned domains; Data Schema must include a \`domain TEXT NOT NULL\` column on every user-data table so future split by domain is trivial; add self-evolution clause ("when activity_log rows for one domain exceed a natural threshold, push a split-suggest widget")
|
|
3467
|
+
- \`SOUL.md\`, \`IDENTITY.md\`, \`USER.md\` (user's own words), \`HEARTBEAT.md\` (empty)
|
|
3468
|
+
- \`exec openclaw gateway call agentlife.createAgent --params '{...}'\` with \`tools: {profile:"full", alsoAllow:["agentlife_push"]}\`. Plugin auto-deletes welcome + welcome-input on success.
|
|
3469
|
+
- **Final step, same turn, before \`done\`:** push ONE actionable first widget for the newly-created agent — an input prompt ("Log your first meal", "Enter today's weight"), a starter metric, or an inviting CTA that owns a surfaceId like \`{id}-today\` / \`{id}-start\`. This is what the user sees when onboarding ends; it MUST invite interaction, not just announce existence. Then delete your \`{domain}-building\` loading widget. Then emit \`done\`.
|
|
3470
|
+
`;
|
|
3471
|
+
function renderWelcomeWidget(state, log) {
|
|
3472
|
+
const welcomeDsl = [
|
|
3473
|
+
`surface ${WELCOME_SURFACE_ID} size=m`,
|
|
3474
|
+
` card`,
|
|
3475
|
+
` column`,
|
|
3476
|
+
` text "\uD83D\uDC4B Welcome to Agent Life" h3`,
|
|
3477
|
+
` text "Answer below and I'll build your first agent from it." body`,
|
|
3478
|
+
` divider`,
|
|
3479
|
+
` badge "Setup" color=#6366F1 outlined`,
|
|
3480
|
+
`goal: Welcome the user and anchor the onboarding flow`,
|
|
3481
|
+
`followup: +24h "If still zero agents, push an encouraging nudge."`,
|
|
3482
|
+
`context: {"phase":"welcome","autoRendered":true}`
|
|
3483
|
+
].join(`
|
|
3484
|
+
`);
|
|
3485
|
+
pushDsl(state, welcomeDsl, { agentId: SYSTEM_AGENT_ID }).then(() => log(`[render-widgets] rendered ${WELCOME_SURFACE_ID}`)).catch((e) => console.warn("[render-widgets] welcome push failed: %s", e?.message));
|
|
3486
|
+
const welcomeInputDsl = [
|
|
3487
|
+
`surface ${WELCOME_INPUT_SURFACE_ID} input`,
|
|
3488
|
+
` card`,
|
|
3489
|
+
` column`,
|
|
3490
|
+
` text "What do you want to track or improve?" h3`,
|
|
3491
|
+
` text "One line. Anything — meals, money, a project, a habit." body`,
|
|
3492
|
+
` textfield placeholder="Type anything…"`,
|
|
3493
|
+
`goal: Onboard the user to their first agent via a guided interview`,
|
|
3494
|
+
`followup: +30m "If unanswered, replace the question with a gentler prompt."`,
|
|
3495
|
+
`context: {"phase":"welcome","turn":1,"answers":[]}`
|
|
3496
|
+
].join(`
|
|
3497
|
+
`);
|
|
3498
|
+
pushDsl(state, welcomeInputDsl, { agentId: SYSTEM_AGENT_ID, sessionKey: ONBOARDING_SESSION_KEY }).then(() => log(`[render-widgets] rendered ${WELCOME_INPUT_SURFACE_ID} (onboarding session)`)).catch((e) => console.warn("[render-widgets] welcome-input push failed: %s", e?.message));
|
|
3499
|
+
}
|
|
3500
|
+
function deleteWelcomeWidget(state, log) {
|
|
3501
|
+
for (const id of [WELCOME_SURFACE_ID, WELCOME_INPUT_SURFACE_ID]) {
|
|
3502
|
+
if (!hasSurface(state, id))
|
|
3503
|
+
continue;
|
|
3504
|
+
const view2 = getSurface(state, id);
|
|
3505
|
+
if (view2 && !view2.isTransient && view2.path) {
|
|
3506
|
+
try {
|
|
3507
|
+
fs4.unlinkSync(view2.path);
|
|
3508
|
+
} catch {}
|
|
3509
|
+
}
|
|
3510
|
+
state.surfaceIndex?.delete(id);
|
|
3511
|
+
state.transientSurfaces.delete(id);
|
|
3512
|
+
state.followupQueue?.cancel(id);
|
|
3513
|
+
purgeSurfaceHistory(state, id);
|
|
3514
|
+
broadcastDelete(id);
|
|
3515
|
+
log(`[render-widgets] deleted ${id}`);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// services/surfaces-init.ts
|
|
3520
|
+
import * as path5 from "node:path";
|
|
3521
|
+
|
|
3522
|
+
// storage/reconciler.ts
|
|
3523
|
+
var defaultLogger = {
|
|
3524
|
+
log: (...args) => console.log(...args),
|
|
3525
|
+
warn: (...args) => console.warn(...args)
|
|
3526
|
+
};
|
|
3527
|
+
function reconcile(params) {
|
|
3528
|
+
const { storage, index, queue, knownAgents } = params;
|
|
3529
|
+
const log = params.logger ?? defaultLogger;
|
|
3530
|
+
const result = {
|
|
3531
|
+
indexed: 0,
|
|
3532
|
+
parseErrors: 0,
|
|
3533
|
+
orphanFiles: 0,
|
|
3534
|
+
staleIndexRowsDropped: 0,
|
|
3535
|
+
followupsScheduled: 0,
|
|
3536
|
+
staleQueueRowsCancelled: 0
|
|
3537
|
+
};
|
|
3538
|
+
const { surfaces, orphans } = storage.listAll(knownAgents);
|
|
3539
|
+
if (orphans.length > 0) {
|
|
3540
|
+
result.orphanFiles = orphans.length;
|
|
3541
|
+
const byAgent = new Map;
|
|
3542
|
+
for (const o of orphans)
|
|
3543
|
+
byAgent.set(o.agentId, (byAgent.get(o.agentId) ?? 0) + 1);
|
|
3544
|
+
for (const [agentId, count] of byAgent) {
|
|
3545
|
+
log.warn(`[reconcile] orphan workspace: ${agentId} (${count} file(s)) — agent not in agents.list, skipping`);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
const foundSids = new Set;
|
|
3549
|
+
for (const file of surfaces) {
|
|
3550
|
+
let block = null;
|
|
3551
|
+
let parseError = null;
|
|
3552
|
+
try {
|
|
3553
|
+
const parsed = parseBlock(file.dsl);
|
|
3554
|
+
if (!parsed) {
|
|
3555
|
+
parseError = "no surface header";
|
|
3556
|
+
} else if (parsed.kind === "delete") {
|
|
3557
|
+
parseError = "file contains only a delete directive";
|
|
3558
|
+
} else {
|
|
3559
|
+
block = parsed;
|
|
3560
|
+
}
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
parseError = err?.message ?? String(err);
|
|
3563
|
+
}
|
|
3564
|
+
if (parseError || !block) {
|
|
3565
|
+
index.upsertParseError({
|
|
3566
|
+
surfaceId: file.surfaceId,
|
|
3567
|
+
agentId: file.agentId,
|
|
3568
|
+
path: file.path,
|
|
3569
|
+
rawDsl: file.dsl,
|
|
3570
|
+
error: parseError ?? "unknown parse error"
|
|
3571
|
+
});
|
|
3572
|
+
foundSids.add(file.surfaceId);
|
|
3573
|
+
result.parseErrors++;
|
|
3574
|
+
log.warn(`[reconcile] parse error in ${file.surfaceId}: ${parseError}`);
|
|
3575
|
+
continue;
|
|
3576
|
+
}
|
|
3577
|
+
const now = Date.now();
|
|
3578
|
+
const existing = index.get(file.surfaceId);
|
|
3579
|
+
const entry = {
|
|
3580
|
+
surfaceId: block.surfaceId,
|
|
3581
|
+
agentId: file.agentId,
|
|
3582
|
+
path: file.path,
|
|
3583
|
+
rawDsl: block.rawDsl,
|
|
3584
|
+
state: existing?.state ?? "active",
|
|
3585
|
+
isOverlay: false,
|
|
3586
|
+
dashboardVisible: existing?.dashboardVisible ?? true,
|
|
3587
|
+
createdAt: existing?.createdAt ?? now,
|
|
3588
|
+
updatedAt: existing?.updatedAt ?? now,
|
|
3589
|
+
expiredSince: existing?.expiredSince ?? null,
|
|
3590
|
+
originSessionKey: existing?.originSessionKey ?? null,
|
|
3591
|
+
goal: block.goal,
|
|
3592
|
+
context: block.context,
|
|
3593
|
+
followup: block.followup,
|
|
3594
|
+
chainRoot: block.chainRoot,
|
|
3595
|
+
chainParent: block.chainParent,
|
|
3596
|
+
chainPosition: block.chainPosition,
|
|
3597
|
+
chainStatus: block.chainStatus,
|
|
3598
|
+
parseError: null
|
|
3599
|
+
};
|
|
3600
|
+
index.upsert(entry);
|
|
3601
|
+
foundSids.add(file.surfaceId);
|
|
3602
|
+
result.indexed++;
|
|
3603
|
+
if (block.followup) {
|
|
3604
|
+
const spec = parseFollowupSpec(block.followup);
|
|
3605
|
+
if (spec) {
|
|
3606
|
+
const pending2 = queue.nextPending(file.surfaceId);
|
|
3607
|
+
if (!pending2) {
|
|
3608
|
+
queue.upsert({
|
|
3609
|
+
surfaceId: file.surfaceId,
|
|
3610
|
+
agentId: file.agentId,
|
|
3611
|
+
fireAt: now + spec.timeOffsetMs,
|
|
3612
|
+
instruction: spec.message,
|
|
3613
|
+
createdAt: now
|
|
3614
|
+
});
|
|
3615
|
+
result.followupsScheduled++;
|
|
3616
|
+
}
|
|
3617
|
+
} else {
|
|
3618
|
+
log.warn(`[reconcile] unparseable followup on ${file.surfaceId}: ${block.followup}`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
for (const sid of index.keys()) {
|
|
3623
|
+
if (foundSids.has(sid))
|
|
3624
|
+
continue;
|
|
3625
|
+
index.delete(sid);
|
|
3626
|
+
queue.cancel(sid);
|
|
3627
|
+
result.staleIndexRowsDropped++;
|
|
3628
|
+
}
|
|
3629
|
+
const pending = params.queue.listPending();
|
|
3630
|
+
for (const row of pending) {
|
|
3631
|
+
if (!foundSids.has(row.surfaceId)) {
|
|
3632
|
+
queue.cancel(row.surfaceId);
|
|
3633
|
+
result.staleQueueRowsCancelled++;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
log.log(`[reconcile] indexed=${result.indexed} parseErrors=${result.parseErrors} ` + `orphanFiles=${result.orphanFiles} staleIndexRows=${result.staleIndexRowsDropped} ` + `followupsScheduled=${result.followupsScheduled} staleQueueRows=${result.staleQueueRowsCancelled}`);
|
|
3637
|
+
return result;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3422
3640
|
// cleanup.ts
|
|
3423
3641
|
function enqueueCleanupTasks(state, surfaceId, agentId, cronId, automations) {
|
|
3424
3642
|
const db = getOrCreateHistoryDb(state);
|
|
@@ -3868,11 +4086,11 @@ async function cleanupSupervisorLegacyState(state) {
|
|
|
3868
4086
|
const actions = [];
|
|
3869
4087
|
try {
|
|
3870
4088
|
if (hasSurface(state, STALE_SUPERVISOR_SURFACE)) {
|
|
3871
|
-
const
|
|
4089
|
+
const view2 = getSurface(state, STALE_SUPERVISOR_SURFACE);
|
|
3872
4090
|
removeFollowup(state, STALE_SUPERVISOR_SURFACE);
|
|
3873
|
-
if (
|
|
4091
|
+
if (view2 && !view2.isTransient && view2.path) {
|
|
3874
4092
|
try {
|
|
3875
|
-
fs6.unlinkSync(
|
|
4093
|
+
fs6.unlinkSync(view2.path);
|
|
3876
4094
|
} catch {}
|
|
3877
4095
|
}
|
|
3878
4096
|
state.surfaceIndex?.delete(STALE_SUPERVISOR_SURFACE);
|
|
@@ -4149,75 +4367,13 @@ function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
|
|
|
4149
4367
|
state.usageAccumulator.delete(sessionKey);
|
|
4150
4368
|
}
|
|
4151
4369
|
|
|
4152
|
-
// notifications.ts
|
|
4153
|
-
import * as fs7 from "node:fs";
|
|
4154
|
-
import * as path7 from "node:path";
|
|
4155
|
-
var config;
|
|
4156
|
-
var recentNotifications = new Map;
|
|
4157
|
-
var DEBOUNCE_MS = 60000;
|
|
4158
|
-
function loadConfig() {
|
|
4159
|
-
if (config !== undefined)
|
|
4160
|
-
return config;
|
|
4161
|
-
try {
|
|
4162
|
-
const configPath = path7.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
4163
|
-
const raw = fs7.readFileSync(configPath, "utf-8");
|
|
4164
|
-
const parsed = JSON.parse(raw);
|
|
4165
|
-
if (parsed.serverUrl && parsed.apiKey) {
|
|
4166
|
-
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
4167
|
-
console.log("[agentlife:notify] Notification config loaded from %s", configPath);
|
|
4168
|
-
return config;
|
|
4169
|
-
}
|
|
4170
|
-
console.warn("[agentlife:notify] Config missing serverUrl or apiKey");
|
|
4171
|
-
config = null;
|
|
4172
|
-
return null;
|
|
4173
|
-
} catch (e) {
|
|
4174
|
-
if (e?.code !== "ENOENT") {
|
|
4175
|
-
console.warn("[agentlife:notify] Failed to load notification config: %s", e?.message);
|
|
4176
|
-
}
|
|
4177
|
-
config = null;
|
|
4178
|
-
return null;
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
function notifyWidgetEvent(_state, event, surfaceId, title, body) {
|
|
4182
|
-
const cfg = loadConfig();
|
|
4183
|
-
if (!cfg)
|
|
4184
|
-
return;
|
|
4185
|
-
const now = Date.now();
|
|
4186
|
-
const lastNotified = recentNotifications.get(surfaceId);
|
|
4187
|
-
if (lastNotified && now - lastNotified < DEBOUNCE_MS)
|
|
4188
|
-
return;
|
|
4189
|
-
recentNotifications.set(surfaceId, now);
|
|
4190
|
-
if (recentNotifications.size > 100) {
|
|
4191
|
-
for (const [sid, ts] of recentNotifications) {
|
|
4192
|
-
if (now - ts > DEBOUNCE_MS)
|
|
4193
|
-
recentNotifications.delete(sid);
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
4196
|
-
const url = `${cfg.serverUrl.replace(/\/+$/, "")}/api/v1/notifications/send`;
|
|
4197
|
-
fetch(url, {
|
|
4198
|
-
method: "POST",
|
|
4199
|
-
headers: {
|
|
4200
|
-
"Content-Type": "application/json",
|
|
4201
|
-
Authorization: `Bearer ${cfg.apiKey}`
|
|
4202
|
-
},
|
|
4203
|
-
body: JSON.stringify({ title, body, data: { event, surfaceId } }),
|
|
4204
|
-
signal: AbortSignal.timeout(1e4)
|
|
4205
|
-
}).then((res) => {
|
|
4206
|
-
if (!res.ok) {
|
|
4207
|
-
console.warn("[agentlife:notify] Server responded %d for %s", res.status, surfaceId);
|
|
4208
|
-
}
|
|
4209
|
-
}).catch((err) => {
|
|
4210
|
-
console.warn("[agentlife:notify] Server failed for %s: %s", surfaceId, err?.message);
|
|
4211
|
-
});
|
|
4212
|
-
}
|
|
4213
|
-
|
|
4214
4370
|
// hooks/activity-hooks.ts
|
|
4215
4371
|
var sessionContext = new Map;
|
|
4216
4372
|
var pendingSnapshots = new Map;
|
|
4217
4373
|
var agentHealth = new Map;
|
|
4218
4374
|
var lastGlobalSupervisorRun = 0;
|
|
4219
4375
|
var PATHOLOGY = process.env.PATHOLOGY_TEST_MODE === "1";
|
|
4220
|
-
var
|
|
4376
|
+
var DEBOUNCE_MS = PATHOLOGY ? 30 * 1000 : 6 * 60 * 60 * 1000;
|
|
4221
4377
|
var CEILING_MS = PATHOLOGY ? 5 * 60 * 1000 : 72 * 60 * 60 * 1000;
|
|
4222
4378
|
var RECOVERY_MS = PATHOLOGY ? 5 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
|
4223
4379
|
function classifySignal(event, metadata) {
|
|
@@ -4365,10 +4521,6 @@ function registerActivityHooks(api, state) {
|
|
|
4365
4521
|
if (cronMatch) {
|
|
4366
4522
|
const cronSurfaceId = cronMatch[1];
|
|
4367
4523
|
recordSurfaceEvent(state, cronSurfaceId, "cron_fired", undefined, agentId ?? undefined);
|
|
4368
|
-
const view = getSurface(state, cronSurfaceId);
|
|
4369
|
-
const agentLabel = agentId ? agentId.charAt(0).toUpperCase() + agentId.slice(1) : "Agent";
|
|
4370
|
-
const surfaceTitle = extractCronTitle(view ? { lines: view.lines } : undefined) ?? "Updated";
|
|
4371
|
-
notifyWidgetEvent(state, "cron_followup", cronSurfaceId, agentLabel, surfaceTitle);
|
|
4372
4524
|
if (agentId && state.runCommand) {
|
|
4373
4525
|
state.runCommand(["openclaw", "agent", "--agent", agentId, "--message", message, "--json"], { timeoutMs: 120000 }).catch((e) => console.warn("[agentlife] followup redirect failed for %s: %s", cronSurfaceId, e?.message));
|
|
4374
4526
|
console.log("[agentlife] followup redirected to agent:%s for %s", agentId, cronSurfaceId);
|
|
@@ -4634,7 +4786,7 @@ function registerActivityHooks(api, state) {
|
|
|
4634
4786
|
h.state = newState;
|
|
4635
4787
|
h.enteredAt = now;
|
|
4636
4788
|
console.log("[agentlife:health] %s: %s → %s (%s)", agentId, prev, newState, reason);
|
|
4637
|
-
if (newState === "review" && state.runCommand && now - h.lastSupervisorRun >
|
|
4789
|
+
if (newState === "review" && state.runCommand && now - h.lastSupervisorRun > DEBOUNCE_MS) {
|
|
4638
4790
|
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
4639
4791
|
const metrics = computeEnhancedMetrics(db, agentId, sevenDaysAgo);
|
|
4640
4792
|
const parts = [`Agent ${agentId} entered REVIEW state: ${reason}.`];
|
|
@@ -4663,7 +4815,7 @@ ${parts.join(`
|
|
|
4663
4815
|
h.lastSupervisorRun = now;
|
|
4664
4816
|
}
|
|
4665
4817
|
}
|
|
4666
|
-
if (now - lastGlobalSupervisorRun >
|
|
4818
|
+
if (now - lastGlobalSupervisorRun > DEBOUNCE_MS && state.runCommand) {
|
|
4667
4819
|
let watchOrReviewCount = 0;
|
|
4668
4820
|
const degraded = [];
|
|
4669
4821
|
for (const [aid, ah] of agentHealth.entries()) {
|
|
@@ -4843,21 +4995,6 @@ function startQualityCheckPoller(state) {
|
|
|
4843
4995
|
}
|
|
4844
4996
|
};
|
|
4845
4997
|
}
|
|
4846
|
-
function extractCronTitle(meta) {
|
|
4847
|
-
if (!meta)
|
|
4848
|
-
return null;
|
|
4849
|
-
const headingLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"\s+h[1-4]/.test(l));
|
|
4850
|
-
if (headingLine)
|
|
4851
|
-
return headingLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
4852
|
-
const goalLine = meta.lines.find((l) => /^\s*goal:?\s+"[^"]+"/.test(l) || /^\s*goal:\s+\S/.test(l));
|
|
4853
|
-
if (goalLine) {
|
|
4854
|
-
return goalLine.match(/goal:?\s+"([^"]+)"/)?.[1] ?? goalLine.match(/goal:\s+(.+)/)?.[1]?.trim() ?? null;
|
|
4855
|
-
}
|
|
4856
|
-
const textLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"/.test(l));
|
|
4857
|
-
if (textLine)
|
|
4858
|
-
return textLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
4859
|
-
return null;
|
|
4860
|
-
}
|
|
4861
4998
|
function persistSessionTrace(state, sessionKey, agentId, durationMs) {
|
|
4862
4999
|
try {
|
|
4863
5000
|
const now = Date.now();
|
|
@@ -4922,7 +5059,7 @@ function rescueOrphanedSurfaces(state, agentId, error) {
|
|
|
4922
5059
|
}
|
|
4923
5060
|
|
|
4924
5061
|
// services/escalation.ts
|
|
4925
|
-
import * as
|
|
5062
|
+
import * as fs7 from "node:fs";
|
|
4926
5063
|
var PATHOLOGY2 = process.env.PATHOLOGY_TEST_MODE === "1";
|
|
4927
5064
|
var ESCALATION_REVIEW_THRESHOLD_MS = PATHOLOGY2 ? 2 * 60 * 1000 : 48 * 60 * 60 * 1000;
|
|
4928
5065
|
var TREND_WINDOW_DAYS = 7;
|
|
@@ -4985,10 +5122,10 @@ function clearEscalationSurface(state, agentId) {
|
|
|
4985
5122
|
const surfaceId = `escalation-${agentId}`;
|
|
4986
5123
|
if (!hasSurface(state, surfaceId))
|
|
4987
5124
|
return false;
|
|
4988
|
-
const
|
|
4989
|
-
if (
|
|
5125
|
+
const view2 = getSurface(state, surfaceId);
|
|
5126
|
+
if (view2 && !view2.isTransient && view2.path) {
|
|
4990
5127
|
try {
|
|
4991
|
-
|
|
5128
|
+
fs7.unlinkSync(view2.path);
|
|
4992
5129
|
} catch {}
|
|
4993
5130
|
}
|
|
4994
5131
|
state.surfaceIndex.delete(surfaceId);
|
|
@@ -5233,30 +5370,30 @@ import * as crypto4 from "node:crypto";
|
|
|
5233
5370
|
import * as fsSync3 from "node:fs";
|
|
5234
5371
|
import { createRequire as createRequire3 } from "node:module";
|
|
5235
5372
|
import * as os6 from "node:os";
|
|
5236
|
-
import * as
|
|
5373
|
+
import * as path10 from "node:path";
|
|
5237
5374
|
|
|
5238
5375
|
// gateway/web-app.ts
|
|
5239
5376
|
import * as crypto3 from "node:crypto";
|
|
5240
5377
|
import * as os5 from "node:os";
|
|
5241
|
-
import * as
|
|
5242
|
-
import * as
|
|
5378
|
+
import * as path9 from "node:path";
|
|
5379
|
+
import * as fs10 from "node:fs";
|
|
5243
5380
|
|
|
5244
5381
|
// services/pairing-access-token.ts
|
|
5245
5382
|
import * as crypto from "node:crypto";
|
|
5246
|
-
import * as
|
|
5383
|
+
import * as fs8 from "node:fs";
|
|
5247
5384
|
import * as os3 from "node:os";
|
|
5248
|
-
import * as
|
|
5385
|
+
import * as path7 from "node:path";
|
|
5249
5386
|
var cachedToken = null;
|
|
5250
5387
|
function pairingAccessPath() {
|
|
5251
|
-
return
|
|
5388
|
+
return path7.join(os3.homedir(), ".openclaw", "agentlife", "pairing-access.json");
|
|
5252
5389
|
}
|
|
5253
5390
|
function loadOrCreatePairingAccessToken() {
|
|
5254
5391
|
if (cachedToken)
|
|
5255
5392
|
return cachedToken;
|
|
5256
5393
|
const filePath = pairingAccessPath();
|
|
5257
5394
|
try {
|
|
5258
|
-
if (
|
|
5259
|
-
const raw =
|
|
5395
|
+
if (fs8.existsSync(filePath)) {
|
|
5396
|
+
const raw = fs8.readFileSync(filePath, "utf-8");
|
|
5260
5397
|
const obj = JSON.parse(raw);
|
|
5261
5398
|
const token2 = typeof obj?.token === "string" ? obj.token : null;
|
|
5262
5399
|
if (token2 && token2.length >= 32) {
|
|
@@ -5265,12 +5402,12 @@ function loadOrCreatePairingAccessToken() {
|
|
|
5265
5402
|
}
|
|
5266
5403
|
}
|
|
5267
5404
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
5268
|
-
const dir =
|
|
5269
|
-
if (!
|
|
5270
|
-
|
|
5271
|
-
|
|
5405
|
+
const dir = path7.dirname(filePath);
|
|
5406
|
+
if (!fs8.existsSync(dir))
|
|
5407
|
+
fs8.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5408
|
+
fs8.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
|
|
5272
5409
|
try {
|
|
5273
|
-
|
|
5410
|
+
fs8.chmodSync(filePath, 384);
|
|
5274
5411
|
} catch {}
|
|
5275
5412
|
cachedToken = token;
|
|
5276
5413
|
return token;
|
|
@@ -5283,18 +5420,18 @@ function loadOrCreatePairingAccessToken() {
|
|
|
5283
5420
|
// services/cloudflared-supervisor.ts
|
|
5284
5421
|
import { spawn, spawnSync } from "node:child_process";
|
|
5285
5422
|
import * as crypto2 from "node:crypto";
|
|
5286
|
-
import * as
|
|
5423
|
+
import * as fs9 from "node:fs";
|
|
5287
5424
|
import * as os4 from "node:os";
|
|
5288
|
-
import * as
|
|
5289
|
-
var AGENTLIFE_DIR =
|
|
5290
|
-
var TUNNEL_FILE =
|
|
5291
|
-
var BIN_DIR =
|
|
5292
|
-
var PAIR_REQUEST_MARKER =
|
|
5293
|
-
var IDENTITY_DIR =
|
|
5294
|
-
var DEVICE_FILE =
|
|
5295
|
-
var LEGACY_DEVICE_FILE =
|
|
5425
|
+
import * as path8 from "node:path";
|
|
5426
|
+
var AGENTLIFE_DIR = path8.join(os4.homedir(), ".openclaw", "agentlife");
|
|
5427
|
+
var TUNNEL_FILE = path8.join(AGENTLIFE_DIR, "tunnel.json");
|
|
5428
|
+
var BIN_DIR = path8.join(AGENTLIFE_DIR, "bin");
|
|
5429
|
+
var PAIR_REQUEST_MARKER = path8.join(AGENTLIFE_DIR, "pair-requested");
|
|
5430
|
+
var IDENTITY_DIR = path8.join(os4.homedir(), ".agentlife");
|
|
5431
|
+
var DEVICE_FILE = path8.join(IDENTITY_DIR, "device.json");
|
|
5432
|
+
var LEGACY_DEVICE_FILE = path8.join(AGENTLIFE_DIR, "device.json");
|
|
5296
5433
|
var CLOUDFLARED_BIN = os4.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
5297
|
-
var CLOUDFLARED_PATH =
|
|
5434
|
+
var CLOUDFLARED_PATH = path8.join(BIN_DIR, CLOUDFLARED_BIN);
|
|
5298
5435
|
var API_BASE = process.env.AGENTLIFE_API_BASE || "https://api.agentlife.app";
|
|
5299
5436
|
var RESTART_DELAY_MS = 5000;
|
|
5300
5437
|
var STABLE_RUNTIME_MS = 60000;
|
|
@@ -5414,20 +5551,20 @@ async function doBootstrap() {
|
|
|
5414
5551
|
return tunnelInfo;
|
|
5415
5552
|
}
|
|
5416
5553
|
function isPairRequested() {
|
|
5417
|
-
return
|
|
5554
|
+
return fs9.existsSync(PAIR_REQUEST_MARKER);
|
|
5418
5555
|
}
|
|
5419
5556
|
function requestPair() {
|
|
5420
5557
|
try {
|
|
5421
|
-
if (!
|
|
5422
|
-
|
|
5423
|
-
|
|
5558
|
+
if (!fs9.existsSync(AGENTLIFE_DIR))
|
|
5559
|
+
fs9.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
5560
|
+
fs9.writeFileSync(PAIR_REQUEST_MARKER, String(Date.now()), { mode: 384 });
|
|
5424
5561
|
} catch (err) {
|
|
5425
5562
|
console.warn(`[cloudflared-supervisor] failed to write pair-request marker: ${err?.message ?? err}`);
|
|
5426
5563
|
}
|
|
5427
5564
|
}
|
|
5428
5565
|
function clearPairRequest() {
|
|
5429
5566
|
try {
|
|
5430
|
-
|
|
5567
|
+
fs9.unlinkSync(PAIR_REQUEST_MARKER);
|
|
5431
5568
|
} catch {}
|
|
5432
5569
|
}
|
|
5433
5570
|
function parseRetryAfter(value) {
|
|
@@ -5451,18 +5588,18 @@ function scheduleProvisionRetry(delayMs) {
|
|
|
5451
5588
|
}, delayMs);
|
|
5452
5589
|
}
|
|
5453
5590
|
function ensureDirs() {
|
|
5454
|
-
if (!
|
|
5455
|
-
|
|
5456
|
-
if (!
|
|
5457
|
-
|
|
5458
|
-
if (!
|
|
5459
|
-
|
|
5591
|
+
if (!fs9.existsSync(AGENTLIFE_DIR))
|
|
5592
|
+
fs9.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
5593
|
+
if (!fs9.existsSync(BIN_DIR))
|
|
5594
|
+
fs9.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
|
|
5595
|
+
if (!fs9.existsSync(IDENTITY_DIR))
|
|
5596
|
+
fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
5460
5597
|
}
|
|
5461
5598
|
function readIdentityFile(filePath) {
|
|
5462
|
-
if (!
|
|
5599
|
+
if (!fs9.existsSync(filePath))
|
|
5463
5600
|
return null;
|
|
5464
5601
|
try {
|
|
5465
|
-
const raw =
|
|
5602
|
+
const raw = fs9.readFileSync(filePath, "utf-8");
|
|
5466
5603
|
const parsed = JSON.parse(raw);
|
|
5467
5604
|
if (typeof parsed.deviceId === "string" && typeof parsed.deviceSecret === "string") {
|
|
5468
5605
|
return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
|
|
@@ -5480,14 +5617,14 @@ function loadDeviceIdentity() {
|
|
|
5480
5617
|
if (!legacy)
|
|
5481
5618
|
return null;
|
|
5482
5619
|
try {
|
|
5483
|
-
if (!
|
|
5484
|
-
|
|
5485
|
-
|
|
5620
|
+
if (!fs9.existsSync(IDENTITY_DIR))
|
|
5621
|
+
fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
5622
|
+
fs9.writeFileSync(DEVICE_FILE, JSON.stringify(legacy, null, 2), { mode: 384 });
|
|
5486
5623
|
try {
|
|
5487
|
-
|
|
5624
|
+
fs9.chmodSync(DEVICE_FILE, 384);
|
|
5488
5625
|
} catch {}
|
|
5489
5626
|
try {
|
|
5490
|
-
|
|
5627
|
+
fs9.unlinkSync(LEGACY_DEVICE_FILE);
|
|
5491
5628
|
} catch {}
|
|
5492
5629
|
console.log(`[cloudflared-supervisor] migrated device.json to ${DEVICE_FILE}`);
|
|
5493
5630
|
} catch (err) {
|
|
@@ -5499,25 +5636,25 @@ function loadOrCreateDeviceIdentity() {
|
|
|
5499
5636
|
const existing = loadDeviceIdentity();
|
|
5500
5637
|
if (existing)
|
|
5501
5638
|
return existing;
|
|
5502
|
-
if (!
|
|
5503
|
-
|
|
5639
|
+
if (!fs9.existsSync(IDENTITY_DIR))
|
|
5640
|
+
fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
5504
5641
|
const identity = {
|
|
5505
5642
|
deviceId: crypto2.randomUUID(),
|
|
5506
5643
|
deviceSecret: crypto2.randomBytes(32).toString("base64url")
|
|
5507
5644
|
};
|
|
5508
|
-
|
|
5645
|
+
fs9.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5509
5646
|
try {
|
|
5510
|
-
|
|
5647
|
+
fs9.chmodSync(DEVICE_FILE, 384);
|
|
5511
5648
|
} catch {}
|
|
5512
5649
|
console.log(`[cloudflared-supervisor] generated new device identity (deviceId=${identity.deviceId})`);
|
|
5513
5650
|
return identity;
|
|
5514
5651
|
}
|
|
5515
5652
|
var AGENTLIFE_API_BASE = API_BASE;
|
|
5516
5653
|
function loadCachedTunnel() {
|
|
5517
|
-
if (!
|
|
5654
|
+
if (!fs9.existsSync(TUNNEL_FILE))
|
|
5518
5655
|
return null;
|
|
5519
5656
|
try {
|
|
5520
|
-
const raw =
|
|
5657
|
+
const raw = fs9.readFileSync(TUNNEL_FILE, "utf-8");
|
|
5521
5658
|
const parsed = JSON.parse(raw);
|
|
5522
5659
|
if (typeof parsed.subdomain === "string" && typeof parsed.hostname === "string" && typeof parsed.tunnelUrl === "string" && typeof parsed.tunnelToken === "string" && typeof parsed.provisionedAt === "number") {
|
|
5523
5660
|
return parsed;
|
|
@@ -5526,9 +5663,9 @@ function loadCachedTunnel() {
|
|
|
5526
5663
|
return null;
|
|
5527
5664
|
}
|
|
5528
5665
|
function persistTunnel(info) {
|
|
5529
|
-
|
|
5666
|
+
fs9.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
|
|
5530
5667
|
try {
|
|
5531
|
-
|
|
5668
|
+
fs9.chmodSync(TUNNEL_FILE, 384);
|
|
5532
5669
|
} catch {}
|
|
5533
5670
|
}
|
|
5534
5671
|
async function provisionTunnel(identity) {
|
|
@@ -5581,9 +5718,9 @@ async function provisionTunnel(identity) {
|
|
|
5581
5718
|
}
|
|
5582
5719
|
}
|
|
5583
5720
|
function ensureCloudflaredBinary() {
|
|
5584
|
-
if (
|
|
5721
|
+
if (fs9.existsSync(CLOUDFLARED_PATH)) {
|
|
5585
5722
|
try {
|
|
5586
|
-
|
|
5723
|
+
fs9.accessSync(CLOUDFLARED_PATH, fs9.constants.X_OK);
|
|
5587
5724
|
return CLOUDFLARED_PATH;
|
|
5588
5725
|
} catch {}
|
|
5589
5726
|
}
|
|
@@ -5601,28 +5738,28 @@ function ensureCloudflaredBinary() {
|
|
|
5601
5738
|
if (release.kind === "tgz") {
|
|
5602
5739
|
const extracted = extractTgzCloudflared(release.tempPath, BIN_DIR);
|
|
5603
5740
|
try {
|
|
5604
|
-
|
|
5741
|
+
fs9.unlinkSync(release.tempPath);
|
|
5605
5742
|
} catch {}
|
|
5606
5743
|
if (!extracted)
|
|
5607
5744
|
return null;
|
|
5608
5745
|
} else {
|
|
5609
5746
|
try {
|
|
5610
|
-
|
|
5747
|
+
fs9.renameSync(release.tempPath, CLOUDFLARED_PATH);
|
|
5611
5748
|
} catch (err) {
|
|
5612
5749
|
console.warn(`[cloudflared-supervisor] rename failed: ${err?.message}`);
|
|
5613
5750
|
return null;
|
|
5614
5751
|
}
|
|
5615
5752
|
}
|
|
5616
5753
|
try {
|
|
5617
|
-
|
|
5754
|
+
fs9.chmodSync(CLOUDFLARED_PATH, 493);
|
|
5618
5755
|
} catch {}
|
|
5619
|
-
return
|
|
5756
|
+
return fs9.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
|
|
5620
5757
|
}
|
|
5621
5758
|
function detectCloudflaredRelease(platform2, arch2) {
|
|
5622
5759
|
const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
5623
5760
|
if (platform2 === "darwin") {
|
|
5624
5761
|
const asset = arch2 === "arm64" ? "cloudflared-darwin-arm64.tgz" : "cloudflared-darwin-amd64.tgz";
|
|
5625
|
-
return { url: `${base}/${asset}`, kind: "tgz", tempPath:
|
|
5762
|
+
return { url: `${base}/${asset}`, kind: "tgz", tempPath: path8.join(BIN_DIR, asset) };
|
|
5626
5763
|
}
|
|
5627
5764
|
if (platform2 === "linux") {
|
|
5628
5765
|
const asset = arch2 === "arm64" ? "cloudflared-linux-arm64" : arch2 === "arm" ? "cloudflared-linux-arm" : arch2 === "x64" ? "cloudflared-linux-amd64" : null;
|
|
@@ -5646,11 +5783,11 @@ function downloadWithCurl(url, dest) {
|
|
|
5646
5783
|
if (result.status !== 0) {
|
|
5647
5784
|
console.warn(`[cloudflared-supervisor] curl failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
5648
5785
|
try {
|
|
5649
|
-
|
|
5786
|
+
fs9.unlinkSync(dest);
|
|
5650
5787
|
} catch {}
|
|
5651
5788
|
return false;
|
|
5652
5789
|
}
|
|
5653
|
-
return
|
|
5790
|
+
return fs9.existsSync(dest) && fs9.statSync(dest).size > 0;
|
|
5654
5791
|
}
|
|
5655
5792
|
function extractTgzCloudflared(tgzPath, destDir) {
|
|
5656
5793
|
const result = spawnSync("tar", ["-xzf", tgzPath, "-C", destDir], { stdio: "pipe" });
|
|
@@ -5658,11 +5795,29 @@ function extractTgzCloudflared(tgzPath, destDir) {
|
|
|
5658
5795
|
console.warn(`[cloudflared-supervisor] tar extract failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
5659
5796
|
return false;
|
|
5660
5797
|
}
|
|
5661
|
-
return
|
|
5798
|
+
return fs9.existsSync(CLOUDFLARED_PATH);
|
|
5799
|
+
}
|
|
5800
|
+
function killOrphanedCloudflareds(binPath) {
|
|
5801
|
+
if (os4.platform() === "win32")
|
|
5802
|
+
return;
|
|
5803
|
+
const found = spawnSync("pgrep", ["-f", binPath], { stdio: "pipe" });
|
|
5804
|
+
if (found.status !== 0)
|
|
5805
|
+
return;
|
|
5806
|
+
const pids = (found.stdout?.toString() ?? "").split(`
|
|
5807
|
+
`).map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
5808
|
+
if (pids.length === 0)
|
|
5809
|
+
return;
|
|
5810
|
+
console.warn(`[cloudflared-supervisor] killing ${pids.length} orphaned cloudflared process(es): ${pids.join(", ")}`);
|
|
5811
|
+
for (const pid of pids) {
|
|
5812
|
+
try {
|
|
5813
|
+
process.kill(pid, "SIGKILL");
|
|
5814
|
+
} catch {}
|
|
5815
|
+
}
|
|
5662
5816
|
}
|
|
5663
5817
|
function startCloudflaredProcess(binPath, tunnelToken) {
|
|
5664
5818
|
if (state.stopped)
|
|
5665
5819
|
return;
|
|
5820
|
+
killOrphanedCloudflareds(binPath);
|
|
5666
5821
|
const startedAt = Date.now();
|
|
5667
5822
|
const child = spawn(binPath, ["tunnel", "--no-autoupdate", "run", "--token", tunnelToken], {
|
|
5668
5823
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5715,11 +5870,11 @@ var MIME_TYPES = {
|
|
|
5715
5870
|
};
|
|
5716
5871
|
function mintBootstrapToken() {
|
|
5717
5872
|
const bootstrapToken = crypto3.randomBytes(32).toString("base64url");
|
|
5718
|
-
const devicesDir =
|
|
5719
|
-
const bootstrapPath =
|
|
5720
|
-
if (!
|
|
5721
|
-
|
|
5722
|
-
const registry =
|
|
5873
|
+
const devicesDir = path9.join(os5.homedir(), ".openclaw", "devices");
|
|
5874
|
+
const bootstrapPath = path9.join(devicesDir, "bootstrap.json");
|
|
5875
|
+
if (!fs10.existsSync(devicesDir))
|
|
5876
|
+
fs10.mkdirSync(devicesDir, { recursive: true });
|
|
5877
|
+
const registry = fs10.existsSync(bootstrapPath) ? JSON.parse(fs10.readFileSync(bootstrapPath, "utf-8")) : {};
|
|
5723
5878
|
registry[bootstrapToken] = {
|
|
5724
5879
|
token: bootstrapToken,
|
|
5725
5880
|
profile: {
|
|
@@ -5728,15 +5883,15 @@ function mintBootstrapToken() {
|
|
|
5728
5883
|
},
|
|
5729
5884
|
issuedAtMs: Date.now()
|
|
5730
5885
|
};
|
|
5731
|
-
|
|
5886
|
+
fs10.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
|
|
5732
5887
|
return bootstrapToken;
|
|
5733
5888
|
}
|
|
5734
5889
|
function registerWebApp(api) {
|
|
5735
|
-
const pluginRoot =
|
|
5736
|
-
const appRoot =
|
|
5737
|
-
const hasWebBuild =
|
|
5738
|
-
const indexPath = hasWebBuild ?
|
|
5739
|
-
const hasIndex = !!(indexPath &&
|
|
5890
|
+
const pluginRoot = path9.resolve(path9.dirname(api.source), "..");
|
|
5891
|
+
const appRoot = path9.join(pluginRoot, "web-build");
|
|
5892
|
+
const hasWebBuild = fs10.existsSync(appRoot);
|
|
5893
|
+
const indexPath = hasWebBuild ? path9.join(appRoot, "index.html") : null;
|
|
5894
|
+
const hasIndex = !!(indexPath && fs10.existsSync(indexPath));
|
|
5740
5895
|
if (!hasWebBuild) {
|
|
5741
5896
|
api.logger.info("[agentlife] web-build/ not found — /agentlife/pair will still register; static dashboard disabled");
|
|
5742
5897
|
} else if (!hasIndex) {
|
|
@@ -5744,8 +5899,8 @@ function registerWebApp(api) {
|
|
|
5744
5899
|
}
|
|
5745
5900
|
let gatewayToken = "";
|
|
5746
5901
|
try {
|
|
5747
|
-
const configPath =
|
|
5748
|
-
const raw = JSON.parse(
|
|
5902
|
+
const configPath = path9.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
|
|
5903
|
+
const raw = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
5749
5904
|
gatewayToken = raw?.gateway?.auth?.token || "";
|
|
5750
5905
|
} catch {}
|
|
5751
5906
|
loadOrCreatePairingAccessToken();
|
|
@@ -5863,16 +6018,16 @@ function registerWebApp(api) {
|
|
|
5863
6018
|
return true;
|
|
5864
6019
|
}
|
|
5865
6020
|
const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
|
|
5866
|
-
const filePath =
|
|
6021
|
+
const filePath = path9.resolve(appRoot, relative);
|
|
5867
6022
|
if (!filePath.startsWith(appRoot)) {
|
|
5868
6023
|
res.writeHead(403);
|
|
5869
6024
|
res.end();
|
|
5870
6025
|
return true;
|
|
5871
6026
|
}
|
|
5872
|
-
const target =
|
|
5873
|
-
const ext =
|
|
6027
|
+
const target = fs10.existsSync(filePath) && fs10.statSync(filePath).isFile() ? filePath : indexPath;
|
|
6028
|
+
const ext = path9.extname(target).toLowerCase();
|
|
5874
6029
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
5875
|
-
let content =
|
|
6030
|
+
let content = fs10.readFileSync(target);
|
|
5876
6031
|
if (target === indexPath) {
|
|
5877
6032
|
setTimeout(approveLatest, 2000);
|
|
5878
6033
|
setTimeout(approveLatest, 5000);
|
|
@@ -5961,9 +6116,9 @@ Setup code: ${setupCode}
|
|
|
5961
6116
|
api.registerService({
|
|
5962
6117
|
id: "agentlife-auto-pair",
|
|
5963
6118
|
start: async (ctx) => {
|
|
5964
|
-
const devicesDir =
|
|
5965
|
-
const pendingPath =
|
|
5966
|
-
const pairedPath =
|
|
6119
|
+
const devicesDir = path10.join(os6.homedir(), ".openclaw", "devices");
|
|
6120
|
+
const pendingPath = path10.join(devicesDir, "pending.json");
|
|
6121
|
+
const pairedPath = path10.join(devicesDir, "paired.json");
|
|
5967
6122
|
const pollInterval = 3000;
|
|
5968
6123
|
const withAdmin = (arr) => {
|
|
5969
6124
|
const base = Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
@@ -6216,6 +6371,111 @@ function registerBootstrapHookImpl(api, state2) {
|
|
|
6216
6371
|
}, { name: "agentlife-a2ui-guidance", description: "Inject A2UI component catalog and dashboard rules" });
|
|
6217
6372
|
}
|
|
6218
6373
|
|
|
6374
|
+
// notifications.ts
|
|
6375
|
+
import * as fs11 from "node:fs";
|
|
6376
|
+
import * as path11 from "node:path";
|
|
6377
|
+
var config;
|
|
6378
|
+
function loadConfig() {
|
|
6379
|
+
if (config !== undefined)
|
|
6380
|
+
return config;
|
|
6381
|
+
try {
|
|
6382
|
+
const configPath = path11.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
6383
|
+
const raw = fs11.readFileSync(configPath, "utf-8");
|
|
6384
|
+
const parsed = JSON.parse(raw);
|
|
6385
|
+
if (parsed.serverUrl && parsed.apiKey) {
|
|
6386
|
+
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
6387
|
+
console.log("[agentlife:notify] Notification config loaded from %s", configPath);
|
|
6388
|
+
return config;
|
|
6389
|
+
}
|
|
6390
|
+
console.warn("[agentlife:notify] Config missing serverUrl or apiKey");
|
|
6391
|
+
config = null;
|
|
6392
|
+
return null;
|
|
6393
|
+
} catch (e) {
|
|
6394
|
+
if (e?.code !== "ENOENT") {
|
|
6395
|
+
console.warn("[agentlife:notify] Failed to load notification config: %s", e?.message);
|
|
6396
|
+
}
|
|
6397
|
+
config = null;
|
|
6398
|
+
return null;
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
function notifyWidgetEvent(_state, event, surfaceId, title, body) {
|
|
6402
|
+
broadcastNotification(event, surfaceId, title, body);
|
|
6403
|
+
postToMobile(event, surfaceId, title, body).catch((err) => {
|
|
6404
|
+
console.warn("[agentlife:notify] mobile push pipeline error: %s", err?.message ?? err);
|
|
6405
|
+
});
|
|
6406
|
+
}
|
|
6407
|
+
async function postToMobile(event, surfaceId, title, body) {
|
|
6408
|
+
const identity = loadDeviceIdentity();
|
|
6409
|
+
let deliveredViaTunnels = false;
|
|
6410
|
+
if (identity) {
|
|
6411
|
+
deliveredViaTunnels = await tryTunnelsNotify(identity.deviceId, identity.deviceSecret, event, surfaceId, title, body);
|
|
6412
|
+
} else {
|
|
6413
|
+
console.log("[agentlife:notify] no device identity — skipping /tunnels/notify");
|
|
6414
|
+
}
|
|
6415
|
+
if (!deliveredViaTunnels) {
|
|
6416
|
+
await tryApiKeyNotify(event, surfaceId, title, body);
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
async function tryTunnelsNotify(deviceId, deviceSecret, event, surfaceId, title, body) {
|
|
6420
|
+
const url = `${AGENTLIFE_API_BASE.replace(/\/+$/, "")}/api/v1/tunnels/notify`;
|
|
6421
|
+
try {
|
|
6422
|
+
const res = await fetch(url, {
|
|
6423
|
+
method: "POST",
|
|
6424
|
+
headers: { "Content-Type": "application/json" },
|
|
6425
|
+
body: JSON.stringify({
|
|
6426
|
+
deviceId,
|
|
6427
|
+
deviceSecret,
|
|
6428
|
+
gatewayId: deviceId,
|
|
6429
|
+
kind: event,
|
|
6430
|
+
title,
|
|
6431
|
+
body,
|
|
6432
|
+
data: { surfaceId }
|
|
6433
|
+
}),
|
|
6434
|
+
signal: AbortSignal.timeout(1e4)
|
|
6435
|
+
});
|
|
6436
|
+
if (!res.ok) {
|
|
6437
|
+
console.warn("[agentlife:notify] /tunnels/notify status=%d event=%s surfaceId=%s", res.status, event, surfaceId);
|
|
6438
|
+
return false;
|
|
6439
|
+
}
|
|
6440
|
+
const parsed = await res.json().catch(() => ({}));
|
|
6441
|
+
if (parsed.skipped === "unclaimed") {
|
|
6442
|
+
console.log("[agentlife:notify] /tunnels/notify unclaimed event=%s surfaceId=%s — trying apiKey fallback", event, surfaceId);
|
|
6443
|
+
return false;
|
|
6444
|
+
}
|
|
6445
|
+
const delivered = parsed.delivered ?? 0;
|
|
6446
|
+
console.log("[agentlife:notify] /tunnels/notify event=%s surfaceId=%s delivered=%d", event, surfaceId, delivered);
|
|
6447
|
+
return delivered > 0;
|
|
6448
|
+
} catch (err) {
|
|
6449
|
+
console.warn("[agentlife:notify] /tunnels/notify error: %s", err?.message ?? err);
|
|
6450
|
+
return false;
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6453
|
+
async function tryApiKeyNotify(event, surfaceId, title, body) {
|
|
6454
|
+
const cfg = loadConfig();
|
|
6455
|
+
if (!cfg)
|
|
6456
|
+
return;
|
|
6457
|
+
const url = `${cfg.serverUrl.replace(/\/+$/, "")}/api/v1/notifications/send`;
|
|
6458
|
+
try {
|
|
6459
|
+
const res = await fetch(url, {
|
|
6460
|
+
method: "POST",
|
|
6461
|
+
headers: {
|
|
6462
|
+
"Content-Type": "application/json",
|
|
6463
|
+
Authorization: `Bearer ${cfg.apiKey}`
|
|
6464
|
+
},
|
|
6465
|
+
body: JSON.stringify({ title, body, data: { event, surfaceId } }),
|
|
6466
|
+
signal: AbortSignal.timeout(1e4)
|
|
6467
|
+
});
|
|
6468
|
+
if (!res.ok) {
|
|
6469
|
+
console.warn("[agentlife:notify] /notifications/send status=%d event=%s surfaceId=%s", res.status, event, surfaceId);
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6472
|
+
const parsed = await res.json().catch(() => null);
|
|
6473
|
+
console.log("[agentlife:notify] /notifications/send event=%s surfaceId=%s messageId=%s", event, surfaceId, parsed?.messageId ?? "(none)");
|
|
6474
|
+
} catch (err) {
|
|
6475
|
+
console.warn("[agentlife:notify] /notifications/send error: %s", err?.message ?? err);
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6219
6479
|
// tools/widget-push.ts
|
|
6220
6480
|
var PROVISIONED_IDS2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6221
6481
|
function isKnownAgent(state2, agentId, api) {
|
|
@@ -6330,13 +6590,21 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6330
6590
|
const filteredDsl = finalBlocks.join(`
|
|
6331
6591
|
---
|
|
6332
6592
|
`);
|
|
6333
|
-
const
|
|
6593
|
+
const deletedSurfaceInfo = new Map;
|
|
6334
6594
|
for (const raw of finalBlocks) {
|
|
6335
|
-
const
|
|
6336
|
-
if (!
|
|
6595
|
+
const deleteMatch = raw.match(/^delete\s+(\S+)/m);
|
|
6596
|
+
if (!deleteMatch)
|
|
6337
6597
|
continue;
|
|
6338
|
-
|
|
6339
|
-
|
|
6598
|
+
const sid = deleteMatch[1];
|
|
6599
|
+
const view2 = getSurface(state2, sid);
|
|
6600
|
+
if (!view2)
|
|
6601
|
+
continue;
|
|
6602
|
+
if (view2.isOverlay || view2.isInput)
|
|
6603
|
+
continue;
|
|
6604
|
+
deletedSurfaceInfo.set(sid, {
|
|
6605
|
+
title: capitalize(agentId),
|
|
6606
|
+
body: `Removed: ${extractWidgetText(view2.lines) ?? sid}`
|
|
6607
|
+
});
|
|
6340
6608
|
}
|
|
6341
6609
|
let results;
|
|
6342
6610
|
try {
|
|
@@ -6345,22 +6613,32 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6345
6613
|
return { content: [{ type: "text", text: `Error: push failed — ${err?.message ?? err}` }] };
|
|
6346
6614
|
}
|
|
6347
6615
|
const pushedSurfaceIds = results.filter((r) => r.kind === "push").map((r) => r.surfaceId);
|
|
6348
|
-
for (const
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6616
|
+
for (const result of results) {
|
|
6617
|
+
if (result.kind === "push") {
|
|
6618
|
+
if (result.isLoading)
|
|
6619
|
+
continue;
|
|
6620
|
+
const view2 = getSurface(state2, result.surfaceId);
|
|
6621
|
+
if (!view2)
|
|
6622
|
+
continue;
|
|
6623
|
+
if (view2.isOverlay || view2.isInput)
|
|
6624
|
+
continue;
|
|
6625
|
+
const notifTitle = capitalize(agentId);
|
|
6626
|
+
const notifBody = extractWidgetText(view2.lines) ?? "New update";
|
|
6627
|
+
const event = result.isNew ? "surface_created" : "surface_updated";
|
|
6628
|
+
notifyWidgetEvent(state2, event, result.surfaceId, notifTitle, notifBody);
|
|
6629
|
+
} else if (result.kind === "delete") {
|
|
6630
|
+
const info = deletedSurfaceInfo.get(result.surfaceId);
|
|
6631
|
+
if (!info)
|
|
6632
|
+
continue;
|
|
6633
|
+
notifyWidgetEvent(state2, "surface_deleted", result.surfaceId, info.title, info.body);
|
|
6634
|
+
}
|
|
6357
6635
|
}
|
|
6358
6636
|
for (const result of results) {
|
|
6359
6637
|
if (result.kind !== "push")
|
|
6360
6638
|
continue;
|
|
6361
|
-
const
|
|
6362
|
-
if (
|
|
6363
|
-
autoRegisterInfraFromContext(state2, result.surfaceId, agentId,
|
|
6639
|
+
const view2 = getSurface(state2, result.surfaceId);
|
|
6640
|
+
if (view2?.context) {
|
|
6641
|
+
autoRegisterInfraFromContext(state2, result.surfaceId, agentId, view2.context);
|
|
6364
6642
|
}
|
|
6365
6643
|
}
|
|
6366
6644
|
if (sessionKey && pushedSurfaceIds.length > 0) {
|
|
@@ -6370,6 +6648,8 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6370
6648
|
const goalChanges = [];
|
|
6371
6649
|
const missingFollowup = [];
|
|
6372
6650
|
const missingDetail = [];
|
|
6651
|
+
const templatedFollowups = [];
|
|
6652
|
+
const proliferationHits = [];
|
|
6373
6653
|
for (const r of results) {
|
|
6374
6654
|
if (r.kind !== "push")
|
|
6375
6655
|
continue;
|
|
@@ -6379,17 +6659,29 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6379
6659
|
data: JSON.stringify({ issue: "goal_changed", surfaceId: r.surfaceId, from: r.goalChanged.from, to: r.goalChanged.to })
|
|
6380
6660
|
});
|
|
6381
6661
|
}
|
|
6662
|
+
if (r.followupTemplated) {
|
|
6663
|
+
templatedFollowups.push({ surfaceId: r.surfaceId, ...r.followupTemplated });
|
|
6664
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
6665
|
+
data: JSON.stringify({ issue: "followup_templated", surfaceId: r.surfaceId, previous: r.followupTemplated.previous, next: r.followupTemplated.next })
|
|
6666
|
+
});
|
|
6667
|
+
}
|
|
6668
|
+
if (r.siblingSurfaces && r.siblingSurfaces.length > 0) {
|
|
6669
|
+
proliferationHits.push({ surfaceId: r.surfaceId, siblings: r.siblingSurfaces });
|
|
6670
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
6671
|
+
data: JSON.stringify({ issue: "surface_proliferation", surfaceId: r.surfaceId, siblings: r.siblingSurfaces })
|
|
6672
|
+
});
|
|
6673
|
+
}
|
|
6382
6674
|
if (r.isTransient)
|
|
6383
6675
|
continue;
|
|
6384
|
-
const
|
|
6385
|
-
const isLoading =
|
|
6386
|
-
if (
|
|
6676
|
+
const view2 = getSurface(state2, r.surfaceId);
|
|
6677
|
+
const isLoading = r.isLoading;
|
|
6678
|
+
if (view2 && !isLoading && !view2.followup && !goalChanges.includes(r.surfaceId)) {
|
|
6387
6679
|
missingFollowup.push(r.surfaceId);
|
|
6388
6680
|
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
6389
6681
|
data: JSON.stringify({ issue: "missing_followup", surfaceId: r.surfaceId })
|
|
6390
6682
|
});
|
|
6391
6683
|
}
|
|
6392
|
-
if (
|
|
6684
|
+
if (view2 && !isLoading && !view2.isInput && !hasDetailContent(view2.rawDsl) && !goalChanges.includes(r.surfaceId) && !missingFollowup.includes(r.surfaceId)) {
|
|
6393
6685
|
missingDetail.push(r.surfaceId);
|
|
6394
6686
|
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
6395
6687
|
data: JSON.stringify({ issue: "missing_detail", surfaceId: r.surfaceId })
|
|
@@ -6402,6 +6694,10 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6402
6694
|
errors.push(buildMissingFollowupError(missingFollowup));
|
|
6403
6695
|
if (missingDetail.length > 0)
|
|
6404
6696
|
errors.push(buildMissingDetailError(missingDetail));
|
|
6697
|
+
if (templatedFollowups.length > 0)
|
|
6698
|
+
errors.push(buildTemplatedFollowupError(templatedFollowups));
|
|
6699
|
+
if (proliferationHits.length > 0)
|
|
6700
|
+
errors.push(buildSurfaceProliferationError(proliferationHits));
|
|
6405
6701
|
const validationErrors = collectValidationErrors(state2, results, sessionKey, agentId);
|
|
6406
6702
|
errors.push(...validationErrors);
|
|
6407
6703
|
if (ownershipViolations.length > 0) {
|
|
@@ -6436,6 +6732,27 @@ function buildMissingFollowupError(ids) {
|
|
|
6436
6732
|
function buildMissingDetailError(ids) {
|
|
6437
6733
|
return `[QUALITY ERROR] ${ids.join(", ")}: missing detail. Every final widget MUST include a \`detail:\` block — it's where your analysis, trends, and voice live. The face alone is just a receipt. Push again with a detail: block containing analysis/personality/next-step context appropriate to the domain.`;
|
|
6438
6734
|
}
|
|
6735
|
+
function buildSurfaceProliferationError(hits) {
|
|
6736
|
+
const detail = hits.map((h) => {
|
|
6737
|
+
const siblingLines = h.siblings.map((s) => ` ${s.surfaceId} — goal: "${s.goal}"`).join(`
|
|
6738
|
+
`);
|
|
6739
|
+
return `${h.surfaceId} has live sibling(s) with near-identical goals:
|
|
6740
|
+
${siblingLines}`;
|
|
6741
|
+
}).join(`
|
|
6742
|
+
`);
|
|
6743
|
+
return `[QUALITY ERROR] surface proliferation detected — the goal you just pushed differs from another live goal owned by this agent only by a parameter dimension (a value that varies between instances but does not change the goal's purpose). Parallel widgets per parameter value violate the consolidation rule.
|
|
6744
|
+
${detail}
|
|
6745
|
+
` + `Resolve by EITHER (a) consolidating: delete the parallel widgets and push ONE widget with that parameter as widget state, or (b) revising the goals so each is genuinely distinct (different success conditions, not just different parameter values). Continuing to push parallel widgets makes the dashboard one-widget-per-moment instead of one-widget-per-goal.`;
|
|
6746
|
+
}
|
|
6747
|
+
function buildTemplatedFollowupError(hits) {
|
|
6748
|
+
const detail = hits.map((h) => `${h.surfaceId}:
|
|
6749
|
+
previous: "${h.previous}"
|
|
6750
|
+
new: "${h.next}"`).join(`
|
|
6751
|
+
`);
|
|
6752
|
+
return `[QUALITY ERROR] templated followup detected — the new followup is the previous one with parameters rotated. The followup must be a hypothesis derived from THIS cycle's findings, not a procedure repeated.
|
|
6753
|
+
${detail}
|
|
6754
|
+
` + `Rewrite the followup so it (a) names a signal observable at the next fire that does not exist now, (b) states a threshold or condition that would change the next move, and (c) implies a real action under each outcome. If the only thing you can produce is a generic data-operation procedure, the widget is stagnant — delete it or push an input surface asking the user what would unblock progress.`;
|
|
6755
|
+
}
|
|
6439
6756
|
function collectValidationErrors(state2, results, sessionKey, agentId) {
|
|
6440
6757
|
const errors = [];
|
|
6441
6758
|
const unknownKeywordHits = [];
|
|
@@ -7220,17 +7537,17 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7220
7537
|
respond(false, { error: "missing surfaceId" });
|
|
7221
7538
|
return;
|
|
7222
7539
|
}
|
|
7223
|
-
const
|
|
7224
|
-
if (!
|
|
7540
|
+
const view2 = getSurface(state2, surfaceId);
|
|
7541
|
+
if (!view2) {
|
|
7225
7542
|
respond(true, { surfaceId, dismissed: false });
|
|
7226
7543
|
return;
|
|
7227
7544
|
}
|
|
7228
7545
|
const reason = typeof params?.reason === "string" ? params.reason : null;
|
|
7229
7546
|
const guided = params?.guided === true;
|
|
7230
7547
|
if (guided) {
|
|
7231
|
-
const agentId2 =
|
|
7548
|
+
const agentId2 = view2.agentId;
|
|
7232
7549
|
if (agentId2 && state2.runCommand) {
|
|
7233
|
-
const goalSnippet =
|
|
7550
|
+
const goalSnippet = view2.goal ? ` goal="${view2.goal}"` : "";
|
|
7234
7551
|
const reasonSnippet = reason ? ` reason="${reason}"` : "";
|
|
7235
7552
|
const sessionKey = buildAgentSessionKey(agentId2);
|
|
7236
7553
|
state2.pendingReactiveGuidance.set(sessionKey, DISMISS_ALTERNATIVES_GUIDANCE);
|
|
@@ -7250,7 +7567,7 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7250
7567
|
}
|
|
7251
7568
|
console.log("[agentlife] guided dismiss: no agent for %s, falling through to immediate", surfaceId);
|
|
7252
7569
|
}
|
|
7253
|
-
const agentId =
|
|
7570
|
+
const agentId = view2.agentId;
|
|
7254
7571
|
const cronIdRow = state2.followupQueue?.nextPending(surfaceId) ?? null;
|
|
7255
7572
|
const cronId = cronIdRow ? String(cronIdRow.id) : null;
|
|
7256
7573
|
const dismissMetadata = reason ? JSON.stringify({ reason }) : undefined;
|
|
@@ -7261,9 +7578,9 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7261
7578
|
automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
|
|
7262
7579
|
} catch {}
|
|
7263
7580
|
guidedDismissSent.delete(surfaceId);
|
|
7264
|
-
if (!
|
|
7581
|
+
if (!view2.isTransient && view2.path) {
|
|
7265
7582
|
try {
|
|
7266
|
-
fs13.unlinkSync(
|
|
7583
|
+
fs13.unlinkSync(view2.path);
|
|
7267
7584
|
} catch (err) {
|
|
7268
7585
|
if (err?.code !== "ENOENT")
|
|
7269
7586
|
console.warn("[agentlife] dismiss: file unlink failed for %s: %s", surfaceId, err?.message);
|
|
@@ -7336,9 +7653,9 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7336
7653
|
respond(false, { error: "missing surfaceId" });
|
|
7337
7654
|
return;
|
|
7338
7655
|
}
|
|
7339
|
-
const
|
|
7340
|
-
const agentId =
|
|
7341
|
-
const ctx =
|
|
7656
|
+
const view2 = getSurface(state2, surfaceId);
|
|
7657
|
+
const agentId = view2?.agentId ?? null;
|
|
7658
|
+
const ctx = view2?.context;
|
|
7342
7659
|
if (ctx && ctx.escalation === true && typeof ctx.target === "string") {
|
|
7343
7660
|
const target = ctx.target;
|
|
7344
7661
|
const sessionKey2 = buildAgentSessionKey(target);
|
|
@@ -7638,6 +7955,17 @@ function registerAdminGateway(api, state2) {
|
|
|
7638
7955
|
respond(false, { error: err?.message ?? "quality metrics unavailable" });
|
|
7639
7956
|
}
|
|
7640
7957
|
}, { scope: "operator.read" });
|
|
7958
|
+
api.registerGatewayMethod("agentlife.health.list", ({ respond }) => {
|
|
7959
|
+
try {
|
|
7960
|
+
const agents = {};
|
|
7961
|
+
for (const [agentId, h] of agentHealth.entries()) {
|
|
7962
|
+
agents[agentId] = { state: h.state, enteredAt: h.enteredAt };
|
|
7963
|
+
}
|
|
7964
|
+
respond(true, { agents });
|
|
7965
|
+
} catch (err) {
|
|
7966
|
+
respond(false, undefined, { code: "HEALTH_LIST_FAILED", message: err?.message ?? "health list failed" });
|
|
7967
|
+
}
|
|
7968
|
+
}, { scope: "operator.read" });
|
|
7641
7969
|
api.registerGatewayMethod("agentlife.observability.runCycle", ({ params, respond }) => {
|
|
7642
7970
|
try {
|
|
7643
7971
|
const day = typeof params?.day === "string" && /^\d{4}-\d{2}-\d{2}$/.test(params.day) ? params.day : new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
@@ -7847,6 +8175,180 @@ function registerAdminGateway(api, state2) {
|
|
|
7847
8175
|
}, { scope: "operator.write" });
|
|
7848
8176
|
}
|
|
7849
8177
|
|
|
8178
|
+
// gateway/evolutions.ts
|
|
8179
|
+
import * as fsSync5 from "node:fs";
|
|
8180
|
+
import * as os9 from "node:os";
|
|
8181
|
+
import * as path14 from "node:path";
|
|
8182
|
+
var ALLOWED_FILENAMES = new Set(["AGENTS.md", "USER.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"]);
|
|
8183
|
+
function workspaceFilePath(agentId, filename) {
|
|
8184
|
+
return path14.join(os9.homedir(), ".openclaw", `workspace-${agentId}`, filename);
|
|
8185
|
+
}
|
|
8186
|
+
function hasRegressionFlag(db, agentId, filename, snapshotAt) {
|
|
8187
|
+
const horizon = snapshotAt + 7 * 24 * 60 * 60 * 1000;
|
|
8188
|
+
const row = db.prepare(`SELECT 1 FROM activity_log
|
|
8189
|
+
WHERE event='quality_warning' AND agentId = ? AND ts BETWEEN ? AND ?
|
|
8190
|
+
AND data LIKE '%"rewrite_regression"%' AND data LIKE ?
|
|
8191
|
+
LIMIT 1`).get(agentId, snapshotAt, horizon, `%"filename":"${filename}"%`);
|
|
8192
|
+
return !!row;
|
|
8193
|
+
}
|
|
8194
|
+
function registerEvolutionsGateway(api, state2) {
|
|
8195
|
+
api.registerGatewayMethod("agentlife.evolutions.list", ({ params, respond }) => {
|
|
8196
|
+
try {
|
|
8197
|
+
const agentId = String(params?.agentId ?? "").trim();
|
|
8198
|
+
if (!agentId) {
|
|
8199
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "agentId required" });
|
|
8200
|
+
return;
|
|
8201
|
+
}
|
|
8202
|
+
const filename = typeof params?.filename === "string" ? params.filename.trim() : null;
|
|
8203
|
+
const limit = Math.min(Math.max(Number(params?.limit) || 50, 1), 100);
|
|
8204
|
+
const db = getOrCreateHistoryDb(state2);
|
|
8205
|
+
const rows = filename ? db.prepare(`SELECT id, agentId, filename, createdAt, note, length(content) AS charCount
|
|
8206
|
+
FROM bootstrap_versions WHERE agentId = ? AND filename = ?
|
|
8207
|
+
ORDER BY createdAt DESC LIMIT ?`).all(agentId, filename, limit) : db.prepare(`SELECT id, agentId, filename, createdAt, note, length(content) AS charCount
|
|
8208
|
+
FROM bootstrap_versions WHERE agentId = ?
|
|
8209
|
+
ORDER BY createdAt DESC LIMIT ?`).all(agentId, limit);
|
|
8210
|
+
const versions = rows.map((r) => ({
|
|
8211
|
+
versionId: r.id,
|
|
8212
|
+
agentId: r.agentId,
|
|
8213
|
+
filename: r.filename,
|
|
8214
|
+
createdAt: r.createdAt,
|
|
8215
|
+
charCount: r.charCount,
|
|
8216
|
+
note: r.note ?? null,
|
|
8217
|
+
regressionFlag: hasRegressionFlag(db, r.agentId, r.filename, r.createdAt)
|
|
8218
|
+
}));
|
|
8219
|
+
respond(true, { versions });
|
|
8220
|
+
} catch (err) {
|
|
8221
|
+
respond(false, undefined, { code: "EVOLUTIONS_LIST_FAILED", message: err?.message ?? "list failed" });
|
|
8222
|
+
}
|
|
8223
|
+
}, { scope: "operator.read" });
|
|
8224
|
+
api.registerGatewayMethod("agentlife.evolutions.get", ({ params, respond }) => {
|
|
8225
|
+
try {
|
|
8226
|
+
const versionId = Number(params?.versionId);
|
|
8227
|
+
if (!Number.isFinite(versionId) || versionId <= 0) {
|
|
8228
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "versionId required" });
|
|
8229
|
+
return;
|
|
8230
|
+
}
|
|
8231
|
+
const db = getOrCreateHistoryDb(state2);
|
|
8232
|
+
const row = db.prepare(`SELECT id, agentId, filename, content, createdAt, note FROM bootstrap_versions WHERE id = ?`).get(versionId);
|
|
8233
|
+
if (!row) {
|
|
8234
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `version ${versionId} not found` });
|
|
8235
|
+
return;
|
|
8236
|
+
}
|
|
8237
|
+
respond(true, {
|
|
8238
|
+
versionId: row.id,
|
|
8239
|
+
agentId: row.agentId,
|
|
8240
|
+
filename: row.filename,
|
|
8241
|
+
content: row.content,
|
|
8242
|
+
createdAt: row.createdAt,
|
|
8243
|
+
note: row.note ?? null
|
|
8244
|
+
});
|
|
8245
|
+
} catch (err) {
|
|
8246
|
+
respond(false, undefined, { code: "EVOLUTIONS_GET_FAILED", message: err?.message ?? "get failed" });
|
|
8247
|
+
}
|
|
8248
|
+
}, { scope: "operator.read" });
|
|
8249
|
+
api.registerGatewayMethod("agentlife.evolutions.recent", ({ params, respond }) => {
|
|
8250
|
+
try {
|
|
8251
|
+
const agentId = String(params?.agentId ?? "").trim();
|
|
8252
|
+
if (!agentId) {
|
|
8253
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "agentId required" });
|
|
8254
|
+
return;
|
|
8255
|
+
}
|
|
8256
|
+
const limit = Math.min(Math.max(Number(params?.limit) || 5, 1), 20);
|
|
8257
|
+
const db = getOrCreateHistoryDb(state2);
|
|
8258
|
+
const rows = db.prepare(`SELECT id, agentId, filename, createdAt, note FROM bootstrap_versions
|
|
8259
|
+
WHERE agentId = ? AND note IS NOT NULL AND note != ''
|
|
8260
|
+
ORDER BY createdAt DESC LIMIT ?`).all(agentId, limit);
|
|
8261
|
+
const versions = rows.map((r) => ({
|
|
8262
|
+
versionId: r.id,
|
|
8263
|
+
agentId: r.agentId,
|
|
8264
|
+
filename: r.filename,
|
|
8265
|
+
createdAt: r.createdAt,
|
|
8266
|
+
note: r.note
|
|
8267
|
+
}));
|
|
8268
|
+
respond(true, { versions });
|
|
8269
|
+
} catch (err) {
|
|
8270
|
+
respond(false, undefined, { code: "EVOLUTIONS_RECENT_FAILED", message: err?.message ?? "recent failed" });
|
|
8271
|
+
}
|
|
8272
|
+
}, { scope: "operator.read" });
|
|
8273
|
+
api.registerGatewayMethod("agentlife.evolutions.restore", ({ params, respond }) => {
|
|
8274
|
+
try {
|
|
8275
|
+
const versionId = Number(params?.versionId);
|
|
8276
|
+
if (!Number.isFinite(versionId) || versionId <= 0) {
|
|
8277
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "versionId required" });
|
|
8278
|
+
return;
|
|
8279
|
+
}
|
|
8280
|
+
const userNote = typeof params?.userNote === "string" ? params.userNote.trim().slice(0, 500) : null;
|
|
8281
|
+
const db = getOrCreateHistoryDb(state2);
|
|
8282
|
+
const row = db.prepare(`SELECT id, agentId, filename, content, createdAt FROM bootstrap_versions WHERE id = ?`).get(versionId);
|
|
8283
|
+
if (!row) {
|
|
8284
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `version ${versionId} not found` });
|
|
8285
|
+
return;
|
|
8286
|
+
}
|
|
8287
|
+
if (!ALLOWED_FILENAMES.has(row.filename)) {
|
|
8288
|
+
respond(false, undefined, { code: "FORBIDDEN", message: `cannot restore ${row.filename}` });
|
|
8289
|
+
return;
|
|
8290
|
+
}
|
|
8291
|
+
const currentSnap = db.prepare(`SELECT id, createdAt FROM bootstrap_versions WHERE agentId = ? AND filename = ?
|
|
8292
|
+
ORDER BY createdAt DESC LIMIT 1`).get(row.agentId, row.filename);
|
|
8293
|
+
const filePath = workspaceFilePath(row.agentId, row.filename);
|
|
8294
|
+
fsSync5.writeFileSync(filePath, row.content, "utf-8");
|
|
8295
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
8296
|
+
const currentMetrics = computeEnhancedMetrics(db, row.agentId, sevenDaysAgo);
|
|
8297
|
+
const payload = {
|
|
8298
|
+
issue: "user_reverted",
|
|
8299
|
+
filename: row.filename,
|
|
8300
|
+
restoredVersionId: row.id,
|
|
8301
|
+
revertedFromVersionId: currentSnap?.id ?? null,
|
|
8302
|
+
currentMetrics: currentMetrics ?? null,
|
|
8303
|
+
userNote
|
|
8304
|
+
};
|
|
8305
|
+
recordActivity(state2, "quality_warning", null, row.agentId, { data: JSON.stringify(payload) });
|
|
8306
|
+
const noteLine = userNote ? ` User note: "${userNote}".` : "";
|
|
8307
|
+
const message = `[system] User reverted ${row.filename} to a prior snapshot. ` + `The user judged the version that was live worse than the version they restored.${noteLine} ` + `Read the current ${row.filename} (now the older content) and the diff against your last write to understand what the user preferred. ` + `Treat this as authoritative — do not re-rewrite the same change. Update your understanding before any future refinement.`;
|
|
8308
|
+
sendToInternalSession(state2, row.agentId, message, `user-reverted-${row.id}-${Date.now()}`);
|
|
8309
|
+
console.log("[agentlife:evolutions] restored %s/%s to version %d (userNote=%s)", row.agentId, row.filename, row.id, userNote ? "yes" : "no");
|
|
8310
|
+
respond(true, {
|
|
8311
|
+
restoredVersionId: row.id,
|
|
8312
|
+
filename: row.filename,
|
|
8313
|
+
agentId: row.agentId,
|
|
8314
|
+
revertedFromVersionId: currentSnap?.id ?? null
|
|
8315
|
+
});
|
|
8316
|
+
} catch (err) {
|
|
8317
|
+
respond(false, undefined, { code: "EVOLUTIONS_RESTORE_FAILED", message: err?.message ?? "restore failed" });
|
|
8318
|
+
}
|
|
8319
|
+
}, { scope: "operator.write" });
|
|
8320
|
+
api.registerGatewayMethod("agentlife.evolutions.note", ({ params, respond }) => {
|
|
8321
|
+
try {
|
|
8322
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
|
8323
|
+
if (!agentId) {
|
|
8324
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "agentId required" });
|
|
8325
|
+
return;
|
|
8326
|
+
}
|
|
8327
|
+
const filename = typeof params?.filename === "string" ? params.filename.trim() : "";
|
|
8328
|
+
if (!filename || !ALLOWED_FILENAMES.has(filename)) {
|
|
8329
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "filename must be one of " + [...ALLOWED_FILENAMES].join(", ") });
|
|
8330
|
+
return;
|
|
8331
|
+
}
|
|
8332
|
+
const note = typeof params?.note === "string" ? params.note.trim().slice(0, 280) : "";
|
|
8333
|
+
if (!note) {
|
|
8334
|
+
respond(false, undefined, { code: "BAD_PARAMS", message: "note required" });
|
|
8335
|
+
return;
|
|
8336
|
+
}
|
|
8337
|
+
const db = getOrCreateHistoryDb(state2);
|
|
8338
|
+
const row = db.prepare(`SELECT id FROM bootstrap_versions WHERE agentId = ? AND filename = ?
|
|
8339
|
+
ORDER BY createdAt DESC LIMIT 1`).get(agentId, filename);
|
|
8340
|
+
if (!row) {
|
|
8341
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `no snapshot for ${agentId}/${filename}` });
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
db.prepare(`UPDATE bootstrap_versions SET note = ? WHERE id = ?`).run(note, row.id);
|
|
8345
|
+
respond(true, { versionId: row.id, note });
|
|
8346
|
+
} catch (err) {
|
|
8347
|
+
respond(false, undefined, { code: "EVOLUTIONS_NOTE_FAILED", message: err?.message ?? "note failed" });
|
|
8348
|
+
}
|
|
8349
|
+
}, { scope: "operator.write" });
|
|
8350
|
+
}
|
|
8351
|
+
|
|
7850
8352
|
// gateway/followups-gateway.ts
|
|
7851
8353
|
function registerFollowupsGateway(api, state2) {
|
|
7852
8354
|
api.registerGatewayMethod("agentlife.followups", ({ params, respond }) => {
|
|
@@ -7884,9 +8386,9 @@ function handleList(state2, params, respond) {
|
|
|
7884
8386
|
}
|
|
7885
8387
|
respond(true, {
|
|
7886
8388
|
followups: rows.map((r) => {
|
|
7887
|
-
const
|
|
7888
|
-
const title =
|
|
7889
|
-
const goal =
|
|
8389
|
+
const view2 = getSurface(state2, r.surfaceId);
|
|
8390
|
+
const title = view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null;
|
|
8391
|
+
const goal = view2?.goal ?? null;
|
|
7890
8392
|
return {
|
|
7891
8393
|
id: r.id,
|
|
7892
8394
|
surfaceId: r.surfaceId,
|
|
@@ -7964,13 +8466,14 @@ function handleRun(state2, params, respond) {
|
|
|
7964
8466
|
respond(false, undefined, { code: "RUN_ERROR", message: e?.message });
|
|
7965
8467
|
}
|
|
7966
8468
|
}
|
|
7967
|
-
function fireFollowupViaChat(state2, sessionKey,
|
|
8469
|
+
function fireFollowupViaChat(state2, sessionKey, instruction2, surfaceId) {
|
|
7968
8470
|
if (!state2.runCommand) {
|
|
7969
8471
|
console.warn("[agentlife:followups-gw] runCommand not available — cannot fire followup");
|
|
7970
8472
|
return;
|
|
7971
8473
|
}
|
|
7972
8474
|
const idempotencyKey = `followup-${surfaceId}-${Date.now()}`;
|
|
7973
|
-
const
|
|
8475
|
+
const wrapped = buildFireInstruction(state2, { surfaceId, instruction: instruction2 });
|
|
8476
|
+
const params = JSON.stringify({ sessionKey, message: `[system] ${wrapped}`, idempotencyKey });
|
|
7974
8477
|
state2.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", params], { timeoutMs: 60000 }).then((result) => {
|
|
7975
8478
|
console.log("[agentlife:followups-gw] chat.send result: code=%s", result?.code ?? "?");
|
|
7976
8479
|
}).catch((e) => {
|
|
@@ -7984,8 +8487,8 @@ function handleEdit(state2, params, respond) {
|
|
|
7984
8487
|
return;
|
|
7985
8488
|
}
|
|
7986
8489
|
const delay = typeof params?.delay === "string" ? params.delay.trim() : null;
|
|
7987
|
-
const
|
|
7988
|
-
if (!delay && !
|
|
8490
|
+
const instruction2 = typeof params?.instruction === "string" ? params.instruction.trim() : null;
|
|
8491
|
+
if (!delay && !instruction2) {
|
|
7989
8492
|
respond(false, undefined, { code: "NO_CHANGES", message: "Provide delay and/or instruction" });
|
|
7990
8493
|
return;
|
|
7991
8494
|
}
|
|
@@ -8009,9 +8512,9 @@ function handleEdit(state2, params, respond) {
|
|
|
8009
8512
|
}
|
|
8010
8513
|
newFireAt = Date.now() + offsetMs;
|
|
8011
8514
|
}
|
|
8012
|
-
const newInstruction =
|
|
8515
|
+
const newInstruction = instruction2 ?? row.instruction;
|
|
8013
8516
|
db.prepare("UPDATE followup_queue SET fireAt = ?, instruction = ? WHERE id = ?").run(newFireAt, newInstruction, id);
|
|
8014
|
-
recordSurfaceEvent(state2, row.surfaceId, "followup_edited", undefined, row.agentId ?? undefined, JSON.stringify({ followupId: id, delay, instruction:
|
|
8517
|
+
recordSurfaceEvent(state2, row.surfaceId, "followup_edited", undefined, row.agentId ?? undefined, JSON.stringify({ followupId: id, delay, instruction: instruction2 ? "(updated)" : null }));
|
|
8015
8518
|
console.log("[agentlife:followups-gw] edited followup %d: fireAt=%d", id, newFireAt);
|
|
8016
8519
|
respond(true, {
|
|
8017
8520
|
followup: {
|
|
@@ -8060,10 +8563,10 @@ import {
|
|
|
8060
8563
|
|
|
8061
8564
|
// gateway/config-utils.ts
|
|
8062
8565
|
import * as fs15 from "node:fs";
|
|
8063
|
-
import * as
|
|
8064
|
-
import * as
|
|
8566
|
+
import * as os10 from "node:os";
|
|
8567
|
+
import * as path15 from "node:path";
|
|
8065
8568
|
function configPath() {
|
|
8066
|
-
return
|
|
8569
|
+
return path15.join(os10.homedir(), ".openclaw", "openclaw.json");
|
|
8067
8570
|
}
|
|
8068
8571
|
function readConfig() {
|
|
8069
8572
|
try {
|
|
@@ -8079,10 +8582,10 @@ function writeConfig(cfg) {
|
|
|
8079
8582
|
|
|
8080
8583
|
// gateway/providers.ts
|
|
8081
8584
|
import * as fs16 from "node:fs";
|
|
8082
|
-
import * as
|
|
8083
|
-
import * as
|
|
8585
|
+
import * as os11 from "node:os";
|
|
8586
|
+
import * as path16 from "node:path";
|
|
8084
8587
|
function siblingAgentDirs() {
|
|
8085
|
-
const root =
|
|
8588
|
+
const root = path16.join(os11.homedir(), ".openclaw", "agents");
|
|
8086
8589
|
let entries;
|
|
8087
8590
|
try {
|
|
8088
8591
|
entries = fs16.readdirSync(root, { withFileTypes: true });
|
|
@@ -8093,7 +8596,7 @@ function siblingAgentDirs() {
|
|
|
8093
8596
|
for (const entry of entries) {
|
|
8094
8597
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
8095
8598
|
continue;
|
|
8096
|
-
const dir =
|
|
8599
|
+
const dir = path16.join(root, entry.name, "agent");
|
|
8097
8600
|
try {
|
|
8098
8601
|
if (fs16.statSync(dir).isDirectory())
|
|
8099
8602
|
out.push(dir);
|
|
@@ -8166,9 +8669,9 @@ async function ensureModels(run) {
|
|
|
8166
8669
|
function readOAuthExpiries() {
|
|
8167
8670
|
const result = new Map;
|
|
8168
8671
|
const fs17 = __require("node:fs");
|
|
8169
|
-
const
|
|
8170
|
-
const
|
|
8171
|
-
const agentsDir =
|
|
8672
|
+
const path17 = __require("node:path");
|
|
8673
|
+
const os12 = __require("node:os");
|
|
8674
|
+
const agentsDir = path17.join(os12.homedir(), ".openclaw", "agents");
|
|
8172
8675
|
let entries;
|
|
8173
8676
|
try {
|
|
8174
8677
|
entries = fs17.readdirSync(agentsDir);
|
|
@@ -8176,7 +8679,7 @@ function readOAuthExpiries() {
|
|
|
8176
8679
|
return result;
|
|
8177
8680
|
}
|
|
8178
8681
|
for (const name of entries) {
|
|
8179
|
-
const filePath =
|
|
8682
|
+
const filePath = path17.join(agentsDir, name, "agent", "auth-profiles.json");
|
|
8180
8683
|
try {
|
|
8181
8684
|
const raw = fs17.readFileSync(filePath, "utf-8");
|
|
8182
8685
|
const data = JSON.parse(raw);
|
|
@@ -8350,7 +8853,7 @@ function registerProvidersGateway(api, state2) {
|
|
|
8350
8853
|
}
|
|
8351
8854
|
writeConfig(cfg);
|
|
8352
8855
|
for (const agentDir of siblingAgentDirs()) {
|
|
8353
|
-
const file =
|
|
8856
|
+
const file = path16.join(agentDir, "auth-profiles.json");
|
|
8354
8857
|
try {
|
|
8355
8858
|
const raw = fs16.readFileSync(file, "utf-8");
|
|
8356
8859
|
const data = JSON.parse(raw);
|
|
@@ -8460,10 +8963,10 @@ ${result?.stderr ?? ""}`;
|
|
|
8460
8963
|
|
|
8461
8964
|
// gateway/models-config.ts
|
|
8462
8965
|
import * as fs17 from "node:fs";
|
|
8463
|
-
import * as
|
|
8464
|
-
import * as
|
|
8966
|
+
import * as os12 from "node:os";
|
|
8967
|
+
import * as path17 from "node:path";
|
|
8465
8968
|
function pluginConfigPath2() {
|
|
8466
|
-
return
|
|
8969
|
+
return path17.join(os12.homedir(), ".openclaw", "agentlife", "plugin-config.json");
|
|
8467
8970
|
}
|
|
8468
8971
|
function readPluginConfig2() {
|
|
8469
8972
|
try {
|
|
@@ -8473,7 +8976,7 @@ function readPluginConfig2() {
|
|
|
8473
8976
|
}
|
|
8474
8977
|
}
|
|
8475
8978
|
function writePluginConfig2(cfg) {
|
|
8476
|
-
fs17.mkdirSync(
|
|
8979
|
+
fs17.mkdirSync(path17.dirname(pluginConfigPath2()), { recursive: true });
|
|
8477
8980
|
fs17.writeFileSync(pluginConfigPath2(), JSON.stringify(cfg, null, 2) + `
|
|
8478
8981
|
`, "utf-8");
|
|
8479
8982
|
}
|
|
@@ -8620,7 +9123,7 @@ var stopQualityCheckPoller = null;
|
|
|
8620
9123
|
var stopCloudflared = null;
|
|
8621
9124
|
function resolveInternalModel(api) {
|
|
8622
9125
|
try {
|
|
8623
|
-
const pluginCfgPath =
|
|
9126
|
+
const pluginCfgPath = path18.join(homedir15(), ".openclaw", "agentlife", "plugin-config.json");
|
|
8624
9127
|
try {
|
|
8625
9128
|
const raw = __require("node:fs").readFileSync(pluginCfgPath, "utf-8");
|
|
8626
9129
|
const pluginCfg = JSON.parse(raw);
|
|
@@ -8656,7 +9159,7 @@ function register(api) {
|
|
|
8656
9159
|
return;
|
|
8657
9160
|
}
|
|
8658
9161
|
registered = true;
|
|
8659
|
-
const fallbackDir =
|
|
9162
|
+
const fallbackDir = path18.join(homedir15(), ".openclaw", "agentlife");
|
|
8660
9163
|
const state2 = {
|
|
8661
9164
|
fileSurfaceStorage: null,
|
|
8662
9165
|
surfaceIndex: null,
|
|
@@ -8668,9 +9171,9 @@ function register(api) {
|
|
|
8668
9171
|
agentDbs: new Map,
|
|
8669
9172
|
historyDb: null,
|
|
8670
9173
|
agentlifeStateDir: fallbackDir,
|
|
8671
|
-
registryFilePath:
|
|
8672
|
-
dbBaseDir:
|
|
8673
|
-
historyDbPath:
|
|
9174
|
+
registryFilePath: path18.join(fallbackDir, "agent-registry.json"),
|
|
9175
|
+
dbBaseDir: path18.join(fallbackDir, "db"),
|
|
9176
|
+
historyDbPath: path18.join(fallbackDir, "agentlife.db"),
|
|
8674
9177
|
runCommand: api.runtime.system?.runCommandWithTimeout ?? null,
|
|
8675
9178
|
enqueueSystemEvent: null,
|
|
8676
9179
|
requestHeartbeatNow: null,
|
|
@@ -8747,10 +9250,11 @@ function register(api) {
|
|
|
8747
9250
|
registerAutomationsGateway(api, state2);
|
|
8748
9251
|
registerFollowupsGateway(api, state2);
|
|
8749
9252
|
registerAdminGateway(api, state2);
|
|
9253
|
+
registerEvolutionsGateway(api, state2);
|
|
8750
9254
|
registerProvidersGateway(api, state2);
|
|
8751
9255
|
registerModelsConfigGateway(api);
|
|
8752
9256
|
registerWebApp(api);
|
|
8753
|
-
const notifyConfigPath =
|
|
9257
|
+
const notifyConfigPath = path18.join(fallbackDir, "notification-config.json");
|
|
8754
9258
|
api.registerGatewayMethod("agentlife.notifications.register", ({ params, respond }) => {
|
|
8755
9259
|
const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
|
|
8756
9260
|
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
@@ -8758,9 +9262,9 @@ function register(api) {
|
|
|
8758
9262
|
return respond(false, { error: "missing serverUrl or apiKey" });
|
|
8759
9263
|
}
|
|
8760
9264
|
try {
|
|
8761
|
-
const { writeFileSync:
|
|
8762
|
-
mkdirSync8(
|
|
8763
|
-
|
|
9265
|
+
const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync8 } = __require("node:fs");
|
|
9266
|
+
mkdirSync8(path18.dirname(notifyConfigPath), { recursive: true });
|
|
9267
|
+
writeFileSync11(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
|
|
8764
9268
|
} catch {}
|
|
8765
9269
|
respond(true, { registered: true });
|
|
8766
9270
|
}, { scope: "operator.write" });
|