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.
Files changed (2) hide show
  1. package/dist/index.js +1174 -670
  2. 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 homedir14 } from "node:os";
6
- import * as path17 from "node:path";
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:** if the user sends many messages of the same type in one day, would each spawn its own widget? If yes, raise the scope.
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
- Each distinct deliverable gets its own id (\`yt-{videoId}\`, \`bank-{date}\`, \`task-{slug}\`). Check Current Dashboard State: same item update; different item new id. Never reuse a generic id across different tasks.
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 your 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.
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
- - **One-shot.** Each followup fires once. Decide the next step each cycle from fresh data.
965
- - **Specific.** References something from current output — a finding, data point, decision. A followup you could have written before the work is too generic.
966
- - **Delay matches the domain.** When can the next meaningful change for this goal occur?
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.** If the action spawns a new widget, delete the source after creating the new one.
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.** Set at creation, MUST NOT change. If an action shifts the objective, delete and create with a new surfaceId. Platform rejects goal mutations with a QUALITY ERROR.
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. That attribute is the only rule: it triggers the animated border on the dashboard so the user sees "work in progress." Everything else — size, layout, what the face shows, even whether it's a brand-new surface or a reused one is your call. Match the feel of the domain: a data fetch can show a progress bar, a long exec can list steps, a quick lookup can be one line.
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, goal, detail, followup, context). See TOOLS.md for DSL structure.
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
- // push.ts
2346
- async function pushDsl(state, dsl, opts) {
2347
- if (!state.surfaceIndex || !state.followupQueue || !state.transientSurfaces || !state.fileSurfaceStorage) {
2348
- throw new Error("push.ts: plugin state not initialized");
2349
- }
2350
- const blocks = parseDsl(dsl);
2351
- if (blocks.length === 0)
2352
- return [];
2353
- const results = [];
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
- // onboarding.ts
2508
- var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.map((a) => a.id));
2509
- function userAgentIds(runtime) {
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
- // render-widgets.ts
2675
- function renderAllAgentWidgets(state, runtime, log) {
2676
- if (isOnboarding(state, runtime)) {
2677
- log("[render-widgets] onboarding active — rendering welcome widget");
2678
- renderWelcomeWidget(state, log);
2679
- return;
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
- log("[render-widgets] onboarding complete — no plugin-pushed widgets; specialists own their surfaces");
2682
- }
2683
- var WELCOME_SURFACE_ID = "welcome";
2684
- var WELCOME_INPUT_SURFACE_ID = "welcome-input";
2685
- var ONBOARDING_SESSION_KEY = "agent:agentlife-builder:agentlife:direct:onboarding";
2686
- var ONBOARDING_PLAYBOOK = `## Zero-agent onboarding
2687
-
2688
- 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.
2689
-
2690
- Run a 2–4 turn guided interview, then create ONE **generalist** agent.
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 executeSchedule(state, surfaceId, parsed, agentId) {
3345
- if (!state.followupQueue)
3346
- return;
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 instruction = [
3351
- `Widget followup for ${surfaceId}${goalLabel}: ${parsed.message}`,
3352
- `Check your dashboard state for engagement. Update widget state. Advance the work or delete the widget.`
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 view = getSurface(state, row.surfaceId);
3382
- if (!view || view.state === "dismissed") {
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 chatParams = JSON.stringify({ sessionKey, message: `[system] ${row.instruction}`, idempotencyKey });
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 view = getSurface(state, STALE_SUPERVISOR_SURFACE);
4089
+ const view2 = getSurface(state, STALE_SUPERVISOR_SURFACE);
3872
4090
  removeFollowup(state, STALE_SUPERVISOR_SURFACE);
3873
- if (view && !view.isTransient && view.path) {
4091
+ if (view2 && !view2.isTransient && view2.path) {
3874
4092
  try {
3875
- fs6.unlinkSync(view.path);
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 DEBOUNCE_MS2 = PATHOLOGY ? 30 * 1000 : 6 * 60 * 60 * 1000;
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 > DEBOUNCE_MS2) {
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 > DEBOUNCE_MS2 && state.runCommand) {
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 fs8 from "node:fs";
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 view = getSurface(state, surfaceId);
4989
- if (view && !view.isTransient && view.path) {
5125
+ const view2 = getSurface(state, surfaceId);
5126
+ if (view2 && !view2.isTransient && view2.path) {
4990
5127
  try {
4991
- fs8.unlinkSync(view.path);
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 path11 from "node:path";
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 path10 from "node:path";
5242
- import * as fs11 from "node:fs";
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 fs9 from "node:fs";
5383
+ import * as fs8 from "node:fs";
5247
5384
  import * as os3 from "node:os";
5248
- import * as path8 from "node:path";
5385
+ import * as path7 from "node:path";
5249
5386
  var cachedToken = null;
5250
5387
  function pairingAccessPath() {
5251
- return path8.join(os3.homedir(), ".openclaw", "agentlife", "pairing-access.json");
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 (fs9.existsSync(filePath)) {
5259
- const raw = fs9.readFileSync(filePath, "utf-8");
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 = path8.dirname(filePath);
5269
- if (!fs9.existsSync(dir))
5270
- fs9.mkdirSync(dir, { recursive: true, mode: 448 });
5271
- fs9.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
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
- fs9.chmodSync(filePath, 384);
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 fs10 from "node:fs";
5423
+ import * as fs9 from "node:fs";
5287
5424
  import * as os4 from "node:os";
5288
- import * as path9 from "node:path";
5289
- var AGENTLIFE_DIR = path9.join(os4.homedir(), ".openclaw", "agentlife");
5290
- var TUNNEL_FILE = path9.join(AGENTLIFE_DIR, "tunnel.json");
5291
- var BIN_DIR = path9.join(AGENTLIFE_DIR, "bin");
5292
- var PAIR_REQUEST_MARKER = path9.join(AGENTLIFE_DIR, "pair-requested");
5293
- var IDENTITY_DIR = path9.join(os4.homedir(), ".agentlife");
5294
- var DEVICE_FILE = path9.join(IDENTITY_DIR, "device.json");
5295
- var LEGACY_DEVICE_FILE = path9.join(AGENTLIFE_DIR, "device.json");
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 = path9.join(BIN_DIR, CLOUDFLARED_BIN);
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 fs10.existsSync(PAIR_REQUEST_MARKER);
5554
+ return fs9.existsSync(PAIR_REQUEST_MARKER);
5418
5555
  }
5419
5556
  function requestPair() {
5420
5557
  try {
5421
- if (!fs10.existsSync(AGENTLIFE_DIR))
5422
- fs10.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
5423
- fs10.writeFileSync(PAIR_REQUEST_MARKER, String(Date.now()), { mode: 384 });
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
- fs10.unlinkSync(PAIR_REQUEST_MARKER);
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 (!fs10.existsSync(AGENTLIFE_DIR))
5455
- fs10.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
5456
- if (!fs10.existsSync(BIN_DIR))
5457
- fs10.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
5458
- if (!fs10.existsSync(IDENTITY_DIR))
5459
- fs10.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
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 (!fs10.existsSync(filePath))
5599
+ if (!fs9.existsSync(filePath))
5463
5600
  return null;
5464
5601
  try {
5465
- const raw = fs10.readFileSync(filePath, "utf-8");
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 (!fs10.existsSync(IDENTITY_DIR))
5484
- fs10.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
5485
- fs10.writeFileSync(DEVICE_FILE, JSON.stringify(legacy, null, 2), { mode: 384 });
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
- fs10.chmodSync(DEVICE_FILE, 384);
5624
+ fs9.chmodSync(DEVICE_FILE, 384);
5488
5625
  } catch {}
5489
5626
  try {
5490
- fs10.unlinkSync(LEGACY_DEVICE_FILE);
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 (!fs10.existsSync(IDENTITY_DIR))
5503
- fs10.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
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
- fs10.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
5645
+ fs9.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
5509
5646
  try {
5510
- fs10.chmodSync(DEVICE_FILE, 384);
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 (!fs10.existsSync(TUNNEL_FILE))
5654
+ if (!fs9.existsSync(TUNNEL_FILE))
5518
5655
  return null;
5519
5656
  try {
5520
- const raw = fs10.readFileSync(TUNNEL_FILE, "utf-8");
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
- fs10.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
5666
+ fs9.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
5530
5667
  try {
5531
- fs10.chmodSync(TUNNEL_FILE, 384);
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 (fs10.existsSync(CLOUDFLARED_PATH)) {
5721
+ if (fs9.existsSync(CLOUDFLARED_PATH)) {
5585
5722
  try {
5586
- fs10.accessSync(CLOUDFLARED_PATH, fs10.constants.X_OK);
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
- fs10.unlinkSync(release.tempPath);
5741
+ fs9.unlinkSync(release.tempPath);
5605
5742
  } catch {}
5606
5743
  if (!extracted)
5607
5744
  return null;
5608
5745
  } else {
5609
5746
  try {
5610
- fs10.renameSync(release.tempPath, CLOUDFLARED_PATH);
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
- fs10.chmodSync(CLOUDFLARED_PATH, 493);
5754
+ fs9.chmodSync(CLOUDFLARED_PATH, 493);
5618
5755
  } catch {}
5619
- return fs10.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
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: path9.join(BIN_DIR, asset) };
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
- fs10.unlinkSync(dest);
5786
+ fs9.unlinkSync(dest);
5650
5787
  } catch {}
5651
5788
  return false;
5652
5789
  }
5653
- return fs10.existsSync(dest) && fs10.statSync(dest).size > 0;
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 fs10.existsSync(CLOUDFLARED_PATH);
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 = path10.join(os5.homedir(), ".openclaw", "devices");
5719
- const bootstrapPath = path10.join(devicesDir, "bootstrap.json");
5720
- if (!fs11.existsSync(devicesDir))
5721
- fs11.mkdirSync(devicesDir, { recursive: true });
5722
- const registry = fs11.existsSync(bootstrapPath) ? JSON.parse(fs11.readFileSync(bootstrapPath, "utf-8")) : {};
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
- fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
5886
+ fs10.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
5732
5887
  return bootstrapToken;
5733
5888
  }
5734
5889
  function registerWebApp(api) {
5735
- const pluginRoot = path10.resolve(path10.dirname(api.source), "..");
5736
- const appRoot = path10.join(pluginRoot, "web-build");
5737
- const hasWebBuild = fs11.existsSync(appRoot);
5738
- const indexPath = hasWebBuild ? path10.join(appRoot, "index.html") : null;
5739
- const hasIndex = !!(indexPath && fs11.existsSync(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 = path10.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
5748
- const raw = JSON.parse(fs11.readFileSync(configPath, "utf-8"));
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 = path10.resolve(appRoot, relative);
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 = fs11.existsSync(filePath) && fs11.statSync(filePath).isFile() ? filePath : indexPath;
5873
- const ext = path10.extname(target).toLowerCase();
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 = fs11.readFileSync(target);
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 = path11.join(os6.homedir(), ".openclaw", "devices");
5965
- const pendingPath = path11.join(devicesDir, "pending.json");
5966
- const pairedPath = path11.join(devicesDir, "paired.json");
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 newSurfaceIds = [];
6593
+ const deletedSurfaceInfo = new Map;
6334
6594
  for (const raw of finalBlocks) {
6335
- const match = raw.match(/^surface\s+(\S+)/m);
6336
- if (!match)
6595
+ const deleteMatch = raw.match(/^delete\s+(\S+)/m);
6596
+ if (!deleteMatch)
6337
6597
  continue;
6338
- if (!hasSurface(state2, match[1]))
6339
- newSurfaceIds.push(match[1]);
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 sid of newSurfaceIds) {
6349
- const view = getSurface(state2, sid);
6350
- if (!view)
6351
- continue;
6352
- if (view.isOverlay || view.isInput || isLoadingPush(view.rawDsl))
6353
- continue;
6354
- const notifTitle = capitalize(agentId);
6355
- const notifBody = extractWidgetText(view.lines) ?? "New update";
6356
- notifyWidgetEvent(state2, "surface_created", sid, notifTitle, notifBody);
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 view = getSurface(state2, result.surfaceId);
6362
- if (view?.context) {
6363
- autoRegisterInfraFromContext(state2, result.surfaceId, agentId, view.context);
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 view = getSurface(state2, r.surfaceId);
6385
- const isLoading = view ? isLoadingPush(view.rawDsl) : false;
6386
- if (view && !isLoading && !view.followup && !goalChanges.includes(r.surfaceId)) {
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 (view && !isLoading && !view.isInput && !hasDetailContent(view.rawDsl) && !goalChanges.includes(r.surfaceId) && !missingFollowup.includes(r.surfaceId)) {
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 view = getSurface(state2, surfaceId);
7224
- if (!view) {
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 = view.agentId;
7548
+ const agentId2 = view2.agentId;
7232
7549
  if (agentId2 && state2.runCommand) {
7233
- const goalSnippet = view.goal ? ` goal="${view.goal}"` : "";
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 = view.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 (!view.isTransient && view.path) {
7581
+ if (!view2.isTransient && view2.path) {
7265
7582
  try {
7266
- fs13.unlinkSync(view.path);
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 view = getSurface(state2, surfaceId);
7340
- const agentId = view?.agentId ?? null;
7341
- const ctx = view?.context;
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 view = getSurface(state2, r.surfaceId);
7888
- const title = view ? extractTitleAndDetail({ lines: view.lines }).title : null;
7889
- const goal = view?.goal ?? null;
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, instruction, surfaceId) {
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 params = JSON.stringify({ sessionKey, message: `[system] ${instruction}`, idempotencyKey });
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 instruction = typeof params?.instruction === "string" ? params.instruction.trim() : null;
7988
- if (!delay && !instruction) {
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 = instruction ?? row.instruction;
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: instruction ? "(updated)" : null }));
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 os9 from "node:os";
8064
- import * as path14 from "node:path";
8566
+ import * as os10 from "node:os";
8567
+ import * as path15 from "node:path";
8065
8568
  function configPath() {
8066
- return path14.join(os9.homedir(), ".openclaw", "openclaw.json");
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 os10 from "node:os";
8083
- import * as path15 from "node:path";
8585
+ import * as os11 from "node:os";
8586
+ import * as path16 from "node:path";
8084
8587
  function siblingAgentDirs() {
8085
- const root = path15.join(os10.homedir(), ".openclaw", "agents");
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 = path15.join(root, entry.name, "agent");
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 path16 = __require("node:path");
8170
- const os11 = __require("node:os");
8171
- const agentsDir = path16.join(os11.homedir(), ".openclaw", "agents");
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 = path16.join(agentsDir, name, "agent", "auth-profiles.json");
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 = path15.join(agentDir, "auth-profiles.json");
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 os11 from "node:os";
8464
- import * as path16 from "node:path";
8966
+ import * as os12 from "node:os";
8967
+ import * as path17 from "node:path";
8465
8968
  function pluginConfigPath2() {
8466
- return path16.join(os11.homedir(), ".openclaw", "agentlife", "plugin-config.json");
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(path16.dirname(pluginConfigPath2()), { recursive: true });
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 = path17.join(homedir14(), ".openclaw", "agentlife", "plugin-config.json");
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 = path17.join(homedir14(), ".openclaw", "agentlife");
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: path17.join(fallbackDir, "agent-registry.json"),
8672
- dbBaseDir: path17.join(fallbackDir, "db"),
8673
- historyDbPath: path17.join(fallbackDir, "agentlife.db"),
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 = path17.join(fallbackDir, "notification-config.json");
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: writeFileSync10, mkdirSync: mkdirSync8 } = __require("node:fs");
8762
- mkdirSync8(path17.dirname(notifyConfigPath), { recursive: true });
8763
- writeFileSync10(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
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" });