agentlife 2.6.22 → 2.6.24

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 +886 -693
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -942,11 +942,13 @@ ${SIGNAL_PROTOCOL}
942
942
 
943
943
  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
944
 
945
+ **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.
946
+
945
947
  **Goal ≠ action.** If the action you performed already fulfills it, it's a completed task, not a goal — don't push a widget.
946
948
 
947
- **Goal scope — three tests:**
949
+ **Goal scope — three tests, run on every push:**
948
950
  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.
951
+ 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
952
  3. **Followup:** can it drive a meaningful followup cycle? If "no, work is done," find the higher-level objective.
951
953
 
952
954
  **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 +957,30 @@ A widget is a **goal** — a persistent objective that lives on the dashboard un
955
957
 
956
958
  ### surfaceId
957
959
 
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.
960
+ 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.
961
+
962
+ Before minting a new surfaceId, scan Current Dashboard State:
963
+ - If a live widget's goal subsumes what you're about to push, update that surfaceId.
964
+ - If you need to ask the user something mid-goal, push a transient \`input\` surface (input bar, never the dashboard).
965
+ - 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.
966
+
967
+ 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.
968
+
969
+ Goal complete: delete the surface. The deletion plus dismiss notification IS the closure — do not push a completion / acknowledgment widget.
959
970
 
960
971
  ### followup — the next step
961
972
 
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.
973
+ \`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.
963
974
 
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.
975
+ The followup is a hypothesis derived from THIS cycle's findings a check whose outcome would change your next move. Every followup must:
976
+ - 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).
977
+ - State the threshold or condition that, if observed, would change your next action.
978
+ - Imply what you'll do under EACH outcome never use a generic data operation (query / recompute / update) as a branch.
979
+ - Use a delay that matches when the answer can change. If nothing material can change before then, the delay is wrong.
980
+
981
+ 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.
982
+
983
+ **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
984
 
970
985
  ### Guided mode — input surfaces
971
986
 
@@ -986,9 +1001,11 @@ Button shapes and dispatch format for input surfaces are in **Signal Protocol**
986
1001
 
987
1002
  **Goal met → delete immediately.** \`delete <surfaceId>\`. No cleanup followups, no passive expiry.
988
1003
 
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.
1004
+ **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
1005
 
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.
1006
+ **One widget, one goal symmetric rule.**
1007
+ - Same surfaceId → goal text MUST NOT change (platform rejects with QUALITY ERROR \`goal_changed\`).
1008
+ - 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
1009
 
993
1010
  **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
1011
 
@@ -1124,11 +1141,16 @@ followup: +<duration> "<next concrete step — what to check, what data to query
1124
1141
  context: <JSON — lookup keys: DB tables, date ranges, phase, config>
1125
1142
  \`\`\`
1126
1143
 
1144
+ \`goal:\` is **mandatory on every widget push** — the platform rejects pushes
1145
+ without one ("widget '<id>' missing 'goal:'"). The only exemption is a
1146
+ re-push that omits \`goal:\` while the existing widget already has one in
1147
+ the index — the previous goal is preserved (data-model refresh pattern).
1148
+
1127
1149
  ### Two-Phase Push — Loading Then Final
1128
1150
 
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.
1151
+ 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
1152
 
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.
1153
+ 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
1154
 
1133
1155
  A loading push without a final push = perpetual spinner.
1134
1156
 
@@ -1935,11 +1957,6 @@ function matchMetadata(block, key) {
1935
1957
  }
1936
1958
  return v || null;
1937
1959
  }
1938
- function isLoadingPush(rawDsl) {
1939
- const header = rawDsl.split(`
1940
- `, 1)[0] ?? "";
1941
- return /\bstate=loading\b/.test(header);
1942
- }
1943
1960
  function hasDetailContent(rawDsl) {
1944
1961
  const lines = rawDsl.split(`
1945
1962
  `);
@@ -2341,183 +2358,21 @@ function broadcastInput(message, sessionKey) {
2341
2358
  return;
2342
2359
  broadcastRef("plugin.agentlife.input", { message, sessionKey, timestamp: Date.now() });
2343
2360
  }
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
- };
2361
+ function broadcastNotification(event, surfaceId, title, body) {
2362
+ if (!broadcastRef)
2363
+ return;
2364
+ broadcastRef("plugin.agentlife.notification", {
2365
+ event,
2366
+ surfaceId,
2367
+ title,
2368
+ body,
2369
+ timestamp: Date.now()
2370
+ });
2505
2371
  }
2506
2372
 
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
- }
2373
+ // dashboard-state.ts
2374
+ import { readFileSync as readFileSync3 } from "node:fs";
2375
+ import { homedir as homedir3 } from "node:os";
2521
2376
 
2522
2377
  // dsl/expiry.ts
2523
2378
  var DEFAULT_CEILING_MS = 7 * 24 * 60 * 60 * 1000;
@@ -2671,263 +2526,58 @@ function indexedToView(rawDsl, e) {
2671
2526
  };
2672
2527
  }
2673
2528
 
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;
2529
+ // dashboard-state.ts
2530
+ function extractTitleAndDetail(meta) {
2531
+ let title = null;
2532
+ let detail = null;
2533
+ const fullText = meta.lines.join(`
2534
+ `);
2535
+ const titleMatch = fullText.match(/text\s+"([^"]+)"\s+h[34]/);
2536
+ if (titleMatch)
2537
+ title = titleMatch[1];
2538
+ if (!title) {
2539
+ const bodyMatch = fullText.match(/text\s+"([^"]+)"/);
2540
+ if (bodyMatch)
2541
+ title = bodyMatch[1];
2680
2542
  }
2681
- log("[render-widgets] onboarding complete — no plugin-pushed widgets; specialists own their surfaces");
2543
+ const inlineDetail = fullText.match(/^\s*detail:\s*(.+)/m);
2544
+ if (inlineDetail && inlineDetail[1].trim().length > 0) {
2545
+ detail = inlineDetail[1].trim().replace(/^"|"$/g, "");
2546
+ } else {
2547
+ const detailBlockMatch = fullText.match(/^\s*detail:\s*\n((?:\s+.+\n?)+)/m);
2548
+ if (detailBlockMatch) {
2549
+ detail = detailBlockMatch[1].replace(/^ {2,4}/gm, "").trim();
2550
+ }
2551
+ }
2552
+ return { title, detail };
2682
2553
  }
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 };
2903
- }
2904
- function buildDashboardStateContext(state, agentId) {
2905
- if (!state.surfaceIndex)
2906
- return null;
2907
- const now = Date.now();
2908
- const engagementMap = new Map;
2909
- try {
2910
- const db = getOrCreateHistoryDb(state);
2911
- const rows = db.prepare(`
2912
- SELECT surfaceId,
2913
- SUM(CASE WHEN event = 'detail_viewed' THEN 1 ELSE 0 END) as viewCount,
2914
- MAX(CASE WHEN event = 'detail_viewed' THEN createdAt ELSE 0 END) as lastViewedAt
2915
- FROM surface_events
2916
- WHERE event IN ('detail_viewed', 'action_clicked')
2917
- GROUP BY surfaceId
2918
- `).all();
2919
- for (const row of rows) {
2920
- engagementMap.set(row.surfaceId, { viewed: row.viewCount, lastViewedAt: row.lastViewedAt });
2921
- }
2922
- } catch {}
2923
- const liveSurfaces = listSurfaces(state, { includeExpired: true });
2924
- const hasActiveSurfaces = liveSurfaces.some((v) => !isExpired({
2925
- updatedAt: v.updatedAt,
2926
- followup: v.followup,
2927
- expiredSince: v.expiredSince
2928
- }, now));
2929
- if (liveSurfaces.length === 0 || !hasActiveSurfaces) {
2930
- let result2 = `## Dashboard is Empty
2554
+ function buildDashboardStateContext(state, agentId) {
2555
+ if (!state.surfaceIndex)
2556
+ return null;
2557
+ const now = Date.now();
2558
+ const engagementMap = new Map;
2559
+ try {
2560
+ const db = getOrCreateHistoryDb(state);
2561
+ const rows = db.prepare(`
2562
+ SELECT surfaceId,
2563
+ SUM(CASE WHEN event = 'detail_viewed' THEN 1 ELSE 0 END) as viewCount,
2564
+ MAX(CASE WHEN event = 'detail_viewed' THEN createdAt ELSE 0 END) as lastViewedAt
2565
+ FROM surface_events
2566
+ WHERE event IN ('detail_viewed', 'action_clicked')
2567
+ GROUP BY surfaceId
2568
+ `).all();
2569
+ for (const row of rows) {
2570
+ engagementMap.set(row.surfaceId, { viewed: row.viewCount, lastViewedAt: row.lastViewedAt });
2571
+ }
2572
+ } catch {}
2573
+ const liveSurfaces = listSurfaces(state, { includeExpired: true });
2574
+ const hasActiveSurfaces = liveSurfaces.some((v) => !isExpired({
2575
+ updatedAt: v.updatedAt,
2576
+ followup: v.followup,
2577
+ expiredSince: v.expiredSince
2578
+ }, now));
2579
+ if (liveSurfaces.length === 0 || !hasActiveSurfaces) {
2580
+ let result2 = `## Dashboard is Empty
2931
2581
 
2932
2582
  No active widgets. Wait for user input.
2933
2583
  `;
@@ -3234,6 +2884,29 @@ async function sweepExpiredSurfaces(params) {
3234
2884
 
3235
2885
  // followup.ts
3236
2886
  var pendingDebounces = new Map;
2887
+ function lookupRecentFiredFollowups(state, surfaceId, limit = 5) {
2888
+ try {
2889
+ const db = getOrCreateHistoryDb(state);
2890
+ const rows = db.prepare("SELECT instruction FROM followup_queue WHERE surfaceId = ? AND status = 'fired' ORDER BY createdAt DESC LIMIT ?").all(surfaceId, limit);
2891
+ return rows.map((r) => r.instruction);
2892
+ } catch {
2893
+ return [];
2894
+ }
2895
+ }
2896
+ function lookupPreviousFiredFollowup(state, surfaceId) {
2897
+ const recent = lookupRecentFiredFollowups(state, surfaceId, 1);
2898
+ return recent[0] ?? null;
2899
+ }
2900
+ function followupResidue(message) {
2901
+ return message.toLowerCase().replace(/\d{4}-\d{2}-\d{2}/g, "").replace(/\d+/g, "").replace(/\s+/g, " ").trim();
2902
+ }
2903
+ function isTemplatedFollowup(prev, next) {
2904
+ const a = followupResidue(prev);
2905
+ const b = followupResidue(next);
2906
+ if (!a || !b)
2907
+ return false;
2908
+ return a === b;
2909
+ }
3237
2910
  var pollerInterval = null;
3238
2911
  var startupPollTimeout = null;
3239
2912
  var sweepCounter = 0;
@@ -3273,6 +2946,19 @@ function scheduleFollowup(state, surfaceId, followupRaw, agentId) {
3273
2946
  console.warn("[agentlife:followup] parse failed for %s: %s", surfaceId, followupRaw);
3274
2947
  return;
3275
2948
  }
2949
+ const previous = lookupPreviousFiredFollowup(state, surfaceId);
2950
+ if (previous && isTemplatedFollowup(previous, parsed.message)) {
2951
+ const owner = agentId ?? getSurface(state, surfaceId)?.agentId ?? null;
2952
+ recordActivity(state, "quality_warning", null, owner, {
2953
+ data: JSON.stringify({
2954
+ issue: "followup_templated",
2955
+ surfaceId,
2956
+ previous,
2957
+ next: parsed.message
2958
+ })
2959
+ });
2960
+ console.warn("[agentlife:followup] templated followup detected for %s — same residue as previous fire", surfaceId);
2961
+ }
3276
2962
  const existing = pendingDebounces.get(surfaceId);
3277
2963
  if (existing)
3278
2964
  clearTimeout(existing);
@@ -3341,17 +3027,25 @@ function rescheduleFailedFollowup(state, row, error) {
3341
3027
  console.error("[agentlife:followup] reschedule failed for %s: %s", row.surfaceId, e?.message);
3342
3028
  }
3343
3029
  }
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);
3030
+ function buildFireInstruction(state, row) {
3031
+ const view2 = getSurface(state, row.surfaceId);
3032
+ const goalText = view2?.goal ?? (view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null);
3349
3033
  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.`
3034
+ const recentFires = lookupRecentFiredFollowups(state, row.surfaceId, 5);
3035
+ const prior = recentFires.filter((m) => m !== row.instruction);
3036
+ const priorBlock = prior.length > 0 ? `
3037
+ Prior followups on this surface — your next must not match any of these with parameters rotated:
3038
+ ${prior.slice(0, 3).map((m) => ` • "${m}"`).join(`
3039
+ `)}` : "";
3040
+ return [
3041
+ `Widget followup for ${row.surfaceId}${goalLabel}: ${row.instruction}`,
3042
+ `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
3043
  ].join(`
3354
3044
  `);
3045
+ }
3046
+ function executeSchedule(state, surfaceId, parsed, agentId) {
3047
+ if (!state.followupQueue)
3048
+ return;
3355
3049
  const fireAt = Date.now() + parsed.timeOffsetMs;
3356
3050
  const owner = agentId ?? view?.agentId ?? null;
3357
3051
  if (!owner) {
@@ -3378,8 +3072,8 @@ function pollFollowups(state) {
3378
3072
  try {
3379
3073
  const due = state.followupQueue.drainDue(Date.now());
3380
3074
  for (const row of due) {
3381
- const view = getSurface(state, row.surfaceId);
3382
- if (!view || view.state === "dismissed") {
3075
+ const view2 = getSurface(state, row.surfaceId);
3076
+ if (!view2 || view2.state === "dismissed") {
3383
3077
  console.log("[agentlife:followup] skipped %s — surface gone or dismissed", row.surfaceId);
3384
3078
  continue;
3385
3079
  }
@@ -3388,35 +3082,481 @@ function pollFollowups(state) {
3388
3082
  console.warn("[agentlife:followup] skipped %s — no agentId", row.surfaceId);
3389
3083
  continue;
3390
3084
  }
3391
- const sessionKey = buildAgentSessionKey(agentId);
3392
- const idempotencyKey = `followup-${row.surfaceId}-${Date.now()}`;
3393
- const chatParams = JSON.stringify({ sessionKey, message: `[system] ${row.instruction}`, idempotencyKey });
3394
- state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 60000 }).then((result) => {
3395
- console.log("[agentlife:followup] chat.send for %s: code=%s", row.surfaceId, result?.code ?? "?");
3396
- }).catch((e) => {
3397
- console.error("[agentlife:followup] chat.send failed for %s: %s — rescheduling", row.surfaceId, e?.message);
3398
- rescheduleFailedFollowup(state, { ...row, agentId }, e?.message);
3085
+ const sessionKey = buildAgentSessionKey(agentId);
3086
+ const idempotencyKey = `followup-${row.surfaceId}-${Date.now()}`;
3087
+ const wrapped = buildFireInstruction(state, row);
3088
+ const chatParams = JSON.stringify({ sessionKey, message: `[system] ${wrapped}`, idempotencyKey });
3089
+ state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 60000 }).then((result) => {
3090
+ console.log("[agentlife:followup] chat.send for %s: code=%s", row.surfaceId, result?.code ?? "?");
3091
+ }).catch((e) => {
3092
+ console.error("[agentlife:followup] chat.send failed for %s: %s — rescheduling", row.surfaceId, e?.message);
3093
+ rescheduleFailedFollowup(state, { ...row, agentId }, e?.message);
3094
+ });
3095
+ recordSurfaceEvent(state, row.surfaceId, "cron_fired", undefined, agentId);
3096
+ console.log("[agentlife:followup] fired %s → %s", row.surfaceId, sessionKey);
3097
+ }
3098
+ } catch (e) {
3099
+ console.warn("[agentlife:followup] poll error: %s", e?.message);
3100
+ }
3101
+ sweepCounter++;
3102
+ if (sweepCounter % 5 === 0 && state.surfaceIndex && state.followupQueue && state.fileSurfaceStorage) {
3103
+ sweepExpiredSurfaces({
3104
+ index: state.surfaceIndex,
3105
+ queue: state.followupQueue,
3106
+ storage: state.fileSurfaceStorage
3107
+ }).then((r) => {
3108
+ for (const sid of r.purged)
3109
+ broadcastDelete(sid);
3110
+ if (r.purged.length > 0) {
3111
+ console.log("[agentlife:sweep] purged %d expired surfaces", r.purged.length);
3112
+ }
3113
+ }).catch((e) => console.warn("[agentlife:sweep] error: %s", e?.message));
3114
+ }
3115
+ }
3116
+
3117
+ // push.ts
3118
+ function tokenizeGoal(goal) {
3119
+ return new Set(goal.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= 3));
3120
+ }
3121
+ function goalSimilarity(a, b) {
3122
+ const ta = tokenizeGoal(a);
3123
+ const tb = tokenizeGoal(b);
3124
+ if (ta.size === 0 || tb.size === 0)
3125
+ return 0;
3126
+ let inter = 0;
3127
+ for (const w of ta)
3128
+ if (tb.has(w))
3129
+ inter++;
3130
+ const union = ta.size + tb.size - inter;
3131
+ return union === 0 ? 0 : inter / union;
3132
+ }
3133
+ var SIBLING_SIMILARITY_THRESHOLD = 0.75;
3134
+ var SIBLING_MIN_SHARED_WORDS = 5;
3135
+ function findSiblingSurfaces(state, surfaceId, goal, agentId) {
3136
+ if (!goal || !state.surfaceIndex)
3137
+ return [];
3138
+ const live = state.surfaceIndex.list({ agentId, state: "active" });
3139
+ const sharedFloor = SIBLING_MIN_SHARED_WORDS;
3140
+ const out = [];
3141
+ const myTokens = tokenizeGoal(goal);
3142
+ for (const e of live) {
3143
+ if (e.surfaceId === surfaceId || !e.goal)
3144
+ continue;
3145
+ const sim = goalSimilarity(goal, e.goal);
3146
+ if (sim < SIBLING_SIMILARITY_THRESHOLD)
3147
+ continue;
3148
+ const otherTokens = tokenizeGoal(e.goal);
3149
+ let shared = 0;
3150
+ for (const w of myTokens)
3151
+ if (otherTokens.has(w))
3152
+ shared++;
3153
+ if (shared < sharedFloor)
3154
+ continue;
3155
+ out.push({ surfaceId: e.surfaceId, goal: e.goal, similarity: Math.round(sim * 100) / 100 });
3156
+ }
3157
+ return out;
3158
+ }
3159
+ async function pushDsl(state, dsl, opts) {
3160
+ if (!state.surfaceIndex || !state.followupQueue || !state.transientSurfaces || !state.fileSurfaceStorage) {
3161
+ throw new Error("push.ts: plugin state not initialized");
3162
+ }
3163
+ const blocks = parseDsl(dsl);
3164
+ if (blocks.length === 0)
3165
+ return [];
3166
+ const results = [];
3167
+ const autoDeletedInputIds = [];
3168
+ if (opts.evictStaleInputs !== false) {
3169
+ const incomingInputIds = new Set;
3170
+ for (const b of blocks) {
3171
+ if (b.kind === "push" && b.isInput)
3172
+ incomingInputIds.add(b.surfaceId);
3173
+ }
3174
+ if (incomingInputIds.size > 0) {
3175
+ autoDeletedInputIds.push(...state.transientSurfaces.takeStaleInputs(incomingInputIds));
3176
+ }
3177
+ }
3178
+ for (const block of blocks) {
3179
+ if (block.kind === "delete") {
3180
+ const result2 = await handleDelete(state, block);
3181
+ results.push(result2);
3182
+ continue;
3183
+ }
3184
+ const result = await handlePush(state, block, opts);
3185
+ results.push(result);
3186
+ }
3187
+ broadcastSurface(dsl);
3188
+ for (const sid of autoDeletedInputIds)
3189
+ broadcastDelete(sid);
3190
+ return results;
3191
+ }
3192
+ async function handleDelete(state, block) {
3193
+ const sid = block.surfaceId;
3194
+ const agentId = state.surfaceIndex.get(sid)?.agentId ?? state.transientSurfaces.get(sid)?.agentId ?? null;
3195
+ state.transientSurfaces.delete(sid);
3196
+ const indexed = state.surfaceIndex.get(sid);
3197
+ if (indexed) {
3198
+ try {
3199
+ await state.fileSurfaceStorage.delete(indexed.agentId, sid);
3200
+ } catch (err) {
3201
+ console.warn("[push] file delete failed for %s: %s", sid, err?.message);
3202
+ }
3203
+ state.surfaceIndex.delete(sid);
3204
+ }
3205
+ state.followupQueue.cancel(sid);
3206
+ recordSurfaceEvent(state, sid, "deleted", undefined, agentId ?? undefined);
3207
+ broadcastDelete(sid);
3208
+ return {
3209
+ surfaceId: sid,
3210
+ kind: "delete",
3211
+ isNew: false,
3212
+ isTransient: false,
3213
+ isLoading: false,
3214
+ followupChanged: false,
3215
+ followupRemoved: true,
3216
+ followupTemplated: null,
3217
+ siblingSurfaces: [],
3218
+ goalChanged: null,
3219
+ contextDroppedFields: [],
3220
+ validation: emptyValidation()
3221
+ };
3222
+ }
3223
+ async function handlePush(state, block, opts) {
3224
+ const now = Date.now();
3225
+ const validation = validateBlockDsl(block.rawDsl);
3226
+ if (block.isTransient) {
3227
+ const existing2 = state.transientSurfaces.get(block.surfaceId);
3228
+ state.transientSurfaces.set(opts.agentId, block, opts.sessionKey ?? null);
3229
+ recordSurfaceEvent(state, block.surfaceId, existing2 ? "updated" : "created", block.rawDsl, opts.agentId);
3230
+ return {
3231
+ surfaceId: block.surfaceId,
3232
+ kind: "push",
3233
+ isNew: !existing2,
3234
+ isTransient: true,
3235
+ isLoading: block.declaredState === "loading",
3236
+ followupChanged: false,
3237
+ followupRemoved: false,
3238
+ followupTemplated: null,
3239
+ siblingSurfaces: [],
3240
+ goalChanged: null,
3241
+ contextDroppedFields: [],
3242
+ validation
3243
+ };
3244
+ }
3245
+ const existing = state.surfaceIndex.get(block.surfaceId);
3246
+ if (!block.goal && !existing?.goal) {
3247
+ 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.`);
3248
+ }
3249
+ const filePath = await state.fileSurfaceStorage.write(opts.agentId, block.surfaceId, block.rawDsl);
3250
+ const resolvedContext = block.context ?? existing?.context ?? null;
3251
+ const contextDroppedFields = block.context && existing?.context ? Object.keys(existing.context).filter((k) => !(k in block.context)) : [];
3252
+ const resolvedGoal = block.goal ?? existing?.goal ?? null;
3253
+ const goalChanged = existing?.goal && block.goal && existing.goal !== block.goal ? { from: existing.goal, to: block.goal } : null;
3254
+ const newState = "active";
3255
+ const nextEntry = {
3256
+ surfaceId: block.surfaceId,
3257
+ agentId: existing?.agentId ?? opts.agentId,
3258
+ path: filePath,
3259
+ rawDsl: block.rawDsl,
3260
+ state: newState,
3261
+ isOverlay: false,
3262
+ dashboardVisible: existing?.dashboardVisible ?? true,
3263
+ createdAt: existing?.createdAt ?? now,
3264
+ updatedAt: now,
3265
+ expiredSince: null,
3266
+ originSessionKey: opts.sessionKey ?? existing?.originSessionKey ?? null,
3267
+ goal: resolvedGoal,
3268
+ context: resolvedContext,
3269
+ followup: block.followup,
3270
+ chainRoot: block.chainRoot,
3271
+ chainParent: block.chainParent,
3272
+ chainPosition: block.chainPosition,
3273
+ chainStatus: block.chainStatus,
3274
+ parseError: null
3275
+ };
3276
+ try {
3277
+ state.surfaceIndex.upsert(nextEntry);
3278
+ } catch (err) {
3279
+ console.warn("[push] index upsert failed for %s: %s", block.surfaceId, err?.message);
3280
+ }
3281
+ const oldFollowup = existing?.followup ?? null;
3282
+ const newFollowup = block.followup ?? null;
3283
+ const followupChanged = oldFollowup !== newFollowup;
3284
+ const followupRemoved = !!oldFollowup && !newFollowup;
3285
+ let followupTemplated = null;
3286
+ if (newFollowup) {
3287
+ const newSpec = parseFollowupSpec(newFollowup);
3288
+ if (newSpec) {
3289
+ const recent = lookupRecentFiredFollowups(state, block.surfaceId, 5);
3290
+ const match = recent.find((r) => isTemplatedFollowup(r, newSpec.message));
3291
+ if (match) {
3292
+ followupTemplated = { previous: match, next: newSpec.message };
3293
+ }
3294
+ }
3295
+ }
3296
+ try {
3297
+ if (followupRemoved || !newFollowup) {
3298
+ state.followupQueue.cancel(block.surfaceId);
3299
+ } else {
3300
+ const spec = parseFollowupSpec(newFollowup);
3301
+ if (spec) {
3302
+ state.followupQueue.upsert({
3303
+ surfaceId: block.surfaceId,
3304
+ agentId: nextEntry.agentId,
3305
+ fireAt: now + spec.timeOffsetMs,
3306
+ instruction: spec.message,
3307
+ createdAt: now
3308
+ });
3309
+ } else {
3310
+ console.warn("[push] unparseable followup on %s: %s", block.surfaceId, newFollowup);
3311
+ }
3312
+ }
3313
+ } catch (err) {
3314
+ console.warn("[push] followup queue update failed for %s: %s", block.surfaceId, err?.message);
3315
+ }
3316
+ recordSurfaceEvent(state, block.surfaceId, existing ? "updated" : "created", block.rawDsl, nextEntry.agentId);
3317
+ const siblingSurfaces = findSiblingSurfaces(state, block.surfaceId, nextEntry.goal ?? null, nextEntry.agentId);
3318
+ return {
3319
+ surfaceId: block.surfaceId,
3320
+ kind: "push",
3321
+ isNew: !existing,
3322
+ isTransient: false,
3323
+ isLoading: block.declaredState === "loading",
3324
+ followupChanged,
3325
+ followupRemoved,
3326
+ followupTemplated,
3327
+ siblingSurfaces,
3328
+ goalChanged,
3329
+ contextDroppedFields,
3330
+ validation
3331
+ };
3332
+ }
3333
+ function emptyValidation() {
3334
+ return {
3335
+ unknownKeywords: [],
3336
+ metadataWithoutColon: [],
3337
+ missingCardStructure: false,
3338
+ invalidInputActions: [],
3339
+ unknownAttrs: [],
3340
+ invalidEnumValues: []
3341
+ };
3342
+ }
3343
+
3344
+ // onboarding.ts
3345
+ var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.map((a) => a.id));
3346
+ function userAgentIds(runtime) {
3347
+ const cfg = runtime.config.loadConfig();
3348
+ const list = cfg?.agents?.list ?? [];
3349
+ return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
3350
+ }
3351
+ function isOnboarding(_state, runtime) {
3352
+ return userAgentIds(runtime).length === 0;
3353
+ }
3354
+ function snapshotOnboarding(state, runtime) {
3355
+ const count = userAgentIds(runtime).length;
3356
+ return { isActive: count === 0, userAgentCount: count };
3357
+ }
3358
+
3359
+ // render-widgets.ts
3360
+ function renderAllAgentWidgets(state, runtime, log) {
3361
+ if (isOnboarding(state, runtime)) {
3362
+ log("[render-widgets] onboarding active — rendering welcome widget");
3363
+ renderWelcomeWidget(state, log);
3364
+ return;
3365
+ }
3366
+ log("[render-widgets] onboarding complete — no plugin-pushed widgets; specialists own their surfaces");
3367
+ }
3368
+ var WELCOME_SURFACE_ID = "welcome";
3369
+ var WELCOME_INPUT_SURFACE_ID = "welcome-input";
3370
+ var ONBOARDING_SESSION_KEY = "agent:agentlife-builder:agentlife:direct:onboarding";
3371
+ var ONBOARDING_PLAYBOOK = `## Zero-agent onboarding
3372
+
3373
+ 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.
3374
+
3375
+ Run a 2–4 turn guided interview, then create ONE **generalist** agent.
3376
+
3377
+ Each turn:
3378
+ 1. Decide: do you have enough signal? Minimum = name/persona hint + 2–3 concrete life areas. Don't drag past 4 turns.
3379
+ 2. Not enough → \`agentlife_push\` the SAME surfaceId \`welcome-input\` with:
3380
+ - h3 question generated from what the user just said
3381
+ - 2–4 multichoice buttons \`action=choice\`, options derived from the user's own words (NEVER hardcoded domain lists)
3382
+ - \`textfield placeholder="Something else…"\` escape hatch
3383
+ - updated \`context: {"phase":"welcome","turn":N+1,"answers":[...]}\`
3384
+ - goal + followup unchanged
3385
+ Respond \`done\`.
3386
+ 3. Enough → create the generalist and hand off the dashboard:
3387
+ - Workspace under \`$HOME/.openclaw/workspace-{id}\` (id = slugified short name or "me")
3388
+ - \`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")
3389
+ - \`SOUL.md\`, \`IDENTITY.md\`, \`USER.md\` (user's own words), \`HEARTBEAT.md\` (empty)
3390
+ - \`exec openclaw gateway call agentlife.createAgent --params '{...}'\` with \`tools: {profile:"full", alsoAllow:["agentlife_push"]}\`. Plugin auto-deletes welcome + welcome-input on success.
3391
+ - **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\`.
3392
+ `;
3393
+ function renderWelcomeWidget(state, log) {
3394
+ const welcomeDsl = [
3395
+ `surface ${WELCOME_SURFACE_ID} size=m`,
3396
+ ` card`,
3397
+ ` column`,
3398
+ ` text "\uD83D\uDC4B Welcome to Agent Life" h3`,
3399
+ ` text "Answer below and I'll build your first agent from it." body`,
3400
+ ` divider`,
3401
+ ` badge "Setup" color=#6366F1 outlined`,
3402
+ `goal: Welcome the user and anchor the onboarding flow`,
3403
+ `followup: +24h "If still zero agents, push an encouraging nudge."`,
3404
+ `context: {"phase":"welcome","autoRendered":true}`
3405
+ ].join(`
3406
+ `);
3407
+ 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));
3408
+ const welcomeInputDsl = [
3409
+ `surface ${WELCOME_INPUT_SURFACE_ID} input`,
3410
+ ` card`,
3411
+ ` column`,
3412
+ ` text "What do you want to track or improve?" h3`,
3413
+ ` text "One line. Anything — meals, money, a project, a habit." body`,
3414
+ ` textfield placeholder="Type anything…"`,
3415
+ `goal: Onboard the user to their first agent via a guided interview`,
3416
+ `followup: +30m "If unanswered, replace the question with a gentler prompt."`,
3417
+ `context: {"phase":"welcome","turn":1,"answers":[]}`
3418
+ ].join(`
3419
+ `);
3420
+ 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));
3421
+ }
3422
+ function deleteWelcomeWidget(state, log) {
3423
+ for (const id of [WELCOME_SURFACE_ID, WELCOME_INPUT_SURFACE_ID]) {
3424
+ if (!hasSurface(state, id))
3425
+ continue;
3426
+ const view2 = getSurface(state, id);
3427
+ if (view2 && !view2.isTransient && view2.path) {
3428
+ try {
3429
+ fs4.unlinkSync(view2.path);
3430
+ } catch {}
3431
+ }
3432
+ state.surfaceIndex?.delete(id);
3433
+ state.transientSurfaces.delete(id);
3434
+ state.followupQueue?.cancel(id);
3435
+ purgeSurfaceHistory(state, id);
3436
+ broadcastDelete(id);
3437
+ log(`[render-widgets] deleted ${id}`);
3438
+ }
3439
+ }
3440
+
3441
+ // services/surfaces-init.ts
3442
+ import * as path5 from "node:path";
3443
+
3444
+ // storage/reconciler.ts
3445
+ var defaultLogger = {
3446
+ log: (...args) => console.log(...args),
3447
+ warn: (...args) => console.warn(...args)
3448
+ };
3449
+ function reconcile(params) {
3450
+ const { storage, index, queue, knownAgents } = params;
3451
+ const log = params.logger ?? defaultLogger;
3452
+ const result = {
3453
+ indexed: 0,
3454
+ parseErrors: 0,
3455
+ orphanFiles: 0,
3456
+ staleIndexRowsDropped: 0,
3457
+ followupsScheduled: 0,
3458
+ staleQueueRowsCancelled: 0
3459
+ };
3460
+ const { surfaces, orphans } = storage.listAll(knownAgents);
3461
+ if (orphans.length > 0) {
3462
+ result.orphanFiles = orphans.length;
3463
+ const byAgent = new Map;
3464
+ for (const o of orphans)
3465
+ byAgent.set(o.agentId, (byAgent.get(o.agentId) ?? 0) + 1);
3466
+ for (const [agentId, count] of byAgent) {
3467
+ log.warn(`[reconcile] orphan workspace: ${agentId} (${count} file(s)) — agent not in agents.list, skipping`);
3468
+ }
3469
+ }
3470
+ const foundSids = new Set;
3471
+ for (const file of surfaces) {
3472
+ let block = null;
3473
+ let parseError = null;
3474
+ try {
3475
+ const parsed = parseBlock(file.dsl);
3476
+ if (!parsed) {
3477
+ parseError = "no surface header";
3478
+ } else if (parsed.kind === "delete") {
3479
+ parseError = "file contains only a delete directive";
3480
+ } else {
3481
+ block = parsed;
3482
+ }
3483
+ } catch (err) {
3484
+ parseError = err?.message ?? String(err);
3485
+ }
3486
+ if (parseError || !block) {
3487
+ index.upsertParseError({
3488
+ surfaceId: file.surfaceId,
3489
+ agentId: file.agentId,
3490
+ path: file.path,
3491
+ rawDsl: file.dsl,
3492
+ error: parseError ?? "unknown parse error"
3399
3493
  });
3400
- recordSurfaceEvent(state, row.surfaceId, "cron_fired", undefined, agentId);
3401
- console.log("[agentlife:followup] fired %s → %s", row.surfaceId, sessionKey);
3494
+ foundSids.add(file.surfaceId);
3495
+ result.parseErrors++;
3496
+ log.warn(`[reconcile] parse error in ${file.surfaceId}: ${parseError}`);
3497
+ continue;
3402
3498
  }
3403
- } catch (e) {
3404
- console.warn("[agentlife:followup] poll error: %s", e?.message);
3405
- }
3406
- sweepCounter++;
3407
- if (sweepCounter % 5 === 0 && state.surfaceIndex && state.followupQueue && state.fileSurfaceStorage) {
3408
- sweepExpiredSurfaces({
3409
- index: state.surfaceIndex,
3410
- queue: state.followupQueue,
3411
- storage: state.fileSurfaceStorage
3412
- }).then((r) => {
3413
- for (const sid of r.purged)
3414
- broadcastDelete(sid);
3415
- if (r.purged.length > 0) {
3416
- console.log("[agentlife:sweep] purged %d expired surfaces", r.purged.length);
3499
+ const now = Date.now();
3500
+ const existing = index.get(file.surfaceId);
3501
+ const entry = {
3502
+ surfaceId: block.surfaceId,
3503
+ agentId: file.agentId,
3504
+ path: file.path,
3505
+ rawDsl: block.rawDsl,
3506
+ state: existing?.state ?? "active",
3507
+ isOverlay: false,
3508
+ dashboardVisible: existing?.dashboardVisible ?? true,
3509
+ createdAt: existing?.createdAt ?? now,
3510
+ updatedAt: existing?.updatedAt ?? now,
3511
+ expiredSince: existing?.expiredSince ?? null,
3512
+ originSessionKey: existing?.originSessionKey ?? null,
3513
+ goal: block.goal,
3514
+ context: block.context,
3515
+ followup: block.followup,
3516
+ chainRoot: block.chainRoot,
3517
+ chainParent: block.chainParent,
3518
+ chainPosition: block.chainPosition,
3519
+ chainStatus: block.chainStatus,
3520
+ parseError: null
3521
+ };
3522
+ index.upsert(entry);
3523
+ foundSids.add(file.surfaceId);
3524
+ result.indexed++;
3525
+ if (block.followup) {
3526
+ const spec = parseFollowupSpec(block.followup);
3527
+ if (spec) {
3528
+ const pending2 = queue.nextPending(file.surfaceId);
3529
+ if (!pending2) {
3530
+ queue.upsert({
3531
+ surfaceId: file.surfaceId,
3532
+ agentId: file.agentId,
3533
+ fireAt: now + spec.timeOffsetMs,
3534
+ instruction: spec.message,
3535
+ createdAt: now
3536
+ });
3537
+ result.followupsScheduled++;
3538
+ }
3539
+ } else {
3540
+ log.warn(`[reconcile] unparseable followup on ${file.surfaceId}: ${block.followup}`);
3417
3541
  }
3418
- }).catch((e) => console.warn("[agentlife:sweep] error: %s", e?.message));
3542
+ }
3543
+ }
3544
+ for (const sid of index.keys()) {
3545
+ if (foundSids.has(sid))
3546
+ continue;
3547
+ index.delete(sid);
3548
+ queue.cancel(sid);
3549
+ result.staleIndexRowsDropped++;
3550
+ }
3551
+ const pending = params.queue.listPending();
3552
+ for (const row of pending) {
3553
+ if (!foundSids.has(row.surfaceId)) {
3554
+ queue.cancel(row.surfaceId);
3555
+ result.staleQueueRowsCancelled++;
3556
+ }
3419
3557
  }
3558
+ log.log(`[reconcile] indexed=${result.indexed} parseErrors=${result.parseErrors} ` + `orphanFiles=${result.orphanFiles} staleIndexRows=${result.staleIndexRowsDropped} ` + `followupsScheduled=${result.followupsScheduled} staleQueueRows=${result.staleQueueRowsCancelled}`);
3559
+ return result;
3420
3560
  }
3421
3561
 
3422
3562
  // cleanup.ts
@@ -3868,11 +4008,11 @@ async function cleanupSupervisorLegacyState(state) {
3868
4008
  const actions = [];
3869
4009
  try {
3870
4010
  if (hasSurface(state, STALE_SUPERVISOR_SURFACE)) {
3871
- const view = getSurface(state, STALE_SUPERVISOR_SURFACE);
4011
+ const view2 = getSurface(state, STALE_SUPERVISOR_SURFACE);
3872
4012
  removeFollowup(state, STALE_SUPERVISOR_SURFACE);
3873
- if (view && !view.isTransient && view.path) {
4013
+ if (view2 && !view2.isTransient && view2.path) {
3874
4014
  try {
3875
- fs6.unlinkSync(view.path);
4015
+ fs6.unlinkSync(view2.path);
3876
4016
  } catch {}
3877
4017
  }
3878
4018
  state.surfaceIndex?.delete(STALE_SUPERVISOR_SURFACE);
@@ -4149,75 +4289,13 @@ function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
4149
4289
  state.usageAccumulator.delete(sessionKey);
4150
4290
  }
4151
4291
 
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
4292
  // hooks/activity-hooks.ts
4215
4293
  var sessionContext = new Map;
4216
4294
  var pendingSnapshots = new Map;
4217
4295
  var agentHealth = new Map;
4218
4296
  var lastGlobalSupervisorRun = 0;
4219
4297
  var PATHOLOGY = process.env.PATHOLOGY_TEST_MODE === "1";
4220
- var DEBOUNCE_MS2 = PATHOLOGY ? 30 * 1000 : 6 * 60 * 60 * 1000;
4298
+ var DEBOUNCE_MS = PATHOLOGY ? 30 * 1000 : 6 * 60 * 60 * 1000;
4221
4299
  var CEILING_MS = PATHOLOGY ? 5 * 60 * 1000 : 72 * 60 * 60 * 1000;
4222
4300
  var RECOVERY_MS = PATHOLOGY ? 5 * 60 * 1000 : 24 * 60 * 60 * 1000;
4223
4301
  function classifySignal(event, metadata) {
@@ -4365,10 +4443,6 @@ function registerActivityHooks(api, state) {
4365
4443
  if (cronMatch) {
4366
4444
  const cronSurfaceId = cronMatch[1];
4367
4445
  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
4446
  if (agentId && state.runCommand) {
4373
4447
  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
4448
  console.log("[agentlife] followup redirected to agent:%s for %s", agentId, cronSurfaceId);
@@ -4634,7 +4708,7 @@ function registerActivityHooks(api, state) {
4634
4708
  h.state = newState;
4635
4709
  h.enteredAt = now;
4636
4710
  console.log("[agentlife:health] %s: %s → %s (%s)", agentId, prev, newState, reason);
4637
- if (newState === "review" && state.runCommand && now - h.lastSupervisorRun > DEBOUNCE_MS2) {
4711
+ if (newState === "review" && state.runCommand && now - h.lastSupervisorRun > DEBOUNCE_MS) {
4638
4712
  const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
4639
4713
  const metrics = computeEnhancedMetrics(db, agentId, sevenDaysAgo);
4640
4714
  const parts = [`Agent ${agentId} entered REVIEW state: ${reason}.`];
@@ -4663,7 +4737,7 @@ ${parts.join(`
4663
4737
  h.lastSupervisorRun = now;
4664
4738
  }
4665
4739
  }
4666
- if (now - lastGlobalSupervisorRun > DEBOUNCE_MS2 && state.runCommand) {
4740
+ if (now - lastGlobalSupervisorRun > DEBOUNCE_MS && state.runCommand) {
4667
4741
  let watchOrReviewCount = 0;
4668
4742
  const degraded = [];
4669
4743
  for (const [aid, ah] of agentHealth.entries()) {
@@ -4843,21 +4917,6 @@ function startQualityCheckPoller(state) {
4843
4917
  }
4844
4918
  };
4845
4919
  }
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
4920
  function persistSessionTrace(state, sessionKey, agentId, durationMs) {
4862
4921
  try {
4863
4922
  const now = Date.now();
@@ -4922,7 +4981,7 @@ function rescueOrphanedSurfaces(state, agentId, error) {
4922
4981
  }
4923
4982
 
4924
4983
  // services/escalation.ts
4925
- import * as fs8 from "node:fs";
4984
+ import * as fs7 from "node:fs";
4926
4985
  var PATHOLOGY2 = process.env.PATHOLOGY_TEST_MODE === "1";
4927
4986
  var ESCALATION_REVIEW_THRESHOLD_MS = PATHOLOGY2 ? 2 * 60 * 1000 : 48 * 60 * 60 * 1000;
4928
4987
  var TREND_WINDOW_DAYS = 7;
@@ -4985,10 +5044,10 @@ function clearEscalationSurface(state, agentId) {
4985
5044
  const surfaceId = `escalation-${agentId}`;
4986
5045
  if (!hasSurface(state, surfaceId))
4987
5046
  return false;
4988
- const view = getSurface(state, surfaceId);
4989
- if (view && !view.isTransient && view.path) {
5047
+ const view2 = getSurface(state, surfaceId);
5048
+ if (view2 && !view2.isTransient && view2.path) {
4990
5049
  try {
4991
- fs8.unlinkSync(view.path);
5050
+ fs7.unlinkSync(view2.path);
4992
5051
  } catch {}
4993
5052
  }
4994
5053
  state.surfaceIndex.delete(surfaceId);
@@ -5233,30 +5292,30 @@ import * as crypto4 from "node:crypto";
5233
5292
  import * as fsSync3 from "node:fs";
5234
5293
  import { createRequire as createRequire3 } from "node:module";
5235
5294
  import * as os6 from "node:os";
5236
- import * as path11 from "node:path";
5295
+ import * as path10 from "node:path";
5237
5296
 
5238
5297
  // gateway/web-app.ts
5239
5298
  import * as crypto3 from "node:crypto";
5240
5299
  import * as os5 from "node:os";
5241
- import * as path10 from "node:path";
5242
- import * as fs11 from "node:fs";
5300
+ import * as path9 from "node:path";
5301
+ import * as fs10 from "node:fs";
5243
5302
 
5244
5303
  // services/pairing-access-token.ts
5245
5304
  import * as crypto from "node:crypto";
5246
- import * as fs9 from "node:fs";
5305
+ import * as fs8 from "node:fs";
5247
5306
  import * as os3 from "node:os";
5248
- import * as path8 from "node:path";
5307
+ import * as path7 from "node:path";
5249
5308
  var cachedToken = null;
5250
5309
  function pairingAccessPath() {
5251
- return path8.join(os3.homedir(), ".openclaw", "agentlife", "pairing-access.json");
5310
+ return path7.join(os3.homedir(), ".openclaw", "agentlife", "pairing-access.json");
5252
5311
  }
5253
5312
  function loadOrCreatePairingAccessToken() {
5254
5313
  if (cachedToken)
5255
5314
  return cachedToken;
5256
5315
  const filePath = pairingAccessPath();
5257
5316
  try {
5258
- if (fs9.existsSync(filePath)) {
5259
- const raw = fs9.readFileSync(filePath, "utf-8");
5317
+ if (fs8.existsSync(filePath)) {
5318
+ const raw = fs8.readFileSync(filePath, "utf-8");
5260
5319
  const obj = JSON.parse(raw);
5261
5320
  const token2 = typeof obj?.token === "string" ? obj.token : null;
5262
5321
  if (token2 && token2.length >= 32) {
@@ -5265,12 +5324,12 @@ function loadOrCreatePairingAccessToken() {
5265
5324
  }
5266
5325
  }
5267
5326
  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 });
5327
+ const dir = path7.dirname(filePath);
5328
+ if (!fs8.existsSync(dir))
5329
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
5330
+ fs8.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
5272
5331
  try {
5273
- fs9.chmodSync(filePath, 384);
5332
+ fs8.chmodSync(filePath, 384);
5274
5333
  } catch {}
5275
5334
  cachedToken = token;
5276
5335
  return token;
@@ -5283,18 +5342,18 @@ function loadOrCreatePairingAccessToken() {
5283
5342
  // services/cloudflared-supervisor.ts
5284
5343
  import { spawn, spawnSync } from "node:child_process";
5285
5344
  import * as crypto2 from "node:crypto";
5286
- import * as fs10 from "node:fs";
5345
+ import * as fs9 from "node:fs";
5287
5346
  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");
5347
+ import * as path8 from "node:path";
5348
+ var AGENTLIFE_DIR = path8.join(os4.homedir(), ".openclaw", "agentlife");
5349
+ var TUNNEL_FILE = path8.join(AGENTLIFE_DIR, "tunnel.json");
5350
+ var BIN_DIR = path8.join(AGENTLIFE_DIR, "bin");
5351
+ var PAIR_REQUEST_MARKER = path8.join(AGENTLIFE_DIR, "pair-requested");
5352
+ var IDENTITY_DIR = path8.join(os4.homedir(), ".agentlife");
5353
+ var DEVICE_FILE = path8.join(IDENTITY_DIR, "device.json");
5354
+ var LEGACY_DEVICE_FILE = path8.join(AGENTLIFE_DIR, "device.json");
5296
5355
  var CLOUDFLARED_BIN = os4.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
5297
- var CLOUDFLARED_PATH = path9.join(BIN_DIR, CLOUDFLARED_BIN);
5356
+ var CLOUDFLARED_PATH = path8.join(BIN_DIR, CLOUDFLARED_BIN);
5298
5357
  var API_BASE = process.env.AGENTLIFE_API_BASE || "https://api.agentlife.app";
5299
5358
  var RESTART_DELAY_MS = 5000;
5300
5359
  var STABLE_RUNTIME_MS = 60000;
@@ -5414,20 +5473,20 @@ async function doBootstrap() {
5414
5473
  return tunnelInfo;
5415
5474
  }
5416
5475
  function isPairRequested() {
5417
- return fs10.existsSync(PAIR_REQUEST_MARKER);
5476
+ return fs9.existsSync(PAIR_REQUEST_MARKER);
5418
5477
  }
5419
5478
  function requestPair() {
5420
5479
  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 });
5480
+ if (!fs9.existsSync(AGENTLIFE_DIR))
5481
+ fs9.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
5482
+ fs9.writeFileSync(PAIR_REQUEST_MARKER, String(Date.now()), { mode: 384 });
5424
5483
  } catch (err) {
5425
5484
  console.warn(`[cloudflared-supervisor] failed to write pair-request marker: ${err?.message ?? err}`);
5426
5485
  }
5427
5486
  }
5428
5487
  function clearPairRequest() {
5429
5488
  try {
5430
- fs10.unlinkSync(PAIR_REQUEST_MARKER);
5489
+ fs9.unlinkSync(PAIR_REQUEST_MARKER);
5431
5490
  } catch {}
5432
5491
  }
5433
5492
  function parseRetryAfter(value) {
@@ -5451,18 +5510,18 @@ function scheduleProvisionRetry(delayMs) {
5451
5510
  }, delayMs);
5452
5511
  }
5453
5512
  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 });
5513
+ if (!fs9.existsSync(AGENTLIFE_DIR))
5514
+ fs9.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
5515
+ if (!fs9.existsSync(BIN_DIR))
5516
+ fs9.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
5517
+ if (!fs9.existsSync(IDENTITY_DIR))
5518
+ fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
5460
5519
  }
5461
5520
  function readIdentityFile(filePath) {
5462
- if (!fs10.existsSync(filePath))
5521
+ if (!fs9.existsSync(filePath))
5463
5522
  return null;
5464
5523
  try {
5465
- const raw = fs10.readFileSync(filePath, "utf-8");
5524
+ const raw = fs9.readFileSync(filePath, "utf-8");
5466
5525
  const parsed = JSON.parse(raw);
5467
5526
  if (typeof parsed.deviceId === "string" && typeof parsed.deviceSecret === "string") {
5468
5527
  return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
@@ -5480,14 +5539,14 @@ function loadDeviceIdentity() {
5480
5539
  if (!legacy)
5481
5540
  return null;
5482
5541
  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 });
5542
+ if (!fs9.existsSync(IDENTITY_DIR))
5543
+ fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
5544
+ fs9.writeFileSync(DEVICE_FILE, JSON.stringify(legacy, null, 2), { mode: 384 });
5486
5545
  try {
5487
- fs10.chmodSync(DEVICE_FILE, 384);
5546
+ fs9.chmodSync(DEVICE_FILE, 384);
5488
5547
  } catch {}
5489
5548
  try {
5490
- fs10.unlinkSync(LEGACY_DEVICE_FILE);
5549
+ fs9.unlinkSync(LEGACY_DEVICE_FILE);
5491
5550
  } catch {}
5492
5551
  console.log(`[cloudflared-supervisor] migrated device.json to ${DEVICE_FILE}`);
5493
5552
  } catch (err) {
@@ -5499,25 +5558,25 @@ function loadOrCreateDeviceIdentity() {
5499
5558
  const existing = loadDeviceIdentity();
5500
5559
  if (existing)
5501
5560
  return existing;
5502
- if (!fs10.existsSync(IDENTITY_DIR))
5503
- fs10.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
5561
+ if (!fs9.existsSync(IDENTITY_DIR))
5562
+ fs9.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
5504
5563
  const identity = {
5505
5564
  deviceId: crypto2.randomUUID(),
5506
5565
  deviceSecret: crypto2.randomBytes(32).toString("base64url")
5507
5566
  };
5508
- fs10.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
5567
+ fs9.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
5509
5568
  try {
5510
- fs10.chmodSync(DEVICE_FILE, 384);
5569
+ fs9.chmodSync(DEVICE_FILE, 384);
5511
5570
  } catch {}
5512
5571
  console.log(`[cloudflared-supervisor] generated new device identity (deviceId=${identity.deviceId})`);
5513
5572
  return identity;
5514
5573
  }
5515
5574
  var AGENTLIFE_API_BASE = API_BASE;
5516
5575
  function loadCachedTunnel() {
5517
- if (!fs10.existsSync(TUNNEL_FILE))
5576
+ if (!fs9.existsSync(TUNNEL_FILE))
5518
5577
  return null;
5519
5578
  try {
5520
- const raw = fs10.readFileSync(TUNNEL_FILE, "utf-8");
5579
+ const raw = fs9.readFileSync(TUNNEL_FILE, "utf-8");
5521
5580
  const parsed = JSON.parse(raw);
5522
5581
  if (typeof parsed.subdomain === "string" && typeof parsed.hostname === "string" && typeof parsed.tunnelUrl === "string" && typeof parsed.tunnelToken === "string" && typeof parsed.provisionedAt === "number") {
5523
5582
  return parsed;
@@ -5526,9 +5585,9 @@ function loadCachedTunnel() {
5526
5585
  return null;
5527
5586
  }
5528
5587
  function persistTunnel(info) {
5529
- fs10.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
5588
+ fs9.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
5530
5589
  try {
5531
- fs10.chmodSync(TUNNEL_FILE, 384);
5590
+ fs9.chmodSync(TUNNEL_FILE, 384);
5532
5591
  } catch {}
5533
5592
  }
5534
5593
  async function provisionTunnel(identity) {
@@ -5581,9 +5640,9 @@ async function provisionTunnel(identity) {
5581
5640
  }
5582
5641
  }
5583
5642
  function ensureCloudflaredBinary() {
5584
- if (fs10.existsSync(CLOUDFLARED_PATH)) {
5643
+ if (fs9.existsSync(CLOUDFLARED_PATH)) {
5585
5644
  try {
5586
- fs10.accessSync(CLOUDFLARED_PATH, fs10.constants.X_OK);
5645
+ fs9.accessSync(CLOUDFLARED_PATH, fs9.constants.X_OK);
5587
5646
  return CLOUDFLARED_PATH;
5588
5647
  } catch {}
5589
5648
  }
@@ -5601,28 +5660,28 @@ function ensureCloudflaredBinary() {
5601
5660
  if (release.kind === "tgz") {
5602
5661
  const extracted = extractTgzCloudflared(release.tempPath, BIN_DIR);
5603
5662
  try {
5604
- fs10.unlinkSync(release.tempPath);
5663
+ fs9.unlinkSync(release.tempPath);
5605
5664
  } catch {}
5606
5665
  if (!extracted)
5607
5666
  return null;
5608
5667
  } else {
5609
5668
  try {
5610
- fs10.renameSync(release.tempPath, CLOUDFLARED_PATH);
5669
+ fs9.renameSync(release.tempPath, CLOUDFLARED_PATH);
5611
5670
  } catch (err) {
5612
5671
  console.warn(`[cloudflared-supervisor] rename failed: ${err?.message}`);
5613
5672
  return null;
5614
5673
  }
5615
5674
  }
5616
5675
  try {
5617
- fs10.chmodSync(CLOUDFLARED_PATH, 493);
5676
+ fs9.chmodSync(CLOUDFLARED_PATH, 493);
5618
5677
  } catch {}
5619
- return fs10.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
5678
+ return fs9.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
5620
5679
  }
5621
5680
  function detectCloudflaredRelease(platform2, arch2) {
5622
5681
  const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
5623
5682
  if (platform2 === "darwin") {
5624
5683
  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) };
5684
+ return { url: `${base}/${asset}`, kind: "tgz", tempPath: path8.join(BIN_DIR, asset) };
5626
5685
  }
5627
5686
  if (platform2 === "linux") {
5628
5687
  const asset = arch2 === "arm64" ? "cloudflared-linux-arm64" : arch2 === "arm" ? "cloudflared-linux-arm" : arch2 === "x64" ? "cloudflared-linux-amd64" : null;
@@ -5646,11 +5705,11 @@ function downloadWithCurl(url, dest) {
5646
5705
  if (result.status !== 0) {
5647
5706
  console.warn(`[cloudflared-supervisor] curl failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
5648
5707
  try {
5649
- fs10.unlinkSync(dest);
5708
+ fs9.unlinkSync(dest);
5650
5709
  } catch {}
5651
5710
  return false;
5652
5711
  }
5653
- return fs10.existsSync(dest) && fs10.statSync(dest).size > 0;
5712
+ return fs9.existsSync(dest) && fs9.statSync(dest).size > 0;
5654
5713
  }
5655
5714
  function extractTgzCloudflared(tgzPath, destDir) {
5656
5715
  const result = spawnSync("tar", ["-xzf", tgzPath, "-C", destDir], { stdio: "pipe" });
@@ -5658,11 +5717,29 @@ function extractTgzCloudflared(tgzPath, destDir) {
5658
5717
  console.warn(`[cloudflared-supervisor] tar extract failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
5659
5718
  return false;
5660
5719
  }
5661
- return fs10.existsSync(CLOUDFLARED_PATH);
5720
+ return fs9.existsSync(CLOUDFLARED_PATH);
5721
+ }
5722
+ function killOrphanedCloudflareds(binPath) {
5723
+ if (os4.platform() === "win32")
5724
+ return;
5725
+ const found = spawnSync("pgrep", ["-f", binPath], { stdio: "pipe" });
5726
+ if (found.status !== 0)
5727
+ return;
5728
+ const pids = (found.stdout?.toString() ?? "").split(`
5729
+ `).map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
5730
+ if (pids.length === 0)
5731
+ return;
5732
+ console.warn(`[cloudflared-supervisor] killing ${pids.length} orphaned cloudflared process(es): ${pids.join(", ")}`);
5733
+ for (const pid of pids) {
5734
+ try {
5735
+ process.kill(pid, "SIGKILL");
5736
+ } catch {}
5737
+ }
5662
5738
  }
5663
5739
  function startCloudflaredProcess(binPath, tunnelToken) {
5664
5740
  if (state.stopped)
5665
5741
  return;
5742
+ killOrphanedCloudflareds(binPath);
5666
5743
  const startedAt = Date.now();
5667
5744
  const child = spawn(binPath, ["tunnel", "--no-autoupdate", "run", "--token", tunnelToken], {
5668
5745
  stdio: ["ignore", "pipe", "pipe"],
@@ -5715,11 +5792,11 @@ var MIME_TYPES = {
5715
5792
  };
5716
5793
  function mintBootstrapToken() {
5717
5794
  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")) : {};
5795
+ const devicesDir = path9.join(os5.homedir(), ".openclaw", "devices");
5796
+ const bootstrapPath = path9.join(devicesDir, "bootstrap.json");
5797
+ if (!fs10.existsSync(devicesDir))
5798
+ fs10.mkdirSync(devicesDir, { recursive: true });
5799
+ const registry = fs10.existsSync(bootstrapPath) ? JSON.parse(fs10.readFileSync(bootstrapPath, "utf-8")) : {};
5723
5800
  registry[bootstrapToken] = {
5724
5801
  token: bootstrapToken,
5725
5802
  profile: {
@@ -5728,15 +5805,15 @@ function mintBootstrapToken() {
5728
5805
  },
5729
5806
  issuedAtMs: Date.now()
5730
5807
  };
5731
- fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
5808
+ fs10.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
5732
5809
  return bootstrapToken;
5733
5810
  }
5734
5811
  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));
5812
+ const pluginRoot = path9.resolve(path9.dirname(api.source), "..");
5813
+ const appRoot = path9.join(pluginRoot, "web-build");
5814
+ const hasWebBuild = fs10.existsSync(appRoot);
5815
+ const indexPath = hasWebBuild ? path9.join(appRoot, "index.html") : null;
5816
+ const hasIndex = !!(indexPath && fs10.existsSync(indexPath));
5740
5817
  if (!hasWebBuild) {
5741
5818
  api.logger.info("[agentlife] web-build/ not found — /agentlife/pair will still register; static dashboard disabled");
5742
5819
  } else if (!hasIndex) {
@@ -5744,8 +5821,8 @@ function registerWebApp(api) {
5744
5821
  }
5745
5822
  let gatewayToken = "";
5746
5823
  try {
5747
- const configPath = path10.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
5748
- const raw = JSON.parse(fs11.readFileSync(configPath, "utf-8"));
5824
+ const configPath = path9.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
5825
+ const raw = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
5749
5826
  gatewayToken = raw?.gateway?.auth?.token || "";
5750
5827
  } catch {}
5751
5828
  loadOrCreatePairingAccessToken();
@@ -5863,16 +5940,16 @@ function registerWebApp(api) {
5863
5940
  return true;
5864
5941
  }
5865
5942
  const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
5866
- const filePath = path10.resolve(appRoot, relative);
5943
+ const filePath = path9.resolve(appRoot, relative);
5867
5944
  if (!filePath.startsWith(appRoot)) {
5868
5945
  res.writeHead(403);
5869
5946
  res.end();
5870
5947
  return true;
5871
5948
  }
5872
- const target = fs11.existsSync(filePath) && fs11.statSync(filePath).isFile() ? filePath : indexPath;
5873
- const ext = path10.extname(target).toLowerCase();
5949
+ const target = fs10.existsSync(filePath) && fs10.statSync(filePath).isFile() ? filePath : indexPath;
5950
+ const ext = path9.extname(target).toLowerCase();
5874
5951
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
5875
- let content = fs11.readFileSync(target);
5952
+ let content = fs10.readFileSync(target);
5876
5953
  if (target === indexPath) {
5877
5954
  setTimeout(approveLatest, 2000);
5878
5955
  setTimeout(approveLatest, 5000);
@@ -5961,9 +6038,9 @@ Setup code: ${setupCode}
5961
6038
  api.registerService({
5962
6039
  id: "agentlife-auto-pair",
5963
6040
  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");
6041
+ const devicesDir = path10.join(os6.homedir(), ".openclaw", "devices");
6042
+ const pendingPath = path10.join(devicesDir, "pending.json");
6043
+ const pairedPath = path10.join(devicesDir, "paired.json");
5967
6044
  const pollInterval = 3000;
5968
6045
  const withAdmin = (arr) => {
5969
6046
  const base = Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
@@ -6216,6 +6293,64 @@ function registerBootstrapHookImpl(api, state2) {
6216
6293
  }, { name: "agentlife-a2ui-guidance", description: "Inject A2UI component catalog and dashboard rules" });
6217
6294
  }
6218
6295
 
6296
+ // notifications.ts
6297
+ import * as fs11 from "node:fs";
6298
+ import * as path11 from "node:path";
6299
+ var config;
6300
+ function loadConfig() {
6301
+ if (config !== undefined)
6302
+ return config;
6303
+ try {
6304
+ const configPath = path11.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
6305
+ const raw = fs11.readFileSync(configPath, "utf-8");
6306
+ const parsed = JSON.parse(raw);
6307
+ if (parsed.serverUrl && parsed.apiKey) {
6308
+ config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
6309
+ console.log("[agentlife:notify] Notification config loaded from %s", configPath);
6310
+ return config;
6311
+ }
6312
+ console.warn("[agentlife:notify] Config missing serverUrl or apiKey");
6313
+ config = null;
6314
+ return null;
6315
+ } catch (e) {
6316
+ if (e?.code !== "ENOENT") {
6317
+ console.warn("[agentlife:notify] Failed to load notification config: %s", e?.message);
6318
+ }
6319
+ config = null;
6320
+ return null;
6321
+ }
6322
+ }
6323
+ function notifyWidgetEvent(_state, event, surfaceId, title, body) {
6324
+ broadcastNotification(event, surfaceId, title, body);
6325
+ const cfg = loadConfig();
6326
+ if (!cfg)
6327
+ return;
6328
+ console.log("[agentlife:notify] fire event=%s surfaceId=%s title=%s", event, surfaceId, title);
6329
+ const url = `${cfg.serverUrl.replace(/\/+$/, "")}/api/v1/notifications/send`;
6330
+ fetch(url, {
6331
+ method: "POST",
6332
+ headers: {
6333
+ "Content-Type": "application/json",
6334
+ Authorization: `Bearer ${cfg.apiKey}`
6335
+ },
6336
+ body: JSON.stringify({ title, body, data: { event, surfaceId } }),
6337
+ signal: AbortSignal.timeout(1e4)
6338
+ }).then(async (res) => {
6339
+ if (!res.ok) {
6340
+ console.warn("[agentlife:notify] Server responded %d for %s (event=%s)", res.status, surfaceId, event);
6341
+ return;
6342
+ }
6343
+ try {
6344
+ const parsed = await res.json();
6345
+ console.log("[agentlife:notify] sent event=%s surfaceId=%s messageId=%s", event, surfaceId, parsed.messageId ?? "(none)");
6346
+ } catch {
6347
+ console.log("[agentlife:notify] sent event=%s surfaceId=%s (no body)", event, surfaceId);
6348
+ }
6349
+ }).catch((err) => {
6350
+ console.warn("[agentlife:notify] Server failed for %s (event=%s): %s", surfaceId, event, err?.message);
6351
+ });
6352
+ }
6353
+
6219
6354
  // tools/widget-push.ts
6220
6355
  var PROVISIONED_IDS2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
6221
6356
  function isKnownAgent(state2, agentId, api) {
@@ -6330,13 +6465,21 @@ function registerWidgetPushTool(api, state2) {
6330
6465
  const filteredDsl = finalBlocks.join(`
6331
6466
  ---
6332
6467
  `);
6333
- const newSurfaceIds = [];
6468
+ const deletedSurfaceInfo = new Map;
6334
6469
  for (const raw of finalBlocks) {
6335
- const match = raw.match(/^surface\s+(\S+)/m);
6336
- if (!match)
6470
+ const deleteMatch = raw.match(/^delete\s+(\S+)/m);
6471
+ if (!deleteMatch)
6472
+ continue;
6473
+ const sid = deleteMatch[1];
6474
+ const view2 = getSurface(state2, sid);
6475
+ if (!view2)
6337
6476
  continue;
6338
- if (!hasSurface(state2, match[1]))
6339
- newSurfaceIds.push(match[1]);
6477
+ if (view2.isOverlay || view2.isInput)
6478
+ continue;
6479
+ deletedSurfaceInfo.set(sid, {
6480
+ title: capitalize(agentId),
6481
+ body: `Removed: ${extractWidgetText(view2.lines) ?? sid}`
6482
+ });
6340
6483
  }
6341
6484
  let results;
6342
6485
  try {
@@ -6345,22 +6488,32 @@ function registerWidgetPushTool(api, state2) {
6345
6488
  return { content: [{ type: "text", text: `Error: push failed — ${err?.message ?? err}` }] };
6346
6489
  }
6347
6490
  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);
6491
+ for (const result of results) {
6492
+ if (result.kind === "push") {
6493
+ if (result.isLoading)
6494
+ continue;
6495
+ const view2 = getSurface(state2, result.surfaceId);
6496
+ if (!view2)
6497
+ continue;
6498
+ if (view2.isOverlay || view2.isInput)
6499
+ continue;
6500
+ const notifTitle = capitalize(agentId);
6501
+ const notifBody = extractWidgetText(view2.lines) ?? "New update";
6502
+ const event = result.isNew ? "surface_created" : "surface_updated";
6503
+ notifyWidgetEvent(state2, event, result.surfaceId, notifTitle, notifBody);
6504
+ } else if (result.kind === "delete") {
6505
+ const info = deletedSurfaceInfo.get(result.surfaceId);
6506
+ if (!info)
6507
+ continue;
6508
+ notifyWidgetEvent(state2, "surface_deleted", result.surfaceId, info.title, info.body);
6509
+ }
6357
6510
  }
6358
6511
  for (const result of results) {
6359
6512
  if (result.kind !== "push")
6360
6513
  continue;
6361
- const view = getSurface(state2, result.surfaceId);
6362
- if (view?.context) {
6363
- autoRegisterInfraFromContext(state2, result.surfaceId, agentId, view.context);
6514
+ const view2 = getSurface(state2, result.surfaceId);
6515
+ if (view2?.context) {
6516
+ autoRegisterInfraFromContext(state2, result.surfaceId, agentId, view2.context);
6364
6517
  }
6365
6518
  }
6366
6519
  if (sessionKey && pushedSurfaceIds.length > 0) {
@@ -6370,6 +6523,8 @@ function registerWidgetPushTool(api, state2) {
6370
6523
  const goalChanges = [];
6371
6524
  const missingFollowup = [];
6372
6525
  const missingDetail = [];
6526
+ const templatedFollowups = [];
6527
+ const proliferationHits = [];
6373
6528
  for (const r of results) {
6374
6529
  if (r.kind !== "push")
6375
6530
  continue;
@@ -6379,17 +6534,29 @@ function registerWidgetPushTool(api, state2) {
6379
6534
  data: JSON.stringify({ issue: "goal_changed", surfaceId: r.surfaceId, from: r.goalChanged.from, to: r.goalChanged.to })
6380
6535
  });
6381
6536
  }
6537
+ if (r.followupTemplated) {
6538
+ templatedFollowups.push({ surfaceId: r.surfaceId, ...r.followupTemplated });
6539
+ recordActivity(state2, "quality_warning", sessionKey, agentId, {
6540
+ data: JSON.stringify({ issue: "followup_templated", surfaceId: r.surfaceId, previous: r.followupTemplated.previous, next: r.followupTemplated.next })
6541
+ });
6542
+ }
6543
+ if (r.siblingSurfaces && r.siblingSurfaces.length > 0) {
6544
+ proliferationHits.push({ surfaceId: r.surfaceId, siblings: r.siblingSurfaces });
6545
+ recordActivity(state2, "quality_warning", sessionKey, agentId, {
6546
+ data: JSON.stringify({ issue: "surface_proliferation", surfaceId: r.surfaceId, siblings: r.siblingSurfaces })
6547
+ });
6548
+ }
6382
6549
  if (r.isTransient)
6383
6550
  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)) {
6551
+ const view2 = getSurface(state2, r.surfaceId);
6552
+ const isLoading = r.isLoading;
6553
+ if (view2 && !isLoading && !view2.followup && !goalChanges.includes(r.surfaceId)) {
6387
6554
  missingFollowup.push(r.surfaceId);
6388
6555
  recordActivity(state2, "quality_warning", sessionKey, agentId, {
6389
6556
  data: JSON.stringify({ issue: "missing_followup", surfaceId: r.surfaceId })
6390
6557
  });
6391
6558
  }
6392
- if (view && !isLoading && !view.isInput && !hasDetailContent(view.rawDsl) && !goalChanges.includes(r.surfaceId) && !missingFollowup.includes(r.surfaceId)) {
6559
+ if (view2 && !isLoading && !view2.isInput && !hasDetailContent(view2.rawDsl) && !goalChanges.includes(r.surfaceId) && !missingFollowup.includes(r.surfaceId)) {
6393
6560
  missingDetail.push(r.surfaceId);
6394
6561
  recordActivity(state2, "quality_warning", sessionKey, agentId, {
6395
6562
  data: JSON.stringify({ issue: "missing_detail", surfaceId: r.surfaceId })
@@ -6402,6 +6569,10 @@ function registerWidgetPushTool(api, state2) {
6402
6569
  errors.push(buildMissingFollowupError(missingFollowup));
6403
6570
  if (missingDetail.length > 0)
6404
6571
  errors.push(buildMissingDetailError(missingDetail));
6572
+ if (templatedFollowups.length > 0)
6573
+ errors.push(buildTemplatedFollowupError(templatedFollowups));
6574
+ if (proliferationHits.length > 0)
6575
+ errors.push(buildSurfaceProliferationError(proliferationHits));
6405
6576
  const validationErrors = collectValidationErrors(state2, results, sessionKey, agentId);
6406
6577
  errors.push(...validationErrors);
6407
6578
  if (ownershipViolations.length > 0) {
@@ -6436,6 +6607,27 @@ function buildMissingFollowupError(ids) {
6436
6607
  function buildMissingDetailError(ids) {
6437
6608
  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
6609
  }
6610
+ function buildSurfaceProliferationError(hits) {
6611
+ const detail = hits.map((h) => {
6612
+ const siblingLines = h.siblings.map((s) => ` ${s.surfaceId} — goal: "${s.goal}"`).join(`
6613
+ `);
6614
+ return `${h.surfaceId} has live sibling(s) with near-identical goals:
6615
+ ${siblingLines}`;
6616
+ }).join(`
6617
+ `);
6618
+ 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.
6619
+ ${detail}
6620
+ ` + `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.`;
6621
+ }
6622
+ function buildTemplatedFollowupError(hits) {
6623
+ const detail = hits.map((h) => `${h.surfaceId}:
6624
+ previous: "${h.previous}"
6625
+ new: "${h.next}"`).join(`
6626
+ `);
6627
+ 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.
6628
+ ${detail}
6629
+ ` + `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.`;
6630
+ }
6439
6631
  function collectValidationErrors(state2, results, sessionKey, agentId) {
6440
6632
  const errors = [];
6441
6633
  const unknownKeywordHits = [];
@@ -7220,17 +7412,17 @@ function registerSurfacesGateway(api, state2) {
7220
7412
  respond(false, { error: "missing surfaceId" });
7221
7413
  return;
7222
7414
  }
7223
- const view = getSurface(state2, surfaceId);
7224
- if (!view) {
7415
+ const view2 = getSurface(state2, surfaceId);
7416
+ if (!view2) {
7225
7417
  respond(true, { surfaceId, dismissed: false });
7226
7418
  return;
7227
7419
  }
7228
7420
  const reason = typeof params?.reason === "string" ? params.reason : null;
7229
7421
  const guided = params?.guided === true;
7230
7422
  if (guided) {
7231
- const agentId2 = view.agentId;
7423
+ const agentId2 = view2.agentId;
7232
7424
  if (agentId2 && state2.runCommand) {
7233
- const goalSnippet = view.goal ? ` goal="${view.goal}"` : "";
7425
+ const goalSnippet = view2.goal ? ` goal="${view2.goal}"` : "";
7234
7426
  const reasonSnippet = reason ? ` reason="${reason}"` : "";
7235
7427
  const sessionKey = buildAgentSessionKey(agentId2);
7236
7428
  state2.pendingReactiveGuidance.set(sessionKey, DISMISS_ALTERNATIVES_GUIDANCE);
@@ -7250,7 +7442,7 @@ function registerSurfacesGateway(api, state2) {
7250
7442
  }
7251
7443
  console.log("[agentlife] guided dismiss: no agent for %s, falling through to immediate", surfaceId);
7252
7444
  }
7253
- const agentId = view.agentId;
7445
+ const agentId = view2.agentId;
7254
7446
  const cronIdRow = state2.followupQueue?.nextPending(surfaceId) ?? null;
7255
7447
  const cronId = cronIdRow ? String(cronIdRow.id) : null;
7256
7448
  const dismissMetadata = reason ? JSON.stringify({ reason }) : undefined;
@@ -7261,9 +7453,9 @@ function registerSurfacesGateway(api, state2) {
7261
7453
  automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
7262
7454
  } catch {}
7263
7455
  guidedDismissSent.delete(surfaceId);
7264
- if (!view.isTransient && view.path) {
7456
+ if (!view2.isTransient && view2.path) {
7265
7457
  try {
7266
- fs13.unlinkSync(view.path);
7458
+ fs13.unlinkSync(view2.path);
7267
7459
  } catch (err) {
7268
7460
  if (err?.code !== "ENOENT")
7269
7461
  console.warn("[agentlife] dismiss: file unlink failed for %s: %s", surfaceId, err?.message);
@@ -7336,9 +7528,9 @@ function registerSurfacesGateway(api, state2) {
7336
7528
  respond(false, { error: "missing surfaceId" });
7337
7529
  return;
7338
7530
  }
7339
- const view = getSurface(state2, surfaceId);
7340
- const agentId = view?.agentId ?? null;
7341
- const ctx = view?.context;
7531
+ const view2 = getSurface(state2, surfaceId);
7532
+ const agentId = view2?.agentId ?? null;
7533
+ const ctx = view2?.context;
7342
7534
  if (ctx && ctx.escalation === true && typeof ctx.target === "string") {
7343
7535
  const target = ctx.target;
7344
7536
  const sessionKey2 = buildAgentSessionKey(target);
@@ -7884,9 +8076,9 @@ function handleList(state2, params, respond) {
7884
8076
  }
7885
8077
  respond(true, {
7886
8078
  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;
8079
+ const view2 = getSurface(state2, r.surfaceId);
8080
+ const title = view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null;
8081
+ const goal = view2?.goal ?? null;
7890
8082
  return {
7891
8083
  id: r.id,
7892
8084
  surfaceId: r.surfaceId,
@@ -7964,13 +8156,14 @@ function handleRun(state2, params, respond) {
7964
8156
  respond(false, undefined, { code: "RUN_ERROR", message: e?.message });
7965
8157
  }
7966
8158
  }
7967
- function fireFollowupViaChat(state2, sessionKey, instruction, surfaceId) {
8159
+ function fireFollowupViaChat(state2, sessionKey, instruction2, surfaceId) {
7968
8160
  if (!state2.runCommand) {
7969
8161
  console.warn("[agentlife:followups-gw] runCommand not available — cannot fire followup");
7970
8162
  return;
7971
8163
  }
7972
8164
  const idempotencyKey = `followup-${surfaceId}-${Date.now()}`;
7973
- const params = JSON.stringify({ sessionKey, message: `[system] ${instruction}`, idempotencyKey });
8165
+ const wrapped = buildFireInstruction(state2, { surfaceId, instruction: instruction2 });
8166
+ const params = JSON.stringify({ sessionKey, message: `[system] ${wrapped}`, idempotencyKey });
7974
8167
  state2.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", params], { timeoutMs: 60000 }).then((result) => {
7975
8168
  console.log("[agentlife:followups-gw] chat.send result: code=%s", result?.code ?? "?");
7976
8169
  }).catch((e) => {
@@ -7984,8 +8177,8 @@ function handleEdit(state2, params, respond) {
7984
8177
  return;
7985
8178
  }
7986
8179
  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) {
8180
+ const instruction2 = typeof params?.instruction === "string" ? params.instruction.trim() : null;
8181
+ if (!delay && !instruction2) {
7989
8182
  respond(false, undefined, { code: "NO_CHANGES", message: "Provide delay and/or instruction" });
7990
8183
  return;
7991
8184
  }
@@ -8009,9 +8202,9 @@ function handleEdit(state2, params, respond) {
8009
8202
  }
8010
8203
  newFireAt = Date.now() + offsetMs;
8011
8204
  }
8012
- const newInstruction = instruction ?? row.instruction;
8205
+ const newInstruction = instruction2 ?? row.instruction;
8013
8206
  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 }));
8207
+ recordSurfaceEvent(state2, row.surfaceId, "followup_edited", undefined, row.agentId ?? undefined, JSON.stringify({ followupId: id, delay, instruction: instruction2 ? "(updated)" : null }));
8015
8208
  console.log("[agentlife:followups-gw] edited followup %d: fireAt=%d", id, newFireAt);
8016
8209
  respond(true, {
8017
8210
  followup: {