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.
- package/dist/index.js +886 -693
- 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:**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
965
|
-
-
|
|
966
|
-
-
|
|
967
|
-
-
|
|
968
|
-
-
|
|
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.**
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const autoDeletedInputIds = [];
|
|
2355
|
-
if (opts.evictStaleInputs !== false) {
|
|
2356
|
-
const incomingInputIds = new Set;
|
|
2357
|
-
for (const b of blocks) {
|
|
2358
|
-
if (b.kind === "push" && b.isInput)
|
|
2359
|
-
incomingInputIds.add(b.surfaceId);
|
|
2360
|
-
}
|
|
2361
|
-
if (incomingInputIds.size > 0) {
|
|
2362
|
-
autoDeletedInputIds.push(...state.transientSurfaces.takeStaleInputs(incomingInputIds));
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
for (const block of blocks) {
|
|
2366
|
-
if (block.kind === "delete") {
|
|
2367
|
-
const result2 = await handleDelete(state, block);
|
|
2368
|
-
results.push(result2);
|
|
2369
|
-
continue;
|
|
2370
|
-
}
|
|
2371
|
-
const result = await handlePush(state, block, opts);
|
|
2372
|
-
results.push(result);
|
|
2373
|
-
}
|
|
2374
|
-
broadcastSurface(dsl);
|
|
2375
|
-
for (const sid of autoDeletedInputIds)
|
|
2376
|
-
broadcastDelete(sid);
|
|
2377
|
-
return results;
|
|
2378
|
-
}
|
|
2379
|
-
async function handleDelete(state, block) {
|
|
2380
|
-
const sid = block.surfaceId;
|
|
2381
|
-
const agentId = state.surfaceIndex.get(sid)?.agentId ?? state.transientSurfaces.get(sid)?.agentId ?? null;
|
|
2382
|
-
state.transientSurfaces.delete(sid);
|
|
2383
|
-
const indexed = state.surfaceIndex.get(sid);
|
|
2384
|
-
if (indexed) {
|
|
2385
|
-
try {
|
|
2386
|
-
await state.fileSurfaceStorage.delete(indexed.agentId, sid);
|
|
2387
|
-
} catch (err) {
|
|
2388
|
-
console.warn("[push] file delete failed for %s: %s", sid, err?.message);
|
|
2389
|
-
}
|
|
2390
|
-
state.surfaceIndex.delete(sid);
|
|
2391
|
-
}
|
|
2392
|
-
state.followupQueue.cancel(sid);
|
|
2393
|
-
recordSurfaceEvent(state, sid, "deleted", undefined, agentId ?? undefined);
|
|
2394
|
-
broadcastDelete(sid);
|
|
2395
|
-
return {
|
|
2396
|
-
surfaceId: sid,
|
|
2397
|
-
kind: "delete",
|
|
2398
|
-
isNew: false,
|
|
2399
|
-
isTransient: false,
|
|
2400
|
-
followupChanged: false,
|
|
2401
|
-
followupRemoved: true,
|
|
2402
|
-
goalChanged: null,
|
|
2403
|
-
contextDroppedFields: [],
|
|
2404
|
-
validation: emptyValidation()
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
async function handlePush(state, block, opts) {
|
|
2408
|
-
const now = Date.now();
|
|
2409
|
-
const validation = validateBlockDsl(block.rawDsl);
|
|
2410
|
-
if (block.isTransient) {
|
|
2411
|
-
const existing2 = state.transientSurfaces.get(block.surfaceId);
|
|
2412
|
-
state.transientSurfaces.set(opts.agentId, block, opts.sessionKey ?? null);
|
|
2413
|
-
recordSurfaceEvent(state, block.surfaceId, existing2 ? "updated" : "created", block.rawDsl, opts.agentId);
|
|
2414
|
-
return {
|
|
2415
|
-
surfaceId: block.surfaceId,
|
|
2416
|
-
kind: "push",
|
|
2417
|
-
isNew: !existing2,
|
|
2418
|
-
isTransient: true,
|
|
2419
|
-
followupChanged: false,
|
|
2420
|
-
followupRemoved: false,
|
|
2421
|
-
goalChanged: null,
|
|
2422
|
-
contextDroppedFields: [],
|
|
2423
|
-
validation
|
|
2424
|
-
};
|
|
2425
|
-
}
|
|
2426
|
-
const existing = state.surfaceIndex.get(block.surfaceId);
|
|
2427
|
-
const filePath = await state.fileSurfaceStorage.write(opts.agentId, block.surfaceId, block.rawDsl);
|
|
2428
|
-
const resolvedContext = block.context ?? existing?.context ?? null;
|
|
2429
|
-
const contextDroppedFields = block.context && existing?.context ? Object.keys(existing.context).filter((k) => !(k in block.context)) : [];
|
|
2430
|
-
const resolvedGoal = block.goal ?? existing?.goal ?? null;
|
|
2431
|
-
const goalChanged = existing?.goal && block.goal && existing.goal !== block.goal ? { from: existing.goal, to: block.goal } : null;
|
|
2432
|
-
const newState = "active";
|
|
2433
|
-
const nextEntry = {
|
|
2434
|
-
surfaceId: block.surfaceId,
|
|
2435
|
-
agentId: existing?.agentId ?? opts.agentId,
|
|
2436
|
-
path: filePath,
|
|
2437
|
-
rawDsl: block.rawDsl,
|
|
2438
|
-
state: newState,
|
|
2439
|
-
isOverlay: false,
|
|
2440
|
-
dashboardVisible: existing?.dashboardVisible ?? true,
|
|
2441
|
-
createdAt: existing?.createdAt ?? now,
|
|
2442
|
-
updatedAt: now,
|
|
2443
|
-
expiredSince: null,
|
|
2444
|
-
originSessionKey: opts.sessionKey ?? existing?.originSessionKey ?? null,
|
|
2445
|
-
goal: resolvedGoal,
|
|
2446
|
-
context: resolvedContext,
|
|
2447
|
-
followup: block.followup,
|
|
2448
|
-
chainRoot: block.chainRoot,
|
|
2449
|
-
chainParent: block.chainParent,
|
|
2450
|
-
chainPosition: block.chainPosition,
|
|
2451
|
-
chainStatus: block.chainStatus,
|
|
2452
|
-
parseError: null
|
|
2453
|
-
};
|
|
2454
|
-
try {
|
|
2455
|
-
state.surfaceIndex.upsert(nextEntry);
|
|
2456
|
-
} catch (err) {
|
|
2457
|
-
console.warn("[push] index upsert failed for %s: %s", block.surfaceId, err?.message);
|
|
2458
|
-
}
|
|
2459
|
-
const oldFollowup = existing?.followup ?? null;
|
|
2460
|
-
const newFollowup = block.followup ?? null;
|
|
2461
|
-
const followupChanged = oldFollowup !== newFollowup;
|
|
2462
|
-
const followupRemoved = !!oldFollowup && !newFollowup;
|
|
2463
|
-
try {
|
|
2464
|
-
if (followupRemoved || !newFollowup) {
|
|
2465
|
-
state.followupQueue.cancel(block.surfaceId);
|
|
2466
|
-
} else if (followupChanged || !existing) {
|
|
2467
|
-
const spec = parseFollowupSpec(newFollowup);
|
|
2468
|
-
if (spec) {
|
|
2469
|
-
state.followupQueue.upsert({
|
|
2470
|
-
surfaceId: block.surfaceId,
|
|
2471
|
-
agentId: nextEntry.agentId,
|
|
2472
|
-
fireAt: now + spec.timeOffsetMs,
|
|
2473
|
-
instruction: spec.message,
|
|
2474
|
-
createdAt: now
|
|
2475
|
-
});
|
|
2476
|
-
} else {
|
|
2477
|
-
console.warn("[push] unparseable followup on %s: %s", block.surfaceId, newFollowup);
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
} catch (err) {
|
|
2481
|
-
console.warn("[push] followup queue update failed for %s: %s", block.surfaceId, err?.message);
|
|
2482
|
-
}
|
|
2483
|
-
recordSurfaceEvent(state, block.surfaceId, existing ? "updated" : "created", block.rawDsl, nextEntry.agentId);
|
|
2484
|
-
return {
|
|
2485
|
-
surfaceId: block.surfaceId,
|
|
2486
|
-
kind: "push",
|
|
2487
|
-
isNew: !existing,
|
|
2488
|
-
isTransient: false,
|
|
2489
|
-
followupChanged,
|
|
2490
|
-
followupRemoved,
|
|
2491
|
-
goalChanged,
|
|
2492
|
-
contextDroppedFields,
|
|
2493
|
-
validation
|
|
2494
|
-
};
|
|
2495
|
-
}
|
|
2496
|
-
function emptyValidation() {
|
|
2497
|
-
return {
|
|
2498
|
-
unknownKeywords: [],
|
|
2499
|
-
metadataWithoutColon: [],
|
|
2500
|
-
missingCardStructure: false,
|
|
2501
|
-
invalidInputActions: [],
|
|
2502
|
-
unknownAttrs: [],
|
|
2503
|
-
invalidEnumValues: []
|
|
2504
|
-
};
|
|
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
|
-
//
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
const cfg = runtime.config.loadConfig();
|
|
2511
|
-
const list = cfg?.agents?.list ?? [];
|
|
2512
|
-
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2513
|
-
}
|
|
2514
|
-
function isOnboarding(_state, runtime) {
|
|
2515
|
-
return userAgentIds(runtime).length === 0;
|
|
2516
|
-
}
|
|
2517
|
-
function snapshotOnboarding(state, runtime) {
|
|
2518
|
-
const count = userAgentIds(runtime).length;
|
|
2519
|
-
return { isActive: count === 0, userAgentCount: count };
|
|
2520
|
-
}
|
|
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
|
-
//
|
|
2675
|
-
function
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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
|
|
3345
|
-
|
|
3346
|
-
|
|
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
|
|
3351
|
-
|
|
3352
|
-
|
|
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
|
|
3382
|
-
if (!
|
|
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
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
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
|
-
|
|
3401
|
-
|
|
3494
|
+
foundSids.add(file.surfaceId);
|
|
3495
|
+
result.parseErrors++;
|
|
3496
|
+
log.warn(`[reconcile] parse error in ${file.surfaceId}: ${parseError}`);
|
|
3497
|
+
continue;
|
|
3402
3498
|
}
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
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
|
-
}
|
|
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
|
|
4011
|
+
const view2 = getSurface(state, STALE_SUPERVISOR_SURFACE);
|
|
3872
4012
|
removeFollowup(state, STALE_SUPERVISOR_SURFACE);
|
|
3873
|
-
if (
|
|
4013
|
+
if (view2 && !view2.isTransient && view2.path) {
|
|
3874
4014
|
try {
|
|
3875
|
-
fs6.unlinkSync(
|
|
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
|
|
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 >
|
|
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 >
|
|
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
|
|
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
|
|
4989
|
-
if (
|
|
5047
|
+
const view2 = getSurface(state, surfaceId);
|
|
5048
|
+
if (view2 && !view2.isTransient && view2.path) {
|
|
4990
5049
|
try {
|
|
4991
|
-
|
|
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
|
|
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
|
|
5242
|
-
import * as
|
|
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
|
|
5305
|
+
import * as fs8 from "node:fs";
|
|
5247
5306
|
import * as os3 from "node:os";
|
|
5248
|
-
import * as
|
|
5307
|
+
import * as path7 from "node:path";
|
|
5249
5308
|
var cachedToken = null;
|
|
5250
5309
|
function pairingAccessPath() {
|
|
5251
|
-
return
|
|
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 (
|
|
5259
|
-
const raw =
|
|
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 =
|
|
5269
|
-
if (!
|
|
5270
|
-
|
|
5271
|
-
|
|
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
|
-
|
|
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
|
|
5345
|
+
import * as fs9 from "node:fs";
|
|
5287
5346
|
import * as os4 from "node:os";
|
|
5288
|
-
import * as
|
|
5289
|
-
var AGENTLIFE_DIR =
|
|
5290
|
-
var TUNNEL_FILE =
|
|
5291
|
-
var BIN_DIR =
|
|
5292
|
-
var PAIR_REQUEST_MARKER =
|
|
5293
|
-
var IDENTITY_DIR =
|
|
5294
|
-
var DEVICE_FILE =
|
|
5295
|
-
var LEGACY_DEVICE_FILE =
|
|
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 =
|
|
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
|
|
5476
|
+
return fs9.existsSync(PAIR_REQUEST_MARKER);
|
|
5418
5477
|
}
|
|
5419
5478
|
function requestPair() {
|
|
5420
5479
|
try {
|
|
5421
|
-
if (!
|
|
5422
|
-
|
|
5423
|
-
|
|
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
|
-
|
|
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 (!
|
|
5455
|
-
|
|
5456
|
-
if (!
|
|
5457
|
-
|
|
5458
|
-
if (!
|
|
5459
|
-
|
|
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 (!
|
|
5521
|
+
if (!fs9.existsSync(filePath))
|
|
5463
5522
|
return null;
|
|
5464
5523
|
try {
|
|
5465
|
-
const raw =
|
|
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 (!
|
|
5484
|
-
|
|
5485
|
-
|
|
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
|
-
|
|
5546
|
+
fs9.chmodSync(DEVICE_FILE, 384);
|
|
5488
5547
|
} catch {}
|
|
5489
5548
|
try {
|
|
5490
|
-
|
|
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 (!
|
|
5503
|
-
|
|
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
|
-
|
|
5567
|
+
fs9.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5509
5568
|
try {
|
|
5510
|
-
|
|
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 (!
|
|
5576
|
+
if (!fs9.existsSync(TUNNEL_FILE))
|
|
5518
5577
|
return null;
|
|
5519
5578
|
try {
|
|
5520
|
-
const raw =
|
|
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
|
-
|
|
5588
|
+
fs9.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
|
|
5530
5589
|
try {
|
|
5531
|
-
|
|
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 (
|
|
5643
|
+
if (fs9.existsSync(CLOUDFLARED_PATH)) {
|
|
5585
5644
|
try {
|
|
5586
|
-
|
|
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
|
-
|
|
5663
|
+
fs9.unlinkSync(release.tempPath);
|
|
5605
5664
|
} catch {}
|
|
5606
5665
|
if (!extracted)
|
|
5607
5666
|
return null;
|
|
5608
5667
|
} else {
|
|
5609
5668
|
try {
|
|
5610
|
-
|
|
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
|
-
|
|
5676
|
+
fs9.chmodSync(CLOUDFLARED_PATH, 493);
|
|
5618
5677
|
} catch {}
|
|
5619
|
-
return
|
|
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:
|
|
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
|
-
|
|
5708
|
+
fs9.unlinkSync(dest);
|
|
5650
5709
|
} catch {}
|
|
5651
5710
|
return false;
|
|
5652
5711
|
}
|
|
5653
|
-
return
|
|
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
|
|
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 =
|
|
5719
|
-
const bootstrapPath =
|
|
5720
|
-
if (!
|
|
5721
|
-
|
|
5722
|
-
const registry =
|
|
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
|
-
|
|
5808
|
+
fs10.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
|
|
5732
5809
|
return bootstrapToken;
|
|
5733
5810
|
}
|
|
5734
5811
|
function registerWebApp(api) {
|
|
5735
|
-
const pluginRoot =
|
|
5736
|
-
const appRoot =
|
|
5737
|
-
const hasWebBuild =
|
|
5738
|
-
const indexPath = hasWebBuild ?
|
|
5739
|
-
const hasIndex = !!(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 =
|
|
5748
|
-
const raw = JSON.parse(
|
|
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 =
|
|
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 =
|
|
5873
|
-
const ext =
|
|
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 =
|
|
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 =
|
|
5965
|
-
const pendingPath =
|
|
5966
|
-
const pairedPath =
|
|
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
|
|
6468
|
+
const deletedSurfaceInfo = new Map;
|
|
6334
6469
|
for (const raw of finalBlocks) {
|
|
6335
|
-
const
|
|
6336
|
-
if (!
|
|
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 (
|
|
6339
|
-
|
|
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
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
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
|
|
6362
|
-
if (
|
|
6363
|
-
autoRegisterInfraFromContext(state2, result.surfaceId, agentId,
|
|
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
|
|
6385
|
-
const isLoading =
|
|
6386
|
-
if (
|
|
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 (
|
|
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
|
|
7224
|
-
if (!
|
|
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 =
|
|
7423
|
+
const agentId2 = view2.agentId;
|
|
7232
7424
|
if (agentId2 && state2.runCommand) {
|
|
7233
|
-
const goalSnippet =
|
|
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 =
|
|
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 (!
|
|
7456
|
+
if (!view2.isTransient && view2.path) {
|
|
7265
7457
|
try {
|
|
7266
|
-
fs13.unlinkSync(
|
|
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
|
|
7340
|
-
const agentId =
|
|
7341
|
-
const ctx =
|
|
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
|
|
7888
|
-
const title =
|
|
7889
|
-
const goal =
|
|
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,
|
|
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
|
|
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
|
|
7988
|
-
if (!delay && !
|
|
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 =
|
|
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:
|
|
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: {
|