agentlife 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1374 -455
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// index.ts
|
|
5
|
-
import { homedir as
|
|
6
|
-
import * as
|
|
5
|
+
import { homedir as homedir11 } from "node:os";
|
|
6
|
+
import * as path15 from "node:path";
|
|
7
7
|
import { existsSync as existsSync6 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// db.ts
|
|
@@ -193,6 +193,31 @@ function getOrCreateHistoryDb(state) {
|
|
|
193
193
|
);
|
|
194
194
|
CREATE INDEX IF NOT EXISTS idx_pqc_createdAt ON pending_quality_checks(createdAt);
|
|
195
195
|
|
|
196
|
+
CREATE TABLE IF NOT EXISTS cold_start_state (
|
|
197
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
198
|
+
phase TEXT NOT NULL,
|
|
199
|
+
enteredAt INTEGER NOT NULL,
|
|
200
|
+
lastActionAt INTEGER,
|
|
201
|
+
ackAt INTEGER,
|
|
202
|
+
deadlineAt INTEGER,
|
|
203
|
+
retryCount INTEGER NOT NULL DEFAULT 0,
|
|
204
|
+
failureReason TEXT,
|
|
205
|
+
lastTrigger TEXT,
|
|
206
|
+
actionSessionKey TEXT,
|
|
207
|
+
contractWarnings TEXT,
|
|
208
|
+
pollerLastTickAt INTEGER,
|
|
209
|
+
updatedAt INTEGER NOT NULL
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS cold_start_triggers (
|
|
213
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
214
|
+
kind TEXT NOT NULL,
|
|
215
|
+
payload TEXT NOT NULL,
|
|
216
|
+
createdAt INTEGER NOT NULL
|
|
217
|
+
);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_cold_start_triggers_createdAt
|
|
219
|
+
ON cold_start_triggers(createdAt);
|
|
220
|
+
|
|
196
221
|
`);
|
|
197
222
|
try {
|
|
198
223
|
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN cronId TEXT");
|
|
@@ -203,6 +228,9 @@ function getOrCreateHistoryDb(state) {
|
|
|
203
228
|
try {
|
|
204
229
|
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN originSessionKey TEXT");
|
|
205
230
|
} catch {}
|
|
231
|
+
try {
|
|
232
|
+
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN dashboardVisible INTEGER NOT NULL DEFAULT 1");
|
|
233
|
+
} catch {}
|
|
206
234
|
try {
|
|
207
235
|
const moved = state.historyDb.prepare(`
|
|
208
236
|
INSERT INTO activity_log (ts, sessionKey, agentId, event, data)
|
|
@@ -324,6 +352,13 @@ class SurfaceDb {
|
|
|
324
352
|
setState(id, state) {
|
|
325
353
|
this.db.prepare("UPDATE surfaces SET state = ? WHERE surfaceId = ?").run(state, id);
|
|
326
354
|
}
|
|
355
|
+
getDashboardVisible(id) {
|
|
356
|
+
const row = this.db.prepare("SELECT dashboardVisible FROM surfaces WHERE surfaceId = ?").get(id);
|
|
357
|
+
return row?.dashboardVisible !== 0;
|
|
358
|
+
}
|
|
359
|
+
setDashboardVisible(id, visible) {
|
|
360
|
+
this.db.prepare("UPDATE surfaces SET dashboardVisible = ? WHERE surfaceId = ?").run(visible ? 1 : 0, id);
|
|
361
|
+
}
|
|
327
362
|
getActiveSurfacesWithCrons() {
|
|
328
363
|
const rows = this.db.prepare("SELECT surfaceId, cronId FROM surfaces WHERE cronId IS NOT NULL AND state != 'dismissed'").all();
|
|
329
364
|
return rows.map((r) => ({ surfaceId: r.surfaceId, cronId: r.cronId }));
|
|
@@ -736,6 +771,28 @@ When receiving via sessions_send: push widgets with results. If nothing to push,
|
|
|
736
771
|
|
|
737
772
|
On every followup: review what you know, update stale entries. After every user interaction: did you learn something new? Store it in context: or agentlife.db.
|
|
738
773
|
|
|
774
|
+
### \`[system:start]\` — First-Run Baseline Capture
|
|
775
|
+
|
|
776
|
+
When you receive \`[system:start]\` on your direct session, you were just created by the builder. You have zero memory about this user. Your one-turn job:
|
|
777
|
+
|
|
778
|
+
1. Push a loading widget (\`state=loading\`) introducing yourself per \`IDENTITY.md\`.
|
|
779
|
+
2. Push ONE \`input\` surface asking the most important baseline question for your domain. Use button choices where possible; offer a textfield for open-ended answers. The final widget push (non-loading) can be the input surface itself.
|
|
780
|
+
3. Respond \`done\`.
|
|
781
|
+
|
|
782
|
+
On each subsequent \`[action:choice]\` or \`[action:submit]\` referencing a \`warmup-*\` surfaceId you pushed:
|
|
783
|
+
1. Append the answer to \`memory/baseline.md\` in your workspace via \`exec\` (create the \`memory/\` directory if missing). This file persists across sessions and feeds \`[system:dashboard-bootstrap]\`.
|
|
784
|
+
2. Delete the answered input surface.
|
|
785
|
+
3. Push the NEXT baseline question as a new \`warmup-*\` input surface, OR — if you've asked enough to establish a meaningful baseline for your domain — stop.
|
|
786
|
+
4. Respond \`done\`.
|
|
787
|
+
|
|
788
|
+
Rules:
|
|
789
|
+
- 2-4 total questions, one at a time. Never ask multiple questions in a single surface.
|
|
790
|
+
- Questions must be domain-relevant baseline — the minimum your domain needs to personalize first recommendations. Your AGENTS.md defines what baseline matters.
|
|
791
|
+
- Capture answers via \`exec\` writing to \`memory/baseline.md\`. Do NOT rely on session history — baseline must persist.
|
|
792
|
+
- No dashboard widgets in this flow beyond the intro + input surfaces. Domain work starts after baseline.
|
|
793
|
+
- Match the user's language.
|
|
794
|
+
- If \`memory/baseline.md\` already exists and is non-empty on entry, skip the questions and respond \`done\`.
|
|
795
|
+
|
|
739
796
|
`;
|
|
740
797
|
var INTERNAL_AGENTS_MD = `
|
|
741
798
|
## Internal Session — Self-Improvement Protocol
|
|
@@ -1253,64 +1310,6 @@ When you receive "Enrich agent registry descriptions" — system task, no widget
|
|
|
1253
1310
|
|
|
1254
1311
|
For each agent: read workspace AGENTS.md + SOUL.md → write one-sentence description via agentlife.createAgent. Specific domains, actions, data types. Process all, respond "Done".
|
|
1255
1312
|
|
|
1256
|
-
## Dashboard Bootstrap
|
|
1257
|
-
|
|
1258
|
-
When you receive "[system:dashboard-bootstrap]" — this is a different mode from agent creation.
|
|
1259
|
-
|
|
1260
|
-
Your task is to create vision widgets, NOT to create or modify agents. The message contains the user's data inline and the widget structure. Read the data, follow the structure, push the widgets with agentlife_push, then respond "Done".
|
|
1261
|
-
|
|
1262
|
-
Do NOT call agentlife.createAgent. Do NOT write workspace files. Do NOT spawn sub-sessions. Do NOT ask interactive questions. The user is not present — this runs at install time. Just read, build, push.
|
|
1263
|
-
|
|
1264
|
-
## Handling Vision Dismiss
|
|
1265
|
-
|
|
1266
|
-
When you receive \`[system:dismiss-requested] surfaceId=vision-{slug}\` — this is vision-specific dismiss handling. It is a third mode, distinct from agent creation and from dashboard bootstrap, and it OVERRIDES the general dismiss-alt protocol described elsewhere.
|
|
1267
|
-
|
|
1268
|
-
**CRITICAL — session scope:** this section applies ONLY on your DIRECT session (where the user is live and can see widgets). The platform also notifies your INTERNAL session with the same \`[system:dismiss-requested]\` message as a cleanup hook AFTER the widget is already deleted. If you see this message on an internal session, do NOT push a dismiss-alt, do NOT run the steps below — the widget is gone and pushing a questionnaire creates a ghost surface that nobody can interact with. On internal sessions, output "done" and stop.
|
|
1269
|
-
|
|
1270
|
-
Direct session vs internal session: internal sessions receive the "Internal Session — Self-Improvement Protocol" preamble at the top of their system prompt. If you see that preamble in your context, you are on an internal session; this Vision Dismiss section does NOT apply to you, output "done".
|
|
1271
|
-
|
|
1272
|
-
The general dismiss-alt rule says "craft 2-3 contextual alternatives from the widget's goal." For vision posters that rule does NOT apply. Vision posters are aspirational content with a closed ontology of failure modes — the user is rejecting either the dream itself, its framing, its timing, or its dashboard placement. Improvising alternatives per dismiss would produce inconsistent wording across dismissals, which reads as broken UX. Use the fixed wording below verbatim.
|
|
1273
|
-
|
|
1274
|
-
### Step 1 — Push the dismiss-alt input surface
|
|
1275
|
-
|
|
1276
|
-
Push exactly this surface (substitute \`{slug}\` with the dismissed vision's slug). Wording is fixed: do not paraphrase, translate, or reorder the buttons. The user's language detection happens at synthesis time, not at dismiss time.
|
|
1277
|
-
|
|
1278
|
-
\`\`\`
|
|
1279
|
-
surface dismiss-alt-vision-{slug} input
|
|
1280
|
-
card
|
|
1281
|
-
column
|
|
1282
|
-
text "Before I remove this:" h4
|
|
1283
|
-
button "This isn't my dream anymore" action=choice
|
|
1284
|
-
button "Wrong framing" action=choice
|
|
1285
|
-
button "Already achieved" action=choice
|
|
1286
|
-
button "Don't show on dashboard" action=choice
|
|
1287
|
-
button "Remove it" action=choice
|
|
1288
|
-
goal: Capture vision dismiss reason for vision-{slug}
|
|
1289
|
-
followup: +1m "User did not respond to vision dismiss alternatives. Say done."
|
|
1290
|
-
\`\`\`
|
|
1291
|
-
|
|
1292
|
-
### Step 2 — On the user's choice, persist the reason
|
|
1293
|
-
|
|
1294
|
-
When you receive \`[action:choice]\` on the dismiss-alt-vision-{slug} surface, append one line to your own workspace file at \`~/.openclaw/workspace-agentlife-builder/memory/vision-feedback.md\` (create the file and parent directory if missing):
|
|
1295
|
-
|
|
1296
|
-
\`\`\`
|
|
1297
|
-
- {ISO timestamp} vision-{slug}: {chosen reason verbatim}
|
|
1298
|
-
\`\`\`
|
|
1299
|
-
|
|
1300
|
-
Use exec for the append: \`mkdir -p ~/.openclaw/workspace-agentlife-builder/memory && echo "..." >> ~/.openclaw/workspace-agentlife-builder/memory/vision-feedback.md\`. This file is read by the next dashboard bootstrap as negative constraints under "## Previously rejected dreams" — your own future self will use it.
|
|
1301
|
-
|
|
1302
|
-
### Step 3 — Let the dismiss complete
|
|
1303
|
-
|
|
1304
|
-
Do NOT push a confirmation widget. Do NOT push \`delete vision-{slug}\`. The widget disappearing is the confirmation. The platform completes the dismiss after you process the action; you only need to record the reason.
|
|
1305
|
-
|
|
1306
|
-
If you receive \`[event:dismissed] surfaceId=vision-{slug}\` afterwards, the surface is already gone — respond "done" and stop.
|
|
1307
|
-
|
|
1308
|
-
### What this is NOT
|
|
1309
|
-
|
|
1310
|
-
- Not a domain widget dismiss (those use the general contextual dismiss-alt rules).
|
|
1311
|
-
- Not an opportunity to generate a replacement vision (the bootstrap path handles regeneration).
|
|
1312
|
-
- Not a place to ask follow-up questions or push additional surfaces beyond the fixed dismiss-alt above.
|
|
1313
|
-
|
|
1314
1313
|
## Deleting an Agent
|
|
1315
1314
|
|
|
1316
1315
|
1. agentlife.deleteAgent: {"id": "{agentId}"}
|
|
@@ -1339,16 +1338,16 @@ If you receive \`[event:dismissed] surfaceId=vision-{slug}\` afterwards, the sur
|
|
|
1339
1338
|
|
|
1340
1339
|
When you receive \`[action:*]\` on a confirmation widget: the user wants to use the agent you just created. Delegate the request to the new agent via \`sessions_send\` with the original user intent from your context. Delete your confirmation widget — the new agent takes over.
|
|
1341
1340
|
|
|
1342
|
-
##
|
|
1341
|
+
## Platform-Dispatched Entry Points — \`[system:*]\` Messages
|
|
1343
1342
|
|
|
1344
|
-
|
|
1343
|
+
The plugin's cold-start state machine dispatches these messages to your direct session at phase transitions. Each entry point is **atomic**: one starting message, one completion criterion. None overlap, none override your normal creation/improvement flow.
|
|
1345
1344
|
|
|
1346
|
-
###
|
|
1345
|
+
### \`[system:onboarding]\`
|
|
1347
1346
|
|
|
1348
|
-
|
|
1347
|
+
User just installed AgentLife with zero specialist agents. Push the life-domain input surface, then \`done\`. Do not create any agent yet.
|
|
1349
1348
|
|
|
1350
1349
|
\`\`\`
|
|
1351
|
-
surface onboarding input
|
|
1350
|
+
surface onboarding-domain input
|
|
1352
1351
|
card
|
|
1353
1352
|
column
|
|
1354
1353
|
text "What area of your life should your first agent focus on?" h4
|
|
@@ -1358,33 +1357,148 @@ surface onboarding input
|
|
|
1358
1357
|
button "Finance & money" action=choice
|
|
1359
1358
|
button "Something else" action=choice
|
|
1360
1359
|
goal: User picks a life domain for their first agent
|
|
1361
|
-
followup: +
|
|
1360
|
+
followup: +30m "User did not respond to onboarding. Say done."
|
|
1362
1361
|
\`\`\`
|
|
1363
1362
|
|
|
1364
|
-
|
|
1363
|
+
Translate button labels into the user's locale if known. Do not vary the surfaceId \`onboarding-domain\` — the state machine observes it.
|
|
1365
1364
|
|
|
1366
|
-
|
|
1365
|
+
**When \`[action:choice] surfaceId=onboarding-domain label=<domain>\` arrives next, execute the full creation flow. You MUST complete every step below. A "ready" confirmation widget pushed without the \`agentlife.createAgent\` RPC call is a critical platform violation — the user sees a green "Ready" badge for an agent that does not exist, and the state machine never advances out of AWAITING_AGENT.**
|
|
1367
1366
|
|
|
1368
|
-
|
|
1367
|
+
Required call sequence for the post-choice creation turn:
|
|
1369
1368
|
|
|
1370
|
-
1.
|
|
1371
|
-
2.
|
|
1372
|
-
3.
|
|
1373
|
-
4.
|
|
1374
|
-
5.
|
|
1369
|
+
1. **Delete the input surface.** Push \`delete onboarding-domain\` (or \`delete health-qN\` if you asked follow-ups).
|
|
1370
|
+
2. **Clarifying questions (optional, 1-2 max).** If domain framing is ambiguous, push \`warmup-{slug}\` input surfaces to narrow scope. Skip this step if the label is already specific enough to create from.
|
|
1371
|
+
3. **Discover environment.** If the domain requires system facts (networks, devices, installed tools), run \`exec\` commands per Step 2 of the main "Creating a New Agent" flow above. Skip if the domain is purely behavioral (health, finance).
|
|
1372
|
+
4. **Write workspace files.** \`exec mkdir -p ~/.openclaw/workspace-{agentId}\` then \`write\` AGENTS.md, SOUL.md, IDENTITY.md, USER.md, HEARTBEAT.md. Follow Step 3 of the main creation flow — every file present, no placeholders.
|
|
1373
|
+
5. **Register the agent via \`exec\`.** Call \`exec openclaw gateway call agentlife.createAgent --params '<JSON>'\`. The JSON must include \`id\`, \`name\`, \`workspace\` (absolute path), \`description\`, \`tools\`, \`identity\`. This call is what makes the agent real — no shortcut.
|
|
1374
|
+
6. **Only after createAgent returns ok**, push the confirmation widget per Step 6 of the main creation flow.
|
|
1375
1375
|
|
|
1376
|
-
|
|
1376
|
+
Forbidden shortcuts:
|
|
1377
|
+
- Do NOT push a "Ready" / "Created" / "Setting up" style confirmation widget before steps 4 and 5 complete.
|
|
1378
|
+
- Do NOT emit \`done\` until the RPC call in step 5 succeeded (check the response, not just that the tool ran).
|
|
1379
|
+
- Do NOT write about the agent being ready in chat text — the confirmation is the widget, and only after step 5.
|
|
1377
1380
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
- Not a comprehensive onboarding wizard. One question. One agent. The user can create more later by chatting normally.
|
|
1381
|
-
- Not a place to explain what AgentLife is. The user installed the plugin — they know.
|
|
1382
|
-
- Not a place to push multiple widgets. One input, one confirmation, done.
|
|
1381
|
+
If the user picked "Something else", push a follow-up textfield input on the same \`onboarding-domain\` surfaceId to capture the freeform domain, then proceed through steps 1-6 once you have an answer.
|
|
1383
1382
|
|
|
1384
1383
|
## What You Are Not
|
|
1385
1384
|
|
|
1386
1385
|
- Not an orchestrator — you don't route messages
|
|
1387
1386
|
- Not a general assistant — only agent creation and improvement
|
|
1387
|
+
- Not the vision synthesizer — \`[system:dashboard-bootstrap]\`, \`[system:ask-dream]\`, and vision dismissals belong to \`agentlife-vision\`
|
|
1388
|
+
`;
|
|
1389
|
+
var VISION_AGENTS_MD = `# AgentLife Vision Synthesizer
|
|
1390
|
+
|
|
1391
|
+
You turn the user's data into the aspirational backdrop of the dashboard. You do not create agents, route messages, or answer questions. The platform dispatches you only for the three entry points below.
|
|
1392
|
+
|
|
1393
|
+
## Platform-Dispatched Entry Points — \`[system:*]\` Messages
|
|
1394
|
+
|
|
1395
|
+
These are your ONLY valid triggers. Each is atomic: one starting message, one completion criterion.
|
|
1396
|
+
|
|
1397
|
+
### \`[system:dashboard-bootstrap]\`
|
|
1398
|
+
|
|
1399
|
+
Synthesize vision posters from user data that the platform injects in the message body under \`## USER DATA\` (and optionally \`## userDream:\` when the user has answered the dream-ask flow). The task has **exactly two valid outcomes**:
|
|
1400
|
+
|
|
1401
|
+
1. Push 1-4 \`vision-*\` posters quoted from USER DATA, then respond \`done\`.
|
|
1402
|
+
2. Respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1403
|
+
|
|
1404
|
+
Pushing a poster and then deleting it is NOT a valid recovery — the user sees the poster flash and disappear, which reads as a broken app. Pick ONE outcome.
|
|
1405
|
+
|
|
1406
|
+
**Step 1 — Scan for aspirational signal.** In your own reasoning (not pushed), list the exact phrases where the user names a future they want, a target they're chasing, or a transformation they're working toward. Operational logging (times, weights, metrics, "no baseline yet") is NOT signal. Tone specs, tool schemas, and agent descriptions are NOT signal.
|
|
1407
|
+
|
|
1408
|
+
**Step 2 — Decide.** If you listed at least one real quoted phrase → push posters. If you listed zero → respond \`NO_SIGNAL\` on its own line and STOP. Never invent a phrase the user didn't say. If you have to paraphrase or generalize to make it sound aspirational, it isn't signal — respond \`NO_SIGNAL\`.
|
|
1409
|
+
|
|
1410
|
+
**Step 3 — Push posters (only if Step 2 said yes).** For each quoted phrase (max 4), push one vision poster. Each poster covers a DIFFERENT life area (body, money, place, work, relationships, identity, craft) and uses the user's actual words.
|
|
1411
|
+
|
|
1412
|
+
A vision poster is the **aspirational backdrop** of the dashboard — it justifies the user's commitment to the platform and anchors why every other widget exists. Treat it like the cover of their year, not a sticker.
|
|
1413
|
+
|
|
1414
|
+
#### Surface rules
|
|
1415
|
+
|
|
1416
|
+
- \`size=l\` and \`priority=high\` — visions earn the largest slot and the highest sort order.
|
|
1417
|
+
- The face lives in \`card > column align=center\`. Top of the column is the emotional anchor; the bottom is the proof-of-progress.
|
|
1418
|
+
- Use the standard widget components (text, divider, badge, metric, progress, gauge, sparkline) — no exotic compositions. Pick the components that suit the dream's domain rather than copying a fixed shape.
|
|
1419
|
+
|
|
1420
|
+
#### Face composition rules (apply to every vision poster)
|
|
1421
|
+
|
|
1422
|
+
- Open with a single emoji at \`h1\` size — visceral, not literal. The emoji carries the emotional charge of the dream.
|
|
1423
|
+
- Follow with a declarative headline at \`h1\` in the accent color — 3-5 words, first-person when natural, naming the dream version (not the next checkpoint).
|
|
1424
|
+
- Add a support line in \`body\` text — 6-12 words drawn from the user's actual language. No generic platitudes.
|
|
1425
|
+
- Place a \`divider\` between the dream framing and the proof framing.
|
|
1426
|
+
- Below the divider include AT LEAST ONE quantitative element (\`progress\`, \`gauge\`, or two paired \`metric\`s) showing where the user stands today relative to the dream. If you have no real measurement, set the value to \`0.05\` and label it as the journey starting — never \`0.0\` (reads as broken), never invent a fake value.
|
|
1427
|
+
- End with one anchor element naming the time horizon, the place, the phase, or the milestone (a \`text\` line, a \`badge\`, or a \`row\` of accent-colored beats). Pick the anchor type that fits the dream's domain.
|
|
1428
|
+
- Vary the accent color across posters in the carousel. Suggested palette: \`#10B981\`, \`#7C3AED\`, \`#3B82F6\`, \`#F59E0B\`, \`#EC4899\`, \`#14B8A6\`.
|
|
1429
|
+
|
|
1430
|
+
#### Metadata rules (every poster must carry all four)
|
|
1431
|
+
|
|
1432
|
+
- \`goal:\` — ONE sentence in the user's voice naming the actual aspiration. References at least one concrete element from USER DATA (a quoted phrase, a number, a place, a domain term). Never write \`Vision poster — {slug}\` or any meta-label — that fails the platform's goal-validation rule (it could have been written before the work).
|
|
1433
|
+
- \`detail:\` — markdown body that earns the poster a place on the paywall. Required sections: a short "Why this matters" paragraph grounded in the user's data; a quote block with 1-2 verbatim phrases from USER DATA; a list of the 2-3 measurable signals (proxies) that would tell a specialist agent the user is on track, each tied to a real database table or habit; one named near-term milestone within the followup window.
|
|
1434
|
+
- \`followup:\` — duration scaled to the dream's natural rhythm: body/habit dreams 14-30d, money/career 30-90d, place/identity 90-180d. The instruction MUST name which proxies to query, the threshold separating "on track" from "drift", and the resulting action (push an update, reframe, or stay silent). \`Refresh this vision if data has shifted\` is forbidden — it could have been written before the work.
|
|
1435
|
+
- \`context:\` — JSON pointers the followup needs: domain tag, the user's quoted phrases that drove this poster, and \`{name, source}\` entries for each proxy so the next invocation knows where to read fresh data. Lookup keys only, no cached values.
|
|
1436
|
+
|
|
1437
|
+
#### Universal rules
|
|
1438
|
+
|
|
1439
|
+
- **Variety:** across the carousel, do not reuse the same anchor element type twice. If two posters end on a \`badge\`, rework one to use a \`row\` of beats or a \`text\` anchor. Composition variety is what stops the dashboard reading as a template grid.
|
|
1440
|
+
- **Goal & followup validation:** before pushing each poster, re-read its \`goal:\` and \`followup:\` lines and ask "could I have written these EXACT words before reading USER DATA?" If yes, rewrite — they're templates, not work product.
|
|
1441
|
+
- **Language consistency:** detect the primary language the user writes in from the data. Write ALL posters in that single language. Never mix languages across the carousel.
|
|
1442
|
+
- **Quality bar:** better to push 2 strong posters than 4 mixed-quality ones. If you cannot find a real, named, repeated dream for a domain, SKIP that domain. Never invent. Never fill a slot with weather data, agent infrastructure, or generic placeholder language. If a poster names tools/agents instead of outcomes, it is wrong — drop it.
|
|
1443
|
+
|
|
1444
|
+
After all pushes, respond \`done\` and STOP. If you cannot find signal, respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1445
|
+
|
|
1446
|
+
### \`[system:ask-dream]\`
|
|
1447
|
+
|
|
1448
|
+
The bootstrap reported \`NO_SIGNAL\`. Push the dream-capture input surface and \`done\`. Do NOT push posters from this entry — the plugin re-fires \`[system:dashboard-bootstrap]\` with \`userDream:\` injected after the user answers.
|
|
1449
|
+
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
surface awaiting-dream-input input
|
|
1452
|
+
card
|
|
1453
|
+
column
|
|
1454
|
+
text "Tell me one dream or goal you want to track" h4
|
|
1455
|
+
button "Body & health" action=choice
|
|
1456
|
+
button "Money & work" action=choice
|
|
1457
|
+
button "Place & life chapter" action=choice
|
|
1458
|
+
button "Craft or identity" action=choice
|
|
1459
|
+
goal: Capture the user's dream so the dashboard can synthesize a vision poster
|
|
1460
|
+
followup: +30m "User did not respond to dream-ask. Say done."
|
|
1461
|
+
\`\`\`
|
|
1462
|
+
|
|
1463
|
+
Translate labels into the user's locale if known. Do not vary the surfaceId \`awaiting-dream-input\` — the state machine observes it.
|
|
1464
|
+
|
|
1465
|
+
### \`[system:dismiss-requested] surfaceId=vision-*\`
|
|
1466
|
+
|
|
1467
|
+
User dismissed a vision poster. Push the fixed dismiss-alt surface below. Wording is fixed: do not paraphrase, translate, or reorder buttons.
|
|
1468
|
+
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
surface dismiss-alt-vision-{slug} input
|
|
1471
|
+
card
|
|
1472
|
+
column
|
|
1473
|
+
text "Before I remove this:" h4
|
|
1474
|
+
button "This isn't my dream anymore" action=choice
|
|
1475
|
+
button "Wrong framing" action=choice
|
|
1476
|
+
button "Already achieved" action=choice
|
|
1477
|
+
button "Don't show on dashboard" action=choice
|
|
1478
|
+
button "Remove it" action=choice
|
|
1479
|
+
goal: Capture vision dismiss reason for vision-{slug}
|
|
1480
|
+
followup: +1m "User did not respond to vision dismiss alternatives. Say done."
|
|
1481
|
+
\`\`\`
|
|
1482
|
+
|
|
1483
|
+
After pushing, respond \`done\`.
|
|
1484
|
+
|
|
1485
|
+
When \`[action:choice]\` arrives on \`dismiss-alt-vision-{slug}\`, append one line to your feedback memory via exec, then \`done\`:
|
|
1486
|
+
|
|
1487
|
+
\`\`\`
|
|
1488
|
+
exec mkdir -p ~/.openclaw/workspace-agentlife-vision/memory && \\
|
|
1489
|
+
echo "- $(date -u +%Y-%m-%dT%H:%M:%SZ) vision-{slug}: {reason}" \\
|
|
1490
|
+
>> ~/.openclaw/workspace-agentlife-vision/memory/vision-feedback.md
|
|
1491
|
+
\`\`\`
|
|
1492
|
+
|
|
1493
|
+
This file is injected as "Previously rejected dreams" into the next \`[system:dashboard-bootstrap]\` so future synthesis avoids the same misreads.
|
|
1494
|
+
|
|
1495
|
+
Do NOT push a confirmation widget. Do NOT push \`delete vision-{slug}\` — the platform handles removal (soft-delete for paywall persistence; the widget disappears from the dashboard). If you receive \`[event:dismissed] surfaceId=vision-{slug}\` afterwards, the surface state has already been finalized — respond \`done\`.
|
|
1496
|
+
|
|
1497
|
+
## What You Are Not
|
|
1498
|
+
|
|
1499
|
+
- Not a builder — agent creation and improvement belong to \`agentlife-builder\`
|
|
1500
|
+
- Not an orchestrator — you never route or answer user questions
|
|
1501
|
+
- Not a chat agent — you produce widgets or \`NO_SIGNAL\`/\`done\` markers only
|
|
1388
1502
|
`;
|
|
1389
1503
|
var SUPERVISOR_AGENTS_MD = `# Dashboard Supervisor
|
|
1390
1504
|
|
|
@@ -1488,6 +1602,12 @@ var PROVISIONED_AGENTS = [
|
|
|
1488
1602
|
agentsMd: BUILDER_AGENTS_MD,
|
|
1489
1603
|
tools: { profile: "full", alsoAllow: ["agentlife_push"] }
|
|
1490
1604
|
},
|
|
1605
|
+
{
|
|
1606
|
+
id: "agentlife-vision",
|
|
1607
|
+
name: "AgentLife Vision",
|
|
1608
|
+
agentsMd: VISION_AGENTS_MD,
|
|
1609
|
+
tools: { profile: "full", alsoAllow: ["agentlife_push"] }
|
|
1610
|
+
},
|
|
1491
1611
|
{
|
|
1492
1612
|
id: "supervisor",
|
|
1493
1613
|
name: "Supervisor",
|
|
@@ -1604,216 +1724,6 @@ ${capped}`);
|
|
|
1604
1724
|
|
|
1605
1725
|
`) };
|
|
1606
1726
|
}
|
|
1607
|
-
var VISION_MIN_MEMORY_CHARS = 200;
|
|
1608
|
-
var BOOTSTRAP_COOLDOWN_MS = 10 * 60 * 1000;
|
|
1609
|
-
var lastBootstrapSentAt = 0;
|
|
1610
|
-
var lastWarmupSentAt = 0;
|
|
1611
|
-
function resetBootstrapCooldown() {
|
|
1612
|
-
lastBootstrapSentAt = 0;
|
|
1613
|
-
lastWarmupSentAt = 0;
|
|
1614
|
-
}
|
|
1615
|
-
async function ensureVisionPosters(state, runtime, log, options = {}) {
|
|
1616
|
-
if (!state.surfaceDb) {
|
|
1617
|
-
log("[agentlife] ensureVisionPosters: skipped — surfaceDb not initialized");
|
|
1618
|
-
return { status: "skipped", reason: "no_surface_db" };
|
|
1619
|
-
}
|
|
1620
|
-
let visionCount = 0;
|
|
1621
|
-
for (const surfaceId of state.surfaceDb.keys()) {
|
|
1622
|
-
if (surfaceId.startsWith("vision-"))
|
|
1623
|
-
visionCount++;
|
|
1624
|
-
}
|
|
1625
|
-
if (visionCount > 0) {
|
|
1626
|
-
log(`[agentlife] ensureVisionPosters: already_exists — ${visionCount} vision surface(s) in surfaceDb`);
|
|
1627
|
-
return { status: "already_exists", count: visionCount };
|
|
1628
|
-
}
|
|
1629
|
-
const SKIP_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
1630
|
-
const finalList = runtime.config.loadConfig().agents?.list ?? [];
|
|
1631
|
-
const specialistCount = [...state.agentRegistry.keys()].filter((id) => !SKIP_IDS.has(id)).length;
|
|
1632
|
-
if (specialistCount === 0) {
|
|
1633
|
-
log("[agentlife] ensureVisionPosters: skipped — specialistCount=0 (no user agents registered)");
|
|
1634
|
-
return { status: "skipped", reason: "no_specialists" };
|
|
1635
|
-
}
|
|
1636
|
-
if (!state.runCommand) {
|
|
1637
|
-
log("[agentlife] ensureVisionPosters: skipped — runCommand not available on runtime");
|
|
1638
|
-
return { status: "skipped", reason: "no_run_command" };
|
|
1639
|
-
}
|
|
1640
|
-
const { totalChars, sections } = await gatherAllAgentMemory(state, finalList, SKIP_IDS);
|
|
1641
|
-
if (totalChars < VISION_MIN_MEMORY_CHARS) {
|
|
1642
|
-
const warmupNow = Date.now();
|
|
1643
|
-
if (state.runCommand && warmupNow - lastWarmupSentAt >= BOOTSTRAP_COOLDOWN_MS) {
|
|
1644
|
-
const specialistId = [...state.agentRegistry.keys()].find((id) => !SKIP_IDS.has(id));
|
|
1645
|
-
if (specialistId) {
|
|
1646
|
-
lastWarmupSentAt = warmupNow;
|
|
1647
|
-
const warmupKey = buildAgentSessionKey(specialistId);
|
|
1648
|
-
const warmupMsg = [
|
|
1649
|
-
`[system:warmup] You were just created and have no user data yet.`,
|
|
1650
|
-
``,
|
|
1651
|
-
`1. Push a guided input widget (input surface, action=choice buttons) asking the user for the most important baseline info for your domain. One question at a time, 2-3 questions max.`,
|
|
1652
|
-
`2. After each answer, WRITE the data to your memory directory: exec mkdir -p ~/.openclaw/workspace-${specialistId}/memory && write the answer to a file there (e.g., memory/baseline.md). This is critical — your memory directory is what the platform reads to generate the user's vision dashboard. If you only store data in widget context, it's lost when the widget is dismissed.`,
|
|
1653
|
-
`3. After collecting all answers, push a DASHBOARD widget (NOT input) summarizing what you learned. This widget must NOT have the "input" keyword — it renders on the main dashboard, not the input bar.`,
|
|
1654
|
-
`4. Use the user's language.`,
|
|
1655
|
-
``,
|
|
1656
|
-
`If you already collected baseline data in a previous session, check your memory directory. If it has content, push a dashboard summary widget and respond "done". Do NOT re-ask questions you already have answers for.`
|
|
1657
|
-
].join(`
|
|
1658
|
-
`);
|
|
1659
|
-
const warmupParams = JSON.stringify({
|
|
1660
|
-
sessionKey: warmupKey,
|
|
1661
|
-
message: warmupMsg,
|
|
1662
|
-
idempotencyKey: `warmup-${specialistId}-${Date.now()}`
|
|
1663
|
-
});
|
|
1664
|
-
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", warmupParams], { timeoutMs: 120000 }).then(() => {
|
|
1665
|
-
log(`[agentlife] ensureVisionPosters: warmup sent to ${specialistId}`);
|
|
1666
|
-
}).catch((e) => {
|
|
1667
|
-
log(`[agentlife] ensureVisionPosters: warmup failed for ${specialistId}: ${e?.message}`);
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
log(`[agentlife] ensureVisionPosters: skipped — totalChars=${totalChars} below threshold ${VISION_MIN_MEMORY_CHARS} (memory too thin)`);
|
|
1672
|
-
return { status: "skipped", reason: "thin_memory", details: `totalChars=${totalChars}` };
|
|
1673
|
-
}
|
|
1674
|
-
const homeDir = options.homedirOverride ?? os.homedir();
|
|
1675
|
-
const builderWorkspace = path3.join(homeDir, ".openclaw", "workspace-agentlife-builder");
|
|
1676
|
-
const feedbackFile = path3.join(builderWorkspace, "memory", "vision-feedback.md");
|
|
1677
|
-
let rejectedDreams = "";
|
|
1678
|
-
try {
|
|
1679
|
-
const content = await fs2.readFile(feedbackFile, "utf-8");
|
|
1680
|
-
if (content.trim())
|
|
1681
|
-
rejectedDreams = content.trim().slice(0, 4000);
|
|
1682
|
-
} catch {}
|
|
1683
|
-
const now = Date.now();
|
|
1684
|
-
if (now - lastBootstrapSentAt < BOOTSTRAP_COOLDOWN_MS) {
|
|
1685
|
-
log(`[agentlife] ensureVisionPosters: skipped — bootstrap cooldown (${Math.round((BOOTSTRAP_COOLDOWN_MS - (now - lastBootstrapSentAt)) / 1000)}s remaining)`);
|
|
1686
|
-
return { status: "skipped", reason: "cooldown", details: `${Math.round((BOOTSTRAP_COOLDOWN_MS - (now - lastBootstrapSentAt)) / 1000)}s remaining` };
|
|
1687
|
-
}
|
|
1688
|
-
const builderKey = buildAgentSessionKey("agentlife-builder");
|
|
1689
|
-
const bootstrapMsg = [
|
|
1690
|
-
`[system:dashboard-bootstrap] The dashboard is empty. Push 3-4 vision posters with agentlife_push.`,
|
|
1691
|
-
``,
|
|
1692
|
-
`## What you do`,
|
|
1693
|
-
``,
|
|
1694
|
-
`Pick 2 to 4 dreams from the user data below — quality over quantity. For each, copy one of the 3 DSL templates exactly and replace the placeholders with the user's own words. Push via agentlife_push.`,
|
|
1695
|
-
``,
|
|
1696
|
-
`## Picking the dreams`,
|
|
1697
|
-
``,
|
|
1698
|
-
`Find aspirational signal — moments where the user names a future they want, a target they're chasing, a transformation they're working toward. Operational logging is not signal.`,
|
|
1699
|
-
``,
|
|
1700
|
-
`Each poster covers a DIFFERENT life area. Body, money, place, work, relationships, identity, craft are distinct. If two candidates share a product, person, or domain name, they're the same area — pick one.`,
|
|
1701
|
-
``,
|
|
1702
|
-
`Pick the dream version of each goal — the one-to-three-year outcome if everything goes right, not the next checkpoint. The first real revenue is a checkpoint; the first profitable year is a dream.`,
|
|
1703
|
-
``,
|
|
1704
|
-
`## Quality bar`,
|
|
1705
|
-
``,
|
|
1706
|
-
`It is better to push 2 strong posters than 4 mixed-quality ones. If you cannot find a real, named, repeated dream for a domain, SKIP that domain. Never invent. Never fill a slot with weather data, agent infrastructure, or generic placeholder language. If a poster's content names tools/agents instead of outcomes, it is wrong — drop it.`,
|
|
1707
|
-
``,
|
|
1708
|
-
`## Language consistency`,
|
|
1709
|
-
``,
|
|
1710
|
-
`Detect the primary language the user writes in (look at the data: Spanish, English, etc.). Write ALL posters in that single language. Never mix languages across the carousel.`,
|
|
1711
|
-
``,
|
|
1712
|
-
`## The 3 templates`,
|
|
1713
|
-
``,
|
|
1714
|
-
`Every template renders emoji + headline + support + a bottom element. The bottom element changes per template.`,
|
|
1715
|
-
``,
|
|
1716
|
-
`### TEMPLATE A — Statement (best for identity, body, declarative truths)`,
|
|
1717
|
-
``,
|
|
1718
|
-
`surface vision-{slug} size=l`,
|
|
1719
|
-
` card`,
|
|
1720
|
-
` column align=center`,
|
|
1721
|
-
` text "{EMOJI}" h1`,
|
|
1722
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1723
|
-
` text "{SUPPORT}" body`,
|
|
1724
|
-
` text "{MOMENT}" h4 color={ACCENT}`,
|
|
1725
|
-
`goal: Vision poster — {slug}`,
|
|
1726
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1727
|
-
``,
|
|
1728
|
-
`### TEMPLATE B — Destination (best for place, move, new chapter)`,
|
|
1729
|
-
``,
|
|
1730
|
-
`surface vision-{slug} size=l`,
|
|
1731
|
-
` card`,
|
|
1732
|
-
` column align=center`,
|
|
1733
|
-
` text "{EMOJI}" h1`,
|
|
1734
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1735
|
-
` text "{SUPPORT}" body`,
|
|
1736
|
-
` badge "{PLACE}" color={ACCENT} outlined`,
|
|
1737
|
-
`goal: Vision poster — {slug}`,
|
|
1738
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1739
|
-
``,
|
|
1740
|
-
`### TEMPLATE C — Outcome (best for work, money, craft)`,
|
|
1741
|
-
``,
|
|
1742
|
-
`surface vision-{slug} size=l`,
|
|
1743
|
-
` card`,
|
|
1744
|
-
` column align=center`,
|
|
1745
|
-
` text "{EMOJI}" h1`,
|
|
1746
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1747
|
-
` text "{SUPPORT}" body`,
|
|
1748
|
-
` row distribute=spaceEvenly`,
|
|
1749
|
-
` text "{C1}" h4 color={ACCENT}`,
|
|
1750
|
-
` text "·" h4`,
|
|
1751
|
-
` text "{C2}" h4 color={ACCENT}`,
|
|
1752
|
-
` text "·" h4`,
|
|
1753
|
-
` text "{C3}" h4 color={ACCENT}`,
|
|
1754
|
-
`goal: Vision poster — {slug}`,
|
|
1755
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1756
|
-
``,
|
|
1757
|
-
`## Placeholder rules`,
|
|
1758
|
-
``,
|
|
1759
|
-
`- {slug}: short kebab-case slug for the life area`,
|
|
1760
|
-
`- {ACCENT}: hex color. Vary across posters: try #10B981, #7C3AED, #3B82F6, #F59E0B, #EC4899, #14B8A6`,
|
|
1761
|
-
`- {EMOJI}: a single emoji capturing the emotional core. Visceral, not literal.`,
|
|
1762
|
-
`- {HEADLINE}: 3-5 words, declarative, first-person if natural, periods welcome. The dream version, not the next checkpoint.`,
|
|
1763
|
-
`- {SUPPORT}: 6-12 words, one short sentence. Concrete supporting context in the user's actual language.`,
|
|
1764
|
-
`- {MOMENT} (Template A): 1-3 words, a time anchor — season, year, phase`,
|
|
1765
|
-
`- {PLACE} (Template B): 1-3 words, must be a real geographic place name (not a date)`,
|
|
1766
|
-
`- {C1}, {C2}, {C3} (Template C): 1-2 words each, concept beats from the user's vocabulary`,
|
|
1767
|
-
``,
|
|
1768
|
-
`## Variety rule`,
|
|
1769
|
-
``,
|
|
1770
|
-
`Across the 3-4 posters, use AT LEAST 2 different templates.`,
|
|
1771
|
-
``,
|
|
1772
|
-
`## Hard constraints`,
|
|
1773
|
-
``,
|
|
1774
|
-
`- Copy ONE of the 3 templates above exactly. Do not add components. Do not remove components. Do not change order.`,
|
|
1775
|
-
`- Use the user's actual language from the data — never invent.`,
|
|
1776
|
-
``,
|
|
1777
|
-
...rejectedDreams ? [
|
|
1778
|
-
`## Previously rejected dreams`,
|
|
1779
|
-
``,
|
|
1780
|
-
`The user has dismissed these past vision posters with the following reasons. Do NOT re-pick these dreams or reframe them with the same emphasis — the user already told you they were wrong.`,
|
|
1781
|
-
``,
|
|
1782
|
-
rejectedDreams,
|
|
1783
|
-
``
|
|
1784
|
-
] : [],
|
|
1785
|
-
`## USER DATA`,
|
|
1786
|
-
``,
|
|
1787
|
-
sections.slice(0, 12000),
|
|
1788
|
-
``,
|
|
1789
|
-
`## Output`,
|
|
1790
|
-
``,
|
|
1791
|
-
`Push each poster with agentlife_push. After the last push, respond "Done".`
|
|
1792
|
-
].join(`
|
|
1793
|
-
`);
|
|
1794
|
-
const params = JSON.stringify({
|
|
1795
|
-
sessionKey: builderKey,
|
|
1796
|
-
message: bootstrapMsg,
|
|
1797
|
-
idempotencyKey: `dashboard-bootstrap-${Date.now()}`
|
|
1798
|
-
});
|
|
1799
|
-
lastBootstrapSentAt = Date.now();
|
|
1800
|
-
const delayMs = options.delayMs ?? 0;
|
|
1801
|
-
const feedbackChars = rejectedDreams.length;
|
|
1802
|
-
const runBootstrap = () => {
|
|
1803
|
-
log(`[agentlife] ensureVisionPosters: synthesizing (${totalChars} chars memory across ${specialistCount} specialists${feedbackChars > 0 ? `, ${feedbackChars} chars prior feedback` : ""})`);
|
|
1804
|
-
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", params], { timeoutMs: 300000 }).then(() => {
|
|
1805
|
-
log("[agentlife] ensureVisionPosters: dashboard bootstrap complete");
|
|
1806
|
-
}).catch((e) => {
|
|
1807
|
-
log(`[agentlife] ensureVisionPosters: dashboard bootstrap failed: ${e?.message}`);
|
|
1808
|
-
});
|
|
1809
|
-
};
|
|
1810
|
-
if (delayMs > 0) {
|
|
1811
|
-
setTimeout(runBootstrap, delayMs);
|
|
1812
|
-
} else {
|
|
1813
|
-
runBootstrap();
|
|
1814
|
-
}
|
|
1815
|
-
return { status: "synthesizing", specialistCount, totalChars, feedbackChars };
|
|
1816
|
-
}
|
|
1817
1727
|
async function provisionAgents(state, cfg, runtime, log) {
|
|
1818
1728
|
const home = os.homedir();
|
|
1819
1729
|
const currentList = [...cfg.agents?.list ?? []];
|
|
@@ -1880,10 +1790,12 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1880
1790
|
log(`[agentlife] backfilled subagents for ${agent.id}`);
|
|
1881
1791
|
}
|
|
1882
1792
|
}
|
|
1883
|
-
const
|
|
1793
|
+
const configPath = path3.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
1794
|
+
const rawCfgForVisibility = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1795
|
+
const backupPath = path3.join(os.homedir(), ".openclaw", "agentlife", "config-backup.json");
|
|
1796
|
+
let visibilityWritten = false;
|
|
1884
1797
|
const currentVisibility = rawCfgForVisibility?.tools?.sessions?.visibility;
|
|
1885
1798
|
if (currentVisibility !== "all") {
|
|
1886
|
-
const backupPath = path3.join(os.homedir(), ".openclaw", "agentlife", "config-backup.json");
|
|
1887
1799
|
try {
|
|
1888
1800
|
let backup = {};
|
|
1889
1801
|
try {
|
|
@@ -1898,14 +1810,40 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1898
1810
|
if (!rawCfgForVisibility.tools.sessions)
|
|
1899
1811
|
rawCfgForVisibility.tools.sessions = {};
|
|
1900
1812
|
rawCfgForVisibility.tools.sessions.visibility = "all";
|
|
1901
|
-
|
|
1813
|
+
visibilityWritten = true;
|
|
1814
|
+
}
|
|
1815
|
+
const currentAllow = rawCfgForVisibility?.tools?.allow;
|
|
1816
|
+
let globalAllowWritten = false;
|
|
1817
|
+
if (Array.isArray(currentAllow) && currentAllow.length > 0 && !currentAllow.includes("*")) {
|
|
1818
|
+
const onlyPluginTool = currentAllow.length === 1 && currentAllow[0] === "agentlife_push";
|
|
1819
|
+
if (onlyPluginTool) {
|
|
1820
|
+
try {
|
|
1821
|
+
let backup = {};
|
|
1822
|
+
try {
|
|
1823
|
+
backup = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
1824
|
+
} catch {}
|
|
1825
|
+
if (backup.toolsAllow === undefined) {
|
|
1826
|
+
backup.toolsAllow = [...currentAllow];
|
|
1827
|
+
writeFileSync(backupPath, JSON.stringify(backup, null, 2) + `
|
|
1828
|
+
`, "utf-8");
|
|
1829
|
+
}
|
|
1830
|
+
} catch {}
|
|
1831
|
+
rawCfgForVisibility.tools.allow = ["*", "agentlife_push"];
|
|
1832
|
+
globalAllowWritten = true;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (visibilityWritten || globalAllowWritten) {
|
|
1836
|
+
writeFileSync(configPath, JSON.stringify(rawCfgForVisibility, null, 2) + `
|
|
1902
1837
|
`, "utf-8");
|
|
1903
1838
|
configChanged = true;
|
|
1904
|
-
|
|
1839
|
+
if (visibilityWritten)
|
|
1840
|
+
log("[agentlife] set tools.sessions.visibility=all (cross-agent delegation)");
|
|
1841
|
+
if (globalAllowWritten)
|
|
1842
|
+
log('[agentlife] added "*" to tools.allow (unblock exec/read/write for provisioned agents)');
|
|
1905
1843
|
}
|
|
1906
1844
|
if (configChanged) {
|
|
1907
|
-
const
|
|
1908
|
-
const rawCfg = JSON.parse(readFileSync(
|
|
1845
|
+
const configPath2 = path3.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
1846
|
+
const rawCfg = JSON.parse(readFileSync(configPath2, "utf-8"));
|
|
1909
1847
|
const updatedCfg = {
|
|
1910
1848
|
...rawCfg,
|
|
1911
1849
|
agents: {
|
|
@@ -1913,7 +1851,7 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1913
1851
|
list: currentList
|
|
1914
1852
|
}
|
|
1915
1853
|
};
|
|
1916
|
-
writeFileSync(
|
|
1854
|
+
writeFileSync(configPath2, JSON.stringify(updatedCfg, null, 2) + `
|
|
1917
1855
|
`, "utf-8");
|
|
1918
1856
|
log("[agentlife] config updated with provisioned agents");
|
|
1919
1857
|
}
|
|
@@ -1991,12 +1929,538 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1991
1929
|
}, 1e4);
|
|
1992
1930
|
}
|
|
1993
1931
|
}
|
|
1994
|
-
await ensureVisionPosters(state, runtime, log, { delayMs: 15000 });
|
|
1995
1932
|
log("[agentlife] agent provisioning complete");
|
|
1996
1933
|
}
|
|
1997
1934
|
|
|
1998
|
-
//
|
|
1935
|
+
// cold-start.ts
|
|
1936
|
+
import * as fs3 from "node:fs/promises";
|
|
1937
|
+
import * as nodeFs from "node:fs";
|
|
1938
|
+
import * as os2 from "node:os";
|
|
1999
1939
|
import * as path4 from "node:path";
|
|
1940
|
+
var DEADLINE_MS = {
|
|
1941
|
+
AWAITING_AGENT: 30 * 60 * 1000,
|
|
1942
|
+
AWAITING_BASELINE: 30 * 60 * 1000,
|
|
1943
|
+
SYNTHESIZING: 3 * 60 * 1000,
|
|
1944
|
+
AWAITING_DREAM: 30 * 60 * 1000
|
|
1945
|
+
};
|
|
1946
|
+
var VISION_MIN_MEMORY_CHARS = 200;
|
|
1947
|
+
var PHASE_BLANK = "BLANK";
|
|
1948
|
+
function readState(state) {
|
|
1949
|
+
const db = getOrCreateHistoryDb(state);
|
|
1950
|
+
const row = db.prepare("SELECT * FROM cold_start_state WHERE id = 1").get();
|
|
1951
|
+
if (!row)
|
|
1952
|
+
return null;
|
|
1953
|
+
let warnings = [];
|
|
1954
|
+
if (row.contractWarnings) {
|
|
1955
|
+
try {
|
|
1956
|
+
warnings = JSON.parse(row.contractWarnings);
|
|
1957
|
+
} catch {}
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
phase: row.phase,
|
|
1961
|
+
enteredAt: row.enteredAt,
|
|
1962
|
+
lastActionAt: row.lastActionAt ?? null,
|
|
1963
|
+
ackAt: row.ackAt ?? null,
|
|
1964
|
+
deadlineAt: row.deadlineAt ?? null,
|
|
1965
|
+
retryCount: row.retryCount ?? 0,
|
|
1966
|
+
failureReason: row.failureReason ?? null,
|
|
1967
|
+
lastTrigger: row.lastTrigger ?? null,
|
|
1968
|
+
actionSessionKey: row.actionSessionKey ?? null,
|
|
1969
|
+
contractWarnings: warnings,
|
|
1970
|
+
pollerLastTickAt: row.pollerLastTickAt ?? null,
|
|
1971
|
+
updatedAt: row.updatedAt
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function ensureInitialRow(state) {
|
|
1975
|
+
const existing = readState(state);
|
|
1976
|
+
if (existing)
|
|
1977
|
+
return existing;
|
|
1978
|
+
const now = Date.now();
|
|
1979
|
+
const db = getOrCreateHistoryDb(state);
|
|
1980
|
+
db.prepare(`
|
|
1981
|
+
INSERT OR IGNORE INTO cold_start_state
|
|
1982
|
+
(id, phase, enteredAt, retryCount, contractWarnings, updatedAt)
|
|
1983
|
+
VALUES (1, ?, ?, 0, '[]', ?)
|
|
1984
|
+
`).run(PHASE_BLANK, now, now);
|
|
1985
|
+
return readState(state);
|
|
1986
|
+
}
|
|
1987
|
+
function writeState(state, partial) {
|
|
1988
|
+
const current = ensureInitialRow(state);
|
|
1989
|
+
const next = { ...current, ...partial, updatedAt: Date.now() };
|
|
1990
|
+
const db = getOrCreateHistoryDb(state);
|
|
1991
|
+
db.prepare(`
|
|
1992
|
+
UPDATE cold_start_state SET
|
|
1993
|
+
phase = ?,
|
|
1994
|
+
enteredAt = ?,
|
|
1995
|
+
lastActionAt = ?,
|
|
1996
|
+
ackAt = ?,
|
|
1997
|
+
deadlineAt = ?,
|
|
1998
|
+
retryCount = ?,
|
|
1999
|
+
failureReason = ?,
|
|
2000
|
+
lastTrigger = ?,
|
|
2001
|
+
actionSessionKey = ?,
|
|
2002
|
+
contractWarnings = ?,
|
|
2003
|
+
pollerLastTickAt = ?,
|
|
2004
|
+
updatedAt = ?
|
|
2005
|
+
WHERE id = 1
|
|
2006
|
+
`).run(next.phase, next.enteredAt, next.lastActionAt, next.ackAt, next.deadlineAt, next.retryCount, next.failureReason, next.lastTrigger, next.actionSessionKey, JSON.stringify(next.contractWarnings ?? []), next.pollerLastTickAt, next.updatedAt);
|
|
2007
|
+
return next;
|
|
2008
|
+
}
|
|
2009
|
+
function emitTrigger(state, kind, payload) {
|
|
2010
|
+
if (state.disabled)
|
|
2011
|
+
return;
|
|
2012
|
+
try {
|
|
2013
|
+
const db = getOrCreateHistoryDb(state);
|
|
2014
|
+
db.prepare(`
|
|
2015
|
+
INSERT INTO cold_start_triggers (kind, payload, createdAt) VALUES (?, ?, ?)
|
|
2016
|
+
`).run(kind, JSON.stringify(payload), Date.now());
|
|
2017
|
+
} catch (e) {
|
|
2018
|
+
console.warn("[cold-start] emitTrigger failed:", e?.message);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function drainTriggers(state, limit = 50) {
|
|
2022
|
+
const db = getOrCreateHistoryDb(state);
|
|
2023
|
+
const rows = db.prepare("SELECT id, kind, payload FROM cold_start_triggers ORDER BY createdAt ASC LIMIT ?").all(limit);
|
|
2024
|
+
if (rows.length === 0)
|
|
2025
|
+
return [];
|
|
2026
|
+
const ids = rows.map((r) => r.id);
|
|
2027
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
2028
|
+
db.prepare(`DELETE FROM cold_start_triggers WHERE id IN (${placeholders})`).run(...ids);
|
|
2029
|
+
return rows.map((r) => ({
|
|
2030
|
+
kind: r.kind,
|
|
2031
|
+
payload: safeJson(r.payload)
|
|
2032
|
+
}));
|
|
2033
|
+
}
|
|
2034
|
+
function safeJson(s) {
|
|
2035
|
+
try {
|
|
2036
|
+
return JSON.parse(s);
|
|
2037
|
+
} catch {
|
|
2038
|
+
return {};
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function purgeStaleTriggers(state) {
|
|
2042
|
+
const db = getOrCreateHistoryDb(state);
|
|
2043
|
+
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
2044
|
+
const r = db.prepare("DELETE FROM cold_start_triggers WHERE createdAt < ?").run(cutoff);
|
|
2045
|
+
return r.changes;
|
|
2046
|
+
}
|
|
2047
|
+
function pendingTriggerCount(state) {
|
|
2048
|
+
const db = getOrCreateHistoryDb(state);
|
|
2049
|
+
const row = db.prepare("SELECT COUNT(*) as c FROM cold_start_triggers").get();
|
|
2050
|
+
return row?.c ?? 0;
|
|
2051
|
+
}
|
|
2052
|
+
var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
2053
|
+
function listSpecialistIds(state, runtime) {
|
|
2054
|
+
const cfg = runtime.config.loadConfig();
|
|
2055
|
+
const list = cfg?.agents?.list ?? [];
|
|
2056
|
+
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2057
|
+
}
|
|
2058
|
+
async function specialistMemoryTotal(state, runtime) {
|
|
2059
|
+
const cfg = runtime.config.loadConfig();
|
|
2060
|
+
const list = cfg?.agents?.list ?? [];
|
|
2061
|
+
const { totalChars, sections } = await gatherAllAgentMemory(state, list, PROVISIONED_IDS);
|
|
2062
|
+
return { total: totalChars, sections };
|
|
2063
|
+
}
|
|
2064
|
+
function visionSurfaceCount(state) {
|
|
2065
|
+
if (!state.surfaceDb)
|
|
2066
|
+
return 0;
|
|
2067
|
+
let n = 0;
|
|
2068
|
+
for (const id of state.surfaceDb.keys())
|
|
2069
|
+
if (id.startsWith("vision-"))
|
|
2070
|
+
n++;
|
|
2071
|
+
return n;
|
|
2072
|
+
}
|
|
2073
|
+
async function sendSystemMessage(state, agentId, message, idempotencyKey, log) {
|
|
2074
|
+
if (!state.runCommand) {
|
|
2075
|
+
log(`[cold-start] sendSystemMessage ${idempotencyKey}: skipped — runCommand not available`);
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
const sessionKey = buildAgentSessionKey(agentId);
|
|
2079
|
+
const chatParams = JSON.stringify({ sessionKey, message, idempotencyKey });
|
|
2080
|
+
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 120000 }).then(() => {
|
|
2081
|
+
log(`[cold-start] sent ${idempotencyKey} → ${sessionKey}`);
|
|
2082
|
+
}).catch((e) => {
|
|
2083
|
+
log(`[cold-start] sendSystemMessage ${idempotencyKey} failed: ${e?.message}`);
|
|
2084
|
+
});
|
|
2085
|
+
return sessionKey;
|
|
2086
|
+
}
|
|
2087
|
+
async function composeBootstrapMessage(state, runtime, userDream) {
|
|
2088
|
+
const { sections } = await specialistMemoryTotal(state, runtime);
|
|
2089
|
+
const rejected = await readRejectedDreams();
|
|
2090
|
+
const parts = ["[system:dashboard-bootstrap]", "", "## USER DATA", "", sections.slice(0, 12000)];
|
|
2091
|
+
if (rejected)
|
|
2092
|
+
parts.push("", "## Previously rejected dreams", "", rejected);
|
|
2093
|
+
if (userDream)
|
|
2094
|
+
parts.push("", "## userDream", "", userDream);
|
|
2095
|
+
return parts.join(`
|
|
2096
|
+
`);
|
|
2097
|
+
}
|
|
2098
|
+
async function readRejectedDreams() {
|
|
2099
|
+
const visionWorkspace = path4.join(os2.homedir(), ".openclaw", "workspace-agentlife-vision");
|
|
2100
|
+
const feedbackFile = path4.join(visionWorkspace, "memory", "vision-feedback.md");
|
|
2101
|
+
try {
|
|
2102
|
+
const content = await fs3.readFile(feedbackFile, "utf-8");
|
|
2103
|
+
return content.trim().slice(0, 4000);
|
|
2104
|
+
} catch {
|
|
2105
|
+
return "";
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async function computeStartingPhase(state, runtime) {
|
|
2109
|
+
if (visionSurfaceCount(state) > 0) {
|
|
2110
|
+
return { phase: "READY", details: "vision surfaces already exist" };
|
|
2111
|
+
}
|
|
2112
|
+
const specialists = listSpecialistIds(state, runtime);
|
|
2113
|
+
if (specialists.length === 0) {
|
|
2114
|
+
return { phase: "BLANK", details: "no specialists" };
|
|
2115
|
+
}
|
|
2116
|
+
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2117
|
+
if (total < VISION_MIN_MEMORY_CHARS) {
|
|
2118
|
+
return { phase: "AWAITING_BASELINE", details: `totalChars=${total} below ${VISION_MIN_MEMORY_CHARS}` };
|
|
2119
|
+
}
|
|
2120
|
+
return { phase: "SYNTHESIZING", details: `totalChars=${total} ready for synthesis` };
|
|
2121
|
+
}
|
|
2122
|
+
async function transition(state, runtime, log, trigger) {
|
|
2123
|
+
const cur = ensureInitialRow(state);
|
|
2124
|
+
log(`[cold-start] trigger=${trigger.kind} phase=${cur.phase}`);
|
|
2125
|
+
switch (trigger.kind) {
|
|
2126
|
+
case "provisioned":
|
|
2127
|
+
case "userRetried": {
|
|
2128
|
+
const { phase, details } = await computeStartingPhase(state, runtime);
|
|
2129
|
+
log(`[cold-start] recompute → ${phase} (${details})`);
|
|
2130
|
+
return enterPhase(state, runtime, log, phase, {
|
|
2131
|
+
bumpRetry: trigger.kind === "userRetried"
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
case "agentRegistered": {
|
|
2135
|
+
if (cur.phase === "BLANK" || cur.phase === "AWAITING_AGENT") {
|
|
2136
|
+
return enterPhase(state, runtime, log, "AWAITING_BASELINE", {
|
|
2137
|
+
specialistId: trigger.payload.agentId
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
return cur;
|
|
2141
|
+
}
|
|
2142
|
+
case "agentDeleted": {
|
|
2143
|
+
const remaining = trigger.payload.remaining;
|
|
2144
|
+
if (remaining === 0) {
|
|
2145
|
+
return enterPhase(state, runtime, log, "BLANK");
|
|
2146
|
+
}
|
|
2147
|
+
return cur;
|
|
2148
|
+
}
|
|
2149
|
+
case "memoryWritten": {
|
|
2150
|
+
if (cur.phase !== "AWAITING_BASELINE")
|
|
2151
|
+
return cur;
|
|
2152
|
+
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2153
|
+
if (total >= VISION_MIN_MEMORY_CHARS) {
|
|
2154
|
+
return enterPhase(state, runtime, log, "SYNTHESIZING");
|
|
2155
|
+
}
|
|
2156
|
+
log(`[cold-start] memory still thin: ${total}/${VISION_MIN_MEMORY_CHARS}`);
|
|
2157
|
+
return cur;
|
|
2158
|
+
}
|
|
2159
|
+
case "surfacePushed": {
|
|
2160
|
+
return cur;
|
|
2161
|
+
}
|
|
2162
|
+
case "agentReplied": {
|
|
2163
|
+
const sessionKey = trigger.payload.sessionKey;
|
|
2164
|
+
const marker = trigger.payload.marker;
|
|
2165
|
+
const lastText = trigger.payload.lastText ?? "";
|
|
2166
|
+
if (cur.actionSessionKey && sessionKey && sessionKey !== cur.actionSessionKey) {
|
|
2167
|
+
return cur;
|
|
2168
|
+
}
|
|
2169
|
+
if (cur.phase === "SYNTHESIZING") {
|
|
2170
|
+
if (marker === "NO_SIGNAL" || /\bNO_SIGNAL\b/.test(lastText)) {
|
|
2171
|
+
return enterPhase(state, runtime, log, "AWAITING_DREAM");
|
|
2172
|
+
}
|
|
2173
|
+
if (visionSurfaceCount(state) >= 1) {
|
|
2174
|
+
return enterPhase(state, runtime, log, "READY");
|
|
2175
|
+
}
|
|
2176
|
+
return cur;
|
|
2177
|
+
}
|
|
2178
|
+
if (cur.phase === "AWAITING_BASELINE") {
|
|
2179
|
+
return cur;
|
|
2180
|
+
}
|
|
2181
|
+
return cur;
|
|
2182
|
+
}
|
|
2183
|
+
case "userAnswered": {
|
|
2184
|
+
const surfaceId = trigger.payload.surfaceId;
|
|
2185
|
+
const answer = trigger.payload.answer ?? "";
|
|
2186
|
+
if (cur.phase === "AWAITING_DREAM" && surfaceId === "awaiting-dream-input") {
|
|
2187
|
+
return enterPhase(state, runtime, log, "SYNTHESIZING", { userDream: answer });
|
|
2188
|
+
}
|
|
2189
|
+
return cur;
|
|
2190
|
+
}
|
|
2191
|
+
case "deadlineElapsed": {
|
|
2192
|
+
const elapsedPhase = trigger.payload.phase;
|
|
2193
|
+
if (elapsedPhase !== cur.phase)
|
|
2194
|
+
return cur;
|
|
2195
|
+
const reason = `${cur.phase.toLowerCase()}_timeout`;
|
|
2196
|
+
return enterFailed(state, log, reason);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
async function enterPhase(state, runtime, log, next, opts = {}) {
|
|
2201
|
+
const cur = ensureInitialRow(state);
|
|
2202
|
+
const now = Date.now();
|
|
2203
|
+
const deadlineMs = DEADLINE_MS[next];
|
|
2204
|
+
const deadlineAt = deadlineMs ? now + deadlineMs : null;
|
|
2205
|
+
const retryCount = opts.bumpRetry ? cur.retryCount + 1 : cur.retryCount;
|
|
2206
|
+
let actionSessionKey = null;
|
|
2207
|
+
let lastActionAt = null;
|
|
2208
|
+
writeState(state, {
|
|
2209
|
+
phase: next,
|
|
2210
|
+
enteredAt: now,
|
|
2211
|
+
deadlineAt,
|
|
2212
|
+
retryCount,
|
|
2213
|
+
failureReason: next === "FAILED" ? cur.failureReason : null,
|
|
2214
|
+
actionSessionKey: null,
|
|
2215
|
+
lastActionAt: null,
|
|
2216
|
+
ackAt: null
|
|
2217
|
+
});
|
|
2218
|
+
log(`[cold-start] enter ${next} (deadline=${deadlineAt ? new Date(deadlineAt).toISOString() : "none"}, retry=${retryCount})`);
|
|
2219
|
+
if (next === "BLANK") {
|
|
2220
|
+
const idem = `cold-start-onboarding-${now}-${retryCount}`;
|
|
2221
|
+
const msg = [
|
|
2222
|
+
"[system:onboarding]",
|
|
2223
|
+
"",
|
|
2224
|
+
"User just installed AgentLife and has zero specialist agents. Push the",
|
|
2225
|
+
"onboarding-domain input surface EXACTLY as specified in the [system:onboarding]",
|
|
2226
|
+
"section of your AGENTS.md, then respond `done`. Do NOT ask the user questions",
|
|
2227
|
+
"as chat text — only via the input surface. Do NOT create any specialist agent",
|
|
2228
|
+
"yet; wait for the user's choice on the surface."
|
|
2229
|
+
].join(`
|
|
2230
|
+
`);
|
|
2231
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-builder", msg, idem, log);
|
|
2232
|
+
lastActionAt = Date.now();
|
|
2233
|
+
writeState(state, {
|
|
2234
|
+
phase: "AWAITING_AGENT",
|
|
2235
|
+
enteredAt: now,
|
|
2236
|
+
deadlineAt: now + (DEADLINE_MS.AWAITING_AGENT ?? 0),
|
|
2237
|
+
actionSessionKey,
|
|
2238
|
+
lastActionAt
|
|
2239
|
+
});
|
|
2240
|
+
scheduleDeadline(state, runtime, log, "AWAITING_AGENT", now + (DEADLINE_MS.AWAITING_AGENT ?? 0));
|
|
2241
|
+
return readState(state);
|
|
2242
|
+
}
|
|
2243
|
+
if (next === "AWAITING_BASELINE") {
|
|
2244
|
+
const specialistId = opts.specialistId ?? listSpecialistIds(state, runtime)[0];
|
|
2245
|
+
if (!specialistId) {
|
|
2246
|
+
log("[cold-start] AWAITING_BASELINE has no specialist to warm up — falling back to BLANK");
|
|
2247
|
+
return enterPhase(state, runtime, log, "BLANK");
|
|
2248
|
+
}
|
|
2249
|
+
const idem = `cold-start-start-${specialistId}-${now}-${retryCount}`;
|
|
2250
|
+
const msg = [
|
|
2251
|
+
"[system:start]",
|
|
2252
|
+
"",
|
|
2253
|
+
"You were just created by the builder. You have zero memory about this user.",
|
|
2254
|
+
"Follow the [system:start] section of your AGENTS.md: push a loading widget",
|
|
2255
|
+
"introducing yourself per IDENTITY.md, then push ONE warmup-* input surface",
|
|
2256
|
+
"asking the most important baseline question for your domain, then respond",
|
|
2257
|
+
"`done`. Do NOT ask multiple questions in chat text — only via input surfaces."
|
|
2258
|
+
].join(`
|
|
2259
|
+
`);
|
|
2260
|
+
actionSessionKey = await sendSystemMessage(state, specialistId, msg, idem, log);
|
|
2261
|
+
lastActionAt = Date.now();
|
|
2262
|
+
}
|
|
2263
|
+
if (next === "SYNTHESIZING") {
|
|
2264
|
+
const msg = await composeBootstrapMessage(state, runtime, opts.userDream);
|
|
2265
|
+
const idem = `cold-start-bootstrap-${now}-${retryCount}`;
|
|
2266
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-vision", msg, idem, log);
|
|
2267
|
+
lastActionAt = Date.now();
|
|
2268
|
+
}
|
|
2269
|
+
if (next === "AWAITING_DREAM") {
|
|
2270
|
+
const idem = `cold-start-ask-dream-${now}-${retryCount}`;
|
|
2271
|
+
const msg = [
|
|
2272
|
+
"[system:ask-dream]",
|
|
2273
|
+
"",
|
|
2274
|
+
"Bootstrap reported NO_SIGNAL — the user's data has no aspirational content yet.",
|
|
2275
|
+
"Push the awaiting-dream-input input surface EXACTLY as specified in the",
|
|
2276
|
+
"[system:ask-dream] section of your AGENTS.md, then respond `done`. Do NOT push",
|
|
2277
|
+
"any vision posters from this turn — the plugin re-fires [system:dashboard-bootstrap]",
|
|
2278
|
+
"with userDream: injected after the user answers."
|
|
2279
|
+
].join(`
|
|
2280
|
+
`);
|
|
2281
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-vision", msg, idem, log);
|
|
2282
|
+
lastActionAt = Date.now();
|
|
2283
|
+
}
|
|
2284
|
+
writeState(state, { actionSessionKey, lastActionAt });
|
|
2285
|
+
if (deadlineAt)
|
|
2286
|
+
scheduleDeadline(state, runtime, log, next, deadlineAt);
|
|
2287
|
+
return readState(state);
|
|
2288
|
+
}
|
|
2289
|
+
function enterFailed(state, log, reason) {
|
|
2290
|
+
log(`[cold-start] FAILED: ${reason}`);
|
|
2291
|
+
return writeState(state, {
|
|
2292
|
+
phase: "FAILED",
|
|
2293
|
+
enteredAt: Date.now(),
|
|
2294
|
+
failureReason: reason,
|
|
2295
|
+
deadlineAt: null,
|
|
2296
|
+
actionSessionKey: null
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
var deadlineTimers = new Map;
|
|
2300
|
+
function scheduleDeadline(state, runtime, log, phase, deadlineAt) {
|
|
2301
|
+
const key = `${phase}-${deadlineAt}`;
|
|
2302
|
+
for (const [k, t] of deadlineTimers.entries()) {
|
|
2303
|
+
if (k.startsWith(`${phase}-`)) {
|
|
2304
|
+
clearTimeout(t);
|
|
2305
|
+
deadlineTimers.delete(k);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const remaining = Math.max(0, deadlineAt - Date.now());
|
|
2309
|
+
const timer = setTimeout(() => {
|
|
2310
|
+
deadlineTimers.delete(key);
|
|
2311
|
+
emitTrigger(state, "deadlineElapsed", { phase });
|
|
2312
|
+
}, remaining);
|
|
2313
|
+
deadlineTimers.set(key, timer);
|
|
2314
|
+
log(`[cold-start] scheduled ${phase} deadline in ${Math.round(remaining / 1000)}s`);
|
|
2315
|
+
}
|
|
2316
|
+
function rearmDeadlines(state, runtime, log) {
|
|
2317
|
+
const cur = ensureInitialRow(state);
|
|
2318
|
+
if (cur.deadlineAt && cur.phase !== "READY" && cur.phase !== "FAILED") {
|
|
2319
|
+
if (cur.deadlineAt <= Date.now()) {
|
|
2320
|
+
emitTrigger(state, "deadlineElapsed", { phase: cur.phase });
|
|
2321
|
+
} else {
|
|
2322
|
+
scheduleDeadline(state, runtime, log, cur.phase, cur.deadlineAt);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
var pollerTimer = null;
|
|
2327
|
+
var POLLER_INTERVAL_MS = 1000;
|
|
2328
|
+
var POLLER_STUCK_THRESHOLD_MS = 30000;
|
|
2329
|
+
var machineRuntime = null;
|
|
2330
|
+
var machineLog = null;
|
|
2331
|
+
var machineState = null;
|
|
2332
|
+
function startPoller(state, runtime, log) {
|
|
2333
|
+
if (pollerTimer)
|
|
2334
|
+
return;
|
|
2335
|
+
let lastDrainAt = Date.now();
|
|
2336
|
+
let lastWarnedAt = 0;
|
|
2337
|
+
pollerTimer = setInterval(async () => {
|
|
2338
|
+
if (state.disabled)
|
|
2339
|
+
return;
|
|
2340
|
+
try {
|
|
2341
|
+
const triggers = drainTriggers(state, 50);
|
|
2342
|
+
const now = Date.now();
|
|
2343
|
+
writeState(state, { pollerLastTickAt: now });
|
|
2344
|
+
if (triggers.length > 0) {
|
|
2345
|
+
lastDrainAt = now;
|
|
2346
|
+
for (const trigger of triggers) {
|
|
2347
|
+
try {
|
|
2348
|
+
await transition(state, runtime, log, trigger);
|
|
2349
|
+
} catch (e) {
|
|
2350
|
+
log(`[cold-start] transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
purgeStaleTriggers(state);
|
|
2354
|
+
} else {
|
|
2355
|
+
const pending = pendingTriggerCount(state);
|
|
2356
|
+
if (pending > 0 && now - lastDrainAt > POLLER_STUCK_THRESHOLD_MS && now - lastWarnedAt > POLLER_STUCK_THRESHOLD_MS) {
|
|
2357
|
+
log(`[cold-start] WARNING: poller has not drained ${pending} pending trigger(s) for ${Math.round((now - lastDrainAt) / 1000)}s`);
|
|
2358
|
+
lastWarnedAt = now;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
} catch (e) {
|
|
2362
|
+
log(`[cold-start] poller tick error: ${e?.message ?? e}`);
|
|
2363
|
+
}
|
|
2364
|
+
}, POLLER_INTERVAL_MS);
|
|
2365
|
+
}
|
|
2366
|
+
function stopPoller() {
|
|
2367
|
+
if (pollerTimer) {
|
|
2368
|
+
clearInterval(pollerTimer);
|
|
2369
|
+
pollerTimer = null;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
function runRuntimeContractProbe(state, runtime, log) {
|
|
2373
|
+
const PLUGIN_REGISTERED_TOOLS = ["agentlife_push"];
|
|
2374
|
+
let list = [];
|
|
2375
|
+
try {
|
|
2376
|
+
const raw = nodeFs.readFileSync(path4.join(os2.homedir(), ".openclaw", "openclaw.json"), "utf-8");
|
|
2377
|
+
const parsed = JSON.parse(raw);
|
|
2378
|
+
list = parsed?.agents?.list ?? [];
|
|
2379
|
+
} catch {
|
|
2380
|
+
list = runtime.config.loadConfig()?.agents?.list ?? [];
|
|
2381
|
+
}
|
|
2382
|
+
const warnings = [];
|
|
2383
|
+
for (const provisioned of PROVISIONED_AGENTS) {
|
|
2384
|
+
if (provisioned.existingAgent)
|
|
2385
|
+
continue;
|
|
2386
|
+
const live = list.find((a) => a?.id === provisioned.id);
|
|
2387
|
+
const liveTools = live?.tools ?? {};
|
|
2388
|
+
const allow = Array.isArray(liveTools.allow) ? liveTools.allow : [];
|
|
2389
|
+
const alsoAllow = Array.isArray(liveTools.alsoAllow) ? liveTools.alsoAllow : [];
|
|
2390
|
+
const deny = Array.isArray(liveTools.deny) ? liveTools.deny : [];
|
|
2391
|
+
for (const tool of PLUGIN_REGISTERED_TOOLS) {
|
|
2392
|
+
const escaped = tool.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
2393
|
+
const re = new RegExp("`[^`]*\\b" + escaped + "\\b[^`]*`");
|
|
2394
|
+
if (!re.test(provisioned.agentsMd))
|
|
2395
|
+
continue;
|
|
2396
|
+
const granted = (allow.includes(tool) || alsoAllow.includes(tool)) && !deny.includes(tool);
|
|
2397
|
+
if (!granted) {
|
|
2398
|
+
const w = `precondition_tool_missing:${provisioned.id}:${tool}`;
|
|
2399
|
+
warnings.push(w);
|
|
2400
|
+
log(`[cold-start] contract warning: ${w}`);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return warnings;
|
|
2405
|
+
}
|
|
2406
|
+
async function initColdStartMachine(state, runtime, log) {
|
|
2407
|
+
machineState = state;
|
|
2408
|
+
machineRuntime = runtime;
|
|
2409
|
+
machineLog = log;
|
|
2410
|
+
ensureInitialRow(state);
|
|
2411
|
+
const warnings = runRuntimeContractProbe(state, runtime, log);
|
|
2412
|
+
writeState(state, { contractWarnings: warnings });
|
|
2413
|
+
rearmDeadlines(state, runtime, log);
|
|
2414
|
+
startPoller(state, runtime, log);
|
|
2415
|
+
const cur = readState(state);
|
|
2416
|
+
if (cur.phase === "READY" && visionSurfaceCount(state) === 0) {
|
|
2417
|
+
log("[cold-start] READY with 0 vision surfaces — demoting to FAILED(ready_no_surfaces) on init");
|
|
2418
|
+
enterFailed(state, log, "ready_no_surfaces");
|
|
2419
|
+
log("[cold-start] machine initialized");
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (cur.phase !== "BLANK") {
|
|
2423
|
+
log(`[cold-start] restart recovery — preserving persisted phase=${cur.phase} (no provisioned re-fire)`);
|
|
2424
|
+
log("[cold-start] machine initialized");
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
emitTrigger(state, "provisioned", { source: "init" });
|
|
2428
|
+
log("[cold-start] machine initialized");
|
|
2429
|
+
}
|
|
2430
|
+
function shutdownColdStartMachine() {
|
|
2431
|
+
stopPoller();
|
|
2432
|
+
for (const t of deadlineTimers.values())
|
|
2433
|
+
clearTimeout(t);
|
|
2434
|
+
deadlineTimers.clear();
|
|
2435
|
+
}
|
|
2436
|
+
function observeColdStart(state) {
|
|
2437
|
+
const s = ensureInitialRow(state);
|
|
2438
|
+
return {
|
|
2439
|
+
phase: s.phase,
|
|
2440
|
+
failureReason: s.failureReason,
|
|
2441
|
+
retryCount: s.retryCount,
|
|
2442
|
+
contractWarnings: s.contractWarnings,
|
|
2443
|
+
enteredAt: s.enteredAt,
|
|
2444
|
+
deadlineAt: s.deadlineAt
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
async function retryColdStart(state) {
|
|
2448
|
+
emitTrigger(state, "userRetried", { ts: Date.now() });
|
|
2449
|
+
if (machineRuntime && machineLog) {
|
|
2450
|
+
const triggers = drainTriggers(state, 50);
|
|
2451
|
+
for (const trigger of triggers) {
|
|
2452
|
+
try {
|
|
2453
|
+
await transition(state, machineRuntime, machineLog, trigger);
|
|
2454
|
+
} catch (e) {
|
|
2455
|
+
machineLog(`[cold-start] retry-drain transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return observeColdStart(state);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// services/surfaces-init.ts
|
|
2463
|
+
import * as path5 from "node:path";
|
|
2000
2464
|
|
|
2001
2465
|
// activity.ts
|
|
2002
2466
|
function recordSurfaceEvent(state, surfaceId, event, dsl, agentId, metadata) {
|
|
@@ -2094,8 +2558,8 @@ function broadcastInput(message, sessionKey) {
|
|
|
2094
2558
|
}
|
|
2095
2559
|
|
|
2096
2560
|
// dashboard-state.ts
|
|
2097
|
-
import { readFileSync as
|
|
2098
|
-
import { homedir as
|
|
2561
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2562
|
+
import { homedir as homedir3 } from "node:os";
|
|
2099
2563
|
function extractTitleAndDetail(meta) {
|
|
2100
2564
|
let title = null;
|
|
2101
2565
|
let detail = null;
|
|
@@ -2308,9 +2772,9 @@ ${enhanced.formatted}`;
|
|
|
2308
2772
|
const agentFiles = [];
|
|
2309
2773
|
for (const w of warnings) {
|
|
2310
2774
|
const aid = w.agentId;
|
|
2311
|
-
const filePath = `${
|
|
2775
|
+
const filePath = `${homedir3()}/.openclaw/workspace-${aid}/AGENTS.md`;
|
|
2312
2776
|
try {
|
|
2313
|
-
const content =
|
|
2777
|
+
const content = readFileSync3(filePath, "utf-8").slice(0, 3000);
|
|
2314
2778
|
agentFiles.push(`### ${aid} AGENTS.md (${w.cnt} warnings)
|
|
2315
2779
|
${content}`);
|
|
2316
2780
|
} catch {}
|
|
@@ -2843,6 +3307,22 @@ function processDslBlock(state, dsl) {
|
|
|
2843
3307
|
missingCardStructure: validation.missingCardStructure ? true : undefined
|
|
2844
3308
|
});
|
|
2845
3309
|
recordSurfaceEvent(state, sid, existing ? "updated" : "created", block, state.surfaceDb.getAgentId(sid));
|
|
3310
|
+
if (!existing) {
|
|
3311
|
+
let kind = null;
|
|
3312
|
+
if (sid.startsWith("vision-"))
|
|
3313
|
+
kind = "vision";
|
|
3314
|
+
else if (sid === "awaiting-dream-input")
|
|
3315
|
+
kind = "dream-input";
|
|
3316
|
+
else if (sid === "onboarding-domain")
|
|
3317
|
+
kind = "onboarding";
|
|
3318
|
+
if (kind) {
|
|
3319
|
+
try {
|
|
3320
|
+
emitTrigger(state, "surfacePushed", { surfaceId: sid, kind, agentId: state.surfaceDb.getAgentId(sid) ?? null });
|
|
3321
|
+
} catch (e) {
|
|
3322
|
+
console.warn("[agentlife] cold-start emit surfacePushed failed: %s", e?.message);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
2846
3326
|
}
|
|
2847
3327
|
return results;
|
|
2848
3328
|
}
|
|
@@ -2986,23 +3466,27 @@ function registerSurfacesService(api, state) {
|
|
|
2986
3466
|
api.registerService({
|
|
2987
3467
|
id: "agentlife-surfaces",
|
|
2988
3468
|
start: async (ctx) => {
|
|
2989
|
-
const agentlifeDir =
|
|
3469
|
+
const agentlifeDir = path5.join(ctx.stateDir, "agentlife");
|
|
2990
3470
|
state.agentlifeStateDir = agentlifeDir;
|
|
2991
|
-
state.registryFilePath =
|
|
2992
|
-
state.dbBaseDir =
|
|
2993
|
-
state.historyDbPath =
|
|
3471
|
+
state.registryFilePath = path5.join(agentlifeDir, "agent-registry.json");
|
|
3472
|
+
state.dbBaseDir = path5.join(agentlifeDir, "db");
|
|
3473
|
+
state.historyDbPath = path5.join(agentlifeDir, "agentlife.db");
|
|
2994
3474
|
if (!state.surfaceDb) {
|
|
2995
3475
|
const db = getOrCreateHistoryDb(state);
|
|
2996
3476
|
state.surfaceDb = new SurfaceDb(db);
|
|
2997
3477
|
}
|
|
2998
3478
|
runStartupPurge(state);
|
|
3479
|
+
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
3480
|
+
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
2999
3481
|
let inputPurged = 0;
|
|
3000
3482
|
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
3001
3483
|
const headerLine = meta.lines[0] ?? "";
|
|
3002
|
-
if (
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3484
|
+
if (!/\binput\b/.test(headerLine))
|
|
3485
|
+
continue;
|
|
3486
|
+
if (isFlowInput(surfaceId))
|
|
3487
|
+
continue;
|
|
3488
|
+
state.surfaceDb.delete(surfaceId);
|
|
3489
|
+
inputPurged++;
|
|
3006
3490
|
}
|
|
3007
3491
|
if (inputPurged > 0) {
|
|
3008
3492
|
console.log("[agentlife] purged %d stale input surfaces on startup", inputPurged);
|
|
@@ -3055,36 +3539,40 @@ function registerSurfacesService(api, state) {
|
|
|
3055
3539
|
}
|
|
3056
3540
|
|
|
3057
3541
|
// services/config-optimizer.ts
|
|
3058
|
-
import * as
|
|
3059
|
-
import * as
|
|
3060
|
-
import * as
|
|
3542
|
+
import * as fs4 from "node:fs/promises";
|
|
3543
|
+
import * as path6 from "node:path";
|
|
3544
|
+
import * as os3 from "node:os";
|
|
3061
3545
|
var TARGET_MAX_PING_PONG = 1;
|
|
3062
3546
|
var TARGET_MAX_CONCURRENT = 10;
|
|
3063
|
-
var CONFIG_PATH =
|
|
3547
|
+
var CONFIG_PATH = path6.join(os3.homedir(), ".openclaw", "openclaw.json");
|
|
3064
3548
|
async function readRawConfigFromDisk() {
|
|
3065
|
-
const raw = await
|
|
3549
|
+
const raw = await fs4.readFile(CONFIG_PATH, "utf-8");
|
|
3066
3550
|
return JSON.parse(raw);
|
|
3067
3551
|
}
|
|
3068
3552
|
async function writeRawConfigToDisk(cfg) {
|
|
3069
|
-
await
|
|
3553
|
+
await fs4.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2) + `
|
|
3070
3554
|
`, "utf-8");
|
|
3071
3555
|
}
|
|
3072
3556
|
async function restoreConfigFromBackup(api, backupPath) {
|
|
3073
|
-
const raw = await
|
|
3557
|
+
const raw = await fs4.readFile(backupPath, "utf-8");
|
|
3074
3558
|
const backup = JSON.parse(raw);
|
|
3075
3559
|
const fileCfg = await readRawConfigFromDisk();
|
|
3076
3560
|
if (backup.maxPingPongTurns != null) {
|
|
3077
3561
|
fileCfg.session ??= {};
|
|
3078
3562
|
fileCfg.session.agentToAgent ??= {};
|
|
3079
3563
|
fileCfg.session.agentToAgent.maxPingPongTurns = backup.maxPingPongTurns;
|
|
3564
|
+
} else {
|
|
3565
|
+
delete fileCfg.session?.agentToAgent?.maxPingPongTurns;
|
|
3080
3566
|
}
|
|
3081
3567
|
if (backup.maxConcurrent != null) {
|
|
3082
3568
|
fileCfg.agents ??= {};
|
|
3083
3569
|
fileCfg.agents.defaults ??= {};
|
|
3084
3570
|
fileCfg.agents.defaults.maxConcurrent = backup.maxConcurrent;
|
|
3571
|
+
} else {
|
|
3572
|
+
delete fileCfg.agents?.defaults?.maxConcurrent;
|
|
3085
3573
|
}
|
|
3086
3574
|
await writeRawConfigToDisk(fileCfg);
|
|
3087
|
-
await
|
|
3575
|
+
await fs4.unlink(backupPath);
|
|
3088
3576
|
console.log("[agentlife] restored config: maxPingPongTurns=%s, maxConcurrent=%s", backup.maxPingPongTurns ?? "default", backup.maxConcurrent ?? "default");
|
|
3089
3577
|
return `config restored`;
|
|
3090
3578
|
}
|
|
@@ -3092,8 +3580,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3092
3580
|
api.registerService({
|
|
3093
3581
|
id: "agentlife-config-optimizer",
|
|
3094
3582
|
start: async (ctx) => {
|
|
3095
|
-
const configBackupDir =
|
|
3096
|
-
const configBackupPath =
|
|
3583
|
+
const configBackupDir = path6.join(ctx.stateDir, "agentlife");
|
|
3584
|
+
const configBackupPath = path6.join(configBackupDir, "config-backup.json");
|
|
3097
3585
|
const cfg = api.runtime.config.loadConfig();
|
|
3098
3586
|
const currentPPT = cfg.session?.agentToAgent?.maxPingPongTurns;
|
|
3099
3587
|
const currentMC = cfg.agents?.defaults?.maxConcurrent;
|
|
@@ -3103,8 +3591,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3103
3591
|
console.log("[agentlife] config already optimized, skipping");
|
|
3104
3592
|
return;
|
|
3105
3593
|
}
|
|
3106
|
-
await
|
|
3107
|
-
await
|
|
3594
|
+
await fs4.mkdir(configBackupDir, { recursive: true });
|
|
3595
|
+
await fs4.writeFile(configBackupPath, JSON.stringify({
|
|
3108
3596
|
maxPingPongTurns: currentPPT ?? null,
|
|
3109
3597
|
maxConcurrent: currentMC ?? null
|
|
3110
3598
|
}));
|
|
@@ -3129,7 +3617,7 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3129
3617
|
},
|
|
3130
3618
|
stop: async (ctx) => {
|
|
3131
3619
|
try {
|
|
3132
|
-
const backupPath =
|
|
3620
|
+
const backupPath = path6.join(ctx.stateDir, "agentlife", "config-backup.json");
|
|
3133
3621
|
await restoreConfigFromBackup(api, backupPath);
|
|
3134
3622
|
} catch {}
|
|
3135
3623
|
}
|
|
@@ -3580,8 +4068,8 @@ function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
|
|
|
3580
4068
|
}
|
|
3581
4069
|
|
|
3582
4070
|
// notifications.ts
|
|
3583
|
-
import * as
|
|
3584
|
-
import * as
|
|
4071
|
+
import * as fs5 from "node:fs";
|
|
4072
|
+
import * as path7 from "node:path";
|
|
3585
4073
|
var config;
|
|
3586
4074
|
var recentNotifications = new Map;
|
|
3587
4075
|
var DEBOUNCE_MS = 60000;
|
|
@@ -3589,8 +4077,8 @@ function loadConfig() {
|
|
|
3589
4077
|
if (config !== undefined)
|
|
3590
4078
|
return config;
|
|
3591
4079
|
try {
|
|
3592
|
-
const configPath =
|
|
3593
|
-
const raw =
|
|
4080
|
+
const configPath = path7.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
4081
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
3594
4082
|
const parsed = JSON.parse(raw);
|
|
3595
4083
|
if (parsed.serverUrl && parsed.apiKey) {
|
|
3596
4084
|
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
@@ -3755,7 +4243,7 @@ function sendToInternalSession(state, agentId, message, idempotencyKey) {
|
|
|
3755
4243
|
});
|
|
3756
4244
|
}
|
|
3757
4245
|
}
|
|
3758
|
-
var SKIP_WIDGET_CHECK = new Set(["agentlife", "agentlife-builder", "supervisor"]);
|
|
4246
|
+
var SKIP_WIDGET_CHECK = new Set(["agentlife", "agentlife-builder", "agentlife-vision", "supervisor"]);
|
|
3759
4247
|
function registerActivityHooks(api, state) {
|
|
3760
4248
|
api.on("llm_output", (event, ctx) => {
|
|
3761
4249
|
if (state.disabled)
|
|
@@ -3821,6 +4309,17 @@ function registerActivityHooks(api, state) {
|
|
|
3821
4309
|
console.log("[agentlife] action redirected to %s for %s (origin=%s)", actionSessionKey, actionSurfaceId, origin ? "set" : "legacy");
|
|
3822
4310
|
}
|
|
3823
4311
|
}
|
|
4312
|
+
const dreamMatch = message.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
4313
|
+
if (dreamMatch) {
|
|
4314
|
+
const labelMatch = message.match(/\blabel=([^\n]+)/);
|
|
4315
|
+
const valueMatch = message.match(/\bvalue=([^\n]+)/);
|
|
4316
|
+
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
4317
|
+
try {
|
|
4318
|
+
emitTrigger(state, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
4319
|
+
} catch (e) {
|
|
4320
|
+
console.warn("[agentlife] cold-start emit userAnswered failed: %s", e?.message);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
3824
4323
|
});
|
|
3825
4324
|
api.on("llm_output", (event, ctx) => {
|
|
3826
4325
|
if (state.disabled)
|
|
@@ -3853,6 +4352,39 @@ function registerActivityHooks(api, state) {
|
|
|
3853
4352
|
})
|
|
3854
4353
|
});
|
|
3855
4354
|
}
|
|
4355
|
+
let isCurrentColdStartSession = false;
|
|
4356
|
+
if (agentId) {
|
|
4357
|
+
try {
|
|
4358
|
+
const cs = readState(state);
|
|
4359
|
+
if (cs && cs.actionSessionKey === sessionKey && cs.phase !== "READY" && cs.phase !== "FAILED") {
|
|
4360
|
+
isCurrentColdStartSession = true;
|
|
4361
|
+
}
|
|
4362
|
+
} catch {}
|
|
4363
|
+
}
|
|
4364
|
+
if (isCurrentColdStartSession && agentId) {
|
|
4365
|
+
const lastText = texts.length > 0 ? texts[texts.length - 1].trim() : "";
|
|
4366
|
+
const lastLine = lastText.split(`
|
|
4367
|
+
`).pop()?.trim() ?? "";
|
|
4368
|
+
let marker = null;
|
|
4369
|
+
if (/\bNO_SIGNAL\b/.test(lastText)) {
|
|
4370
|
+
marker = "NO_SIGNAL";
|
|
4371
|
+
} else if (TERMINAL_MARKERS.has(lastLine)) {
|
|
4372
|
+
marker = lastLine;
|
|
4373
|
+
} else if (TERMINAL_MARKERS.has(lastText)) {
|
|
4374
|
+
marker = lastText;
|
|
4375
|
+
}
|
|
4376
|
+
try {
|
|
4377
|
+
emitTrigger(state, "agentReplied", {
|
|
4378
|
+
agentId,
|
|
4379
|
+
sessionKey,
|
|
4380
|
+
marker,
|
|
4381
|
+
lastText: lastText.slice(0, 4000),
|
|
4382
|
+
terminal: marker !== null
|
|
4383
|
+
});
|
|
4384
|
+
} catch (e) {
|
|
4385
|
+
console.warn("[agentlife] cold-start emit agentReplied failed: %s", e?.message);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
3856
4388
|
}, { priority: 100 });
|
|
3857
4389
|
api.on("before_tool_call", (event, ctx) => {
|
|
3858
4390
|
if (state.disabled)
|
|
@@ -3897,6 +4429,26 @@ function registerActivityHooks(api, state) {
|
|
|
3897
4429
|
if (event.toolName === "agentlife_push" && typeof event.params?.dsl === "string") {
|
|
3898
4430
|
canvasDsl = event.params.dsl;
|
|
3899
4431
|
}
|
|
4432
|
+
if (!isError && agentId) {
|
|
4433
|
+
let touchedMemory = false;
|
|
4434
|
+
if (toolName === "exec") {
|
|
4435
|
+
const command = event.params?.command ?? event.params?.script ?? "";
|
|
4436
|
+
if (typeof command === "string" && /memory\/[\w.\-]+\.md/.test(command) && /(>>?|tee\b|cp\b|mv\b)/.test(command)) {
|
|
4437
|
+
touchedMemory = true;
|
|
4438
|
+
}
|
|
4439
|
+
} else if (toolName === "write" || toolName === "edit" || toolName === "str_replace" || toolName === "apply_patch") {
|
|
4440
|
+
const filePath = event.params?.path ?? event.params?.file_path ?? "";
|
|
4441
|
+
if (/memory\/[\w.\-]+\.md$/.test(filePath))
|
|
4442
|
+
touchedMemory = true;
|
|
4443
|
+
}
|
|
4444
|
+
if (touchedMemory) {
|
|
4445
|
+
try {
|
|
4446
|
+
emitTrigger(state, "memoryWritten", { agentId, sessionKey });
|
|
4447
|
+
} catch (e) {
|
|
4448
|
+
console.warn("[agentlife] cold-start emit memoryWritten failed: %s", e?.message);
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
3900
4452
|
const resultStr = typeof event.result === "string" ? event.result : event.result != null ? JSON.stringify(event.result) : null;
|
|
3901
4453
|
recordActivity(state, "tool_end", sessionKey, agentId, {
|
|
3902
4454
|
toolName,
|
|
@@ -4682,31 +5234,31 @@ function startDailySweepService(state) {
|
|
|
4682
5234
|
import * as crypto4 from "node:crypto";
|
|
4683
5235
|
import * as fsSync3 from "node:fs";
|
|
4684
5236
|
import { createRequire as createRequire3 } from "node:module";
|
|
4685
|
-
import * as
|
|
4686
|
-
import * as
|
|
5237
|
+
import * as os7 from "node:os";
|
|
5238
|
+
import * as path11 from "node:path";
|
|
4687
5239
|
|
|
4688
5240
|
// gateway/web-app.ts
|
|
4689
5241
|
import * as crypto3 from "node:crypto";
|
|
4690
|
-
import * as
|
|
4691
|
-
import * as
|
|
4692
|
-
import * as
|
|
5242
|
+
import * as os6 from "node:os";
|
|
5243
|
+
import * as path10 from "node:path";
|
|
5244
|
+
import * as fs8 from "node:fs";
|
|
4693
5245
|
|
|
4694
5246
|
// services/pairing-access-token.ts
|
|
4695
5247
|
import * as crypto from "node:crypto";
|
|
4696
|
-
import * as
|
|
4697
|
-
import * as
|
|
4698
|
-
import * as
|
|
5248
|
+
import * as fs6 from "node:fs";
|
|
5249
|
+
import * as os4 from "node:os";
|
|
5250
|
+
import * as path8 from "node:path";
|
|
4699
5251
|
var cachedToken = null;
|
|
4700
5252
|
function pairingAccessPath() {
|
|
4701
|
-
return
|
|
5253
|
+
return path8.join(os4.homedir(), ".openclaw", "agentlife", "pairing-access.json");
|
|
4702
5254
|
}
|
|
4703
5255
|
function loadOrCreatePairingAccessToken() {
|
|
4704
5256
|
if (cachedToken)
|
|
4705
5257
|
return cachedToken;
|
|
4706
5258
|
const filePath = pairingAccessPath();
|
|
4707
5259
|
try {
|
|
4708
|
-
if (
|
|
4709
|
-
const raw =
|
|
5260
|
+
if (fs6.existsSync(filePath)) {
|
|
5261
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
4710
5262
|
const obj = JSON.parse(raw);
|
|
4711
5263
|
const token2 = typeof obj?.token === "string" ? obj.token : null;
|
|
4712
5264
|
if (token2 && token2.length >= 32) {
|
|
@@ -4715,12 +5267,12 @@ function loadOrCreatePairingAccessToken() {
|
|
|
4715
5267
|
}
|
|
4716
5268
|
}
|
|
4717
5269
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
4718
|
-
const dir =
|
|
4719
|
-
if (!
|
|
4720
|
-
|
|
4721
|
-
|
|
5270
|
+
const dir = path8.dirname(filePath);
|
|
5271
|
+
if (!fs6.existsSync(dir))
|
|
5272
|
+
fs6.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5273
|
+
fs6.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
|
|
4722
5274
|
try {
|
|
4723
|
-
|
|
5275
|
+
fs6.chmodSync(filePath, 384);
|
|
4724
5276
|
} catch {}
|
|
4725
5277
|
cachedToken = token;
|
|
4726
5278
|
return token;
|
|
@@ -4733,15 +5285,15 @@ function loadOrCreatePairingAccessToken() {
|
|
|
4733
5285
|
// services/cloudflared-supervisor.ts
|
|
4734
5286
|
import { spawn, spawnSync } from "node:child_process";
|
|
4735
5287
|
import * as crypto2 from "node:crypto";
|
|
4736
|
-
import * as
|
|
4737
|
-
import * as
|
|
4738
|
-
import * as
|
|
4739
|
-
var AGENTLIFE_DIR =
|
|
4740
|
-
var DEVICE_FILE =
|
|
4741
|
-
var TUNNEL_FILE =
|
|
4742
|
-
var BIN_DIR =
|
|
4743
|
-
var CLOUDFLARED_BIN =
|
|
4744
|
-
var CLOUDFLARED_PATH =
|
|
5288
|
+
import * as fs7 from "node:fs";
|
|
5289
|
+
import * as os5 from "node:os";
|
|
5290
|
+
import * as path9 from "node:path";
|
|
5291
|
+
var AGENTLIFE_DIR = path9.join(os5.homedir(), ".openclaw", "agentlife");
|
|
5292
|
+
var DEVICE_FILE = path9.join(AGENTLIFE_DIR, "device.json");
|
|
5293
|
+
var TUNNEL_FILE = path9.join(AGENTLIFE_DIR, "tunnel.json");
|
|
5294
|
+
var BIN_DIR = path9.join(AGENTLIFE_DIR, "bin");
|
|
5295
|
+
var CLOUDFLARED_BIN = os5.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
5296
|
+
var CLOUDFLARED_PATH = path9.join(BIN_DIR, CLOUDFLARED_BIN);
|
|
4745
5297
|
var API_BASE = process.env.AGENTLIFE_API_BASE || "https://api.agentlife.app";
|
|
4746
5298
|
var RESTART_DELAY_MS = 5000;
|
|
4747
5299
|
var STABLE_RUNTIME_MS = 60000;
|
|
@@ -4818,16 +5370,16 @@ async function doBootstrap() {
|
|
|
4818
5370
|
return tunnelInfo;
|
|
4819
5371
|
}
|
|
4820
5372
|
function ensureDirs() {
|
|
4821
|
-
if (!
|
|
4822
|
-
|
|
4823
|
-
if (!
|
|
4824
|
-
|
|
5373
|
+
if (!fs7.existsSync(AGENTLIFE_DIR))
|
|
5374
|
+
fs7.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
5375
|
+
if (!fs7.existsSync(BIN_DIR))
|
|
5376
|
+
fs7.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
|
|
4825
5377
|
}
|
|
4826
5378
|
function loadDeviceIdentity() {
|
|
4827
|
-
if (!
|
|
5379
|
+
if (!fs7.existsSync(DEVICE_FILE))
|
|
4828
5380
|
return null;
|
|
4829
5381
|
try {
|
|
4830
|
-
const raw =
|
|
5382
|
+
const raw = fs7.readFileSync(DEVICE_FILE, "utf-8");
|
|
4831
5383
|
const parsed = JSON.parse(raw);
|
|
4832
5384
|
if (typeof parsed.deviceId === "string" && typeof parsed.deviceSecret === "string") {
|
|
4833
5385
|
return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
|
|
@@ -4845,19 +5397,19 @@ function loadOrCreateDeviceIdentity() {
|
|
|
4845
5397
|
deviceId: crypto2.randomUUID(),
|
|
4846
5398
|
deviceSecret: crypto2.randomBytes(32).toString("base64url")
|
|
4847
5399
|
};
|
|
4848
|
-
|
|
5400
|
+
fs7.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
4849
5401
|
try {
|
|
4850
|
-
|
|
5402
|
+
fs7.chmodSync(DEVICE_FILE, 384);
|
|
4851
5403
|
} catch {}
|
|
4852
5404
|
console.log(`[cloudflared-supervisor] generated new device identity (deviceId=${identity.deviceId})`);
|
|
4853
5405
|
return identity;
|
|
4854
5406
|
}
|
|
4855
5407
|
var AGENTLIFE_API_BASE = API_BASE;
|
|
4856
5408
|
function loadCachedTunnel() {
|
|
4857
|
-
if (!
|
|
5409
|
+
if (!fs7.existsSync(TUNNEL_FILE))
|
|
4858
5410
|
return null;
|
|
4859
5411
|
try {
|
|
4860
|
-
const raw =
|
|
5412
|
+
const raw = fs7.readFileSync(TUNNEL_FILE, "utf-8");
|
|
4861
5413
|
const parsed = JSON.parse(raw);
|
|
4862
5414
|
if (typeof parsed.subdomain === "string" && typeof parsed.hostname === "string" && typeof parsed.tunnelUrl === "string" && typeof parsed.tunnelToken === "string" && typeof parsed.provisionedAt === "number") {
|
|
4863
5415
|
return parsed;
|
|
@@ -4866,9 +5418,9 @@ function loadCachedTunnel() {
|
|
|
4866
5418
|
return null;
|
|
4867
5419
|
}
|
|
4868
5420
|
function persistTunnel(info) {
|
|
4869
|
-
|
|
5421
|
+
fs7.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
|
|
4870
5422
|
try {
|
|
4871
|
-
|
|
5423
|
+
fs7.chmodSync(TUNNEL_FILE, 384);
|
|
4872
5424
|
} catch {}
|
|
4873
5425
|
}
|
|
4874
5426
|
async function provisionTunnel(identity) {
|
|
@@ -4905,14 +5457,14 @@ async function provisionTunnel(identity) {
|
|
|
4905
5457
|
}
|
|
4906
5458
|
}
|
|
4907
5459
|
function ensureCloudflaredBinary() {
|
|
4908
|
-
if (
|
|
5460
|
+
if (fs7.existsSync(CLOUDFLARED_PATH)) {
|
|
4909
5461
|
try {
|
|
4910
|
-
|
|
5462
|
+
fs7.accessSync(CLOUDFLARED_PATH, fs7.constants.X_OK);
|
|
4911
5463
|
return CLOUDFLARED_PATH;
|
|
4912
5464
|
} catch {}
|
|
4913
5465
|
}
|
|
4914
|
-
const platform2 =
|
|
4915
|
-
const arch2 =
|
|
5466
|
+
const platform2 = os5.platform();
|
|
5467
|
+
const arch2 = os5.arch();
|
|
4916
5468
|
const release = detectCloudflaredRelease(platform2, arch2);
|
|
4917
5469
|
if (!release) {
|
|
4918
5470
|
console.warn(`[cloudflared-supervisor] unsupported platform: ${platform2}/${arch2}`);
|
|
@@ -4925,28 +5477,28 @@ function ensureCloudflaredBinary() {
|
|
|
4925
5477
|
if (release.kind === "tgz") {
|
|
4926
5478
|
const extracted = extractTgzCloudflared(release.tempPath, BIN_DIR);
|
|
4927
5479
|
try {
|
|
4928
|
-
|
|
5480
|
+
fs7.unlinkSync(release.tempPath);
|
|
4929
5481
|
} catch {}
|
|
4930
5482
|
if (!extracted)
|
|
4931
5483
|
return null;
|
|
4932
5484
|
} else {
|
|
4933
5485
|
try {
|
|
4934
|
-
|
|
5486
|
+
fs7.renameSync(release.tempPath, CLOUDFLARED_PATH);
|
|
4935
5487
|
} catch (err) {
|
|
4936
5488
|
console.warn(`[cloudflared-supervisor] rename failed: ${err?.message}`);
|
|
4937
5489
|
return null;
|
|
4938
5490
|
}
|
|
4939
5491
|
}
|
|
4940
5492
|
try {
|
|
4941
|
-
|
|
5493
|
+
fs7.chmodSync(CLOUDFLARED_PATH, 493);
|
|
4942
5494
|
} catch {}
|
|
4943
|
-
return
|
|
5495
|
+
return fs7.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
|
|
4944
5496
|
}
|
|
4945
5497
|
function detectCloudflaredRelease(platform2, arch2) {
|
|
4946
5498
|
const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
4947
5499
|
if (platform2 === "darwin") {
|
|
4948
5500
|
const asset = arch2 === "arm64" ? "cloudflared-darwin-arm64.tgz" : "cloudflared-darwin-amd64.tgz";
|
|
4949
|
-
return { url: `${base}/${asset}`, kind: "tgz", tempPath:
|
|
5501
|
+
return { url: `${base}/${asset}`, kind: "tgz", tempPath: path9.join(BIN_DIR, asset) };
|
|
4950
5502
|
}
|
|
4951
5503
|
if (platform2 === "linux") {
|
|
4952
5504
|
const asset = arch2 === "arm64" ? "cloudflared-linux-arm64" : arch2 === "arm" ? "cloudflared-linux-arm" : arch2 === "x64" ? "cloudflared-linux-amd64" : null;
|
|
@@ -4970,11 +5522,11 @@ function downloadWithCurl(url, dest) {
|
|
|
4970
5522
|
if (result.status !== 0) {
|
|
4971
5523
|
console.warn(`[cloudflared-supervisor] curl failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
4972
5524
|
try {
|
|
4973
|
-
|
|
5525
|
+
fs7.unlinkSync(dest);
|
|
4974
5526
|
} catch {}
|
|
4975
5527
|
return false;
|
|
4976
5528
|
}
|
|
4977
|
-
return
|
|
5529
|
+
return fs7.existsSync(dest) && fs7.statSync(dest).size > 0;
|
|
4978
5530
|
}
|
|
4979
5531
|
function extractTgzCloudflared(tgzPath, destDir) {
|
|
4980
5532
|
const result = spawnSync("tar", ["-xzf", tgzPath, "-C", destDir], { stdio: "pipe" });
|
|
@@ -4982,7 +5534,7 @@ function extractTgzCloudflared(tgzPath, destDir) {
|
|
|
4982
5534
|
console.warn(`[cloudflared-supervisor] tar extract failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
4983
5535
|
return false;
|
|
4984
5536
|
}
|
|
4985
|
-
return
|
|
5537
|
+
return fs7.existsSync(CLOUDFLARED_PATH);
|
|
4986
5538
|
}
|
|
4987
5539
|
function startCloudflaredProcess(binPath, tunnelToken) {
|
|
4988
5540
|
if (state.stopped)
|
|
@@ -5039,11 +5591,11 @@ var MIME_TYPES = {
|
|
|
5039
5591
|
};
|
|
5040
5592
|
function mintBootstrapToken() {
|
|
5041
5593
|
const bootstrapToken = crypto3.randomBytes(32).toString("base64url");
|
|
5042
|
-
const devicesDir =
|
|
5043
|
-
const bootstrapPath =
|
|
5044
|
-
if (!
|
|
5045
|
-
|
|
5046
|
-
const registry =
|
|
5594
|
+
const devicesDir = path10.join(os6.homedir(), ".openclaw", "devices");
|
|
5595
|
+
const bootstrapPath = path10.join(devicesDir, "bootstrap.json");
|
|
5596
|
+
if (!fs8.existsSync(devicesDir))
|
|
5597
|
+
fs8.mkdirSync(devicesDir, { recursive: true });
|
|
5598
|
+
const registry = fs8.existsSync(bootstrapPath) ? JSON.parse(fs8.readFileSync(bootstrapPath, "utf-8")) : {};
|
|
5047
5599
|
registry[bootstrapToken] = {
|
|
5048
5600
|
token: bootstrapToken,
|
|
5049
5601
|
profile: {
|
|
@@ -5052,15 +5604,15 @@ function mintBootstrapToken() {
|
|
|
5052
5604
|
},
|
|
5053
5605
|
issuedAtMs: Date.now()
|
|
5054
5606
|
};
|
|
5055
|
-
|
|
5607
|
+
fs8.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
|
|
5056
5608
|
return bootstrapToken;
|
|
5057
5609
|
}
|
|
5058
5610
|
function registerWebApp(api) {
|
|
5059
|
-
const pluginRoot =
|
|
5060
|
-
const appRoot =
|
|
5061
|
-
const hasWebBuild =
|
|
5062
|
-
const indexPath = hasWebBuild ?
|
|
5063
|
-
const hasIndex = !!(indexPath &&
|
|
5611
|
+
const pluginRoot = path10.resolve(path10.dirname(api.source), "..");
|
|
5612
|
+
const appRoot = path10.join(pluginRoot, "web-build");
|
|
5613
|
+
const hasWebBuild = fs8.existsSync(appRoot);
|
|
5614
|
+
const indexPath = hasWebBuild ? path10.join(appRoot, "index.html") : null;
|
|
5615
|
+
const hasIndex = !!(indexPath && fs8.existsSync(indexPath));
|
|
5064
5616
|
if (!hasWebBuild) {
|
|
5065
5617
|
api.logger.info("[agentlife] web-build/ not found — /agentlife/pair will still register; static dashboard disabled");
|
|
5066
5618
|
} else if (!hasIndex) {
|
|
@@ -5068,8 +5620,8 @@ function registerWebApp(api) {
|
|
|
5068
5620
|
}
|
|
5069
5621
|
let gatewayToken = "";
|
|
5070
5622
|
try {
|
|
5071
|
-
const configPath =
|
|
5072
|
-
const raw = JSON.parse(
|
|
5623
|
+
const configPath = path10.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
|
|
5624
|
+
const raw = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
5073
5625
|
gatewayToken = raw?.gateway?.auth?.token || "";
|
|
5074
5626
|
} catch {}
|
|
5075
5627
|
loadOrCreatePairingAccessToken();
|
|
@@ -5185,16 +5737,16 @@ function registerWebApp(api) {
|
|
|
5185
5737
|
return true;
|
|
5186
5738
|
}
|
|
5187
5739
|
const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
|
|
5188
|
-
const filePath =
|
|
5740
|
+
const filePath = path10.resolve(appRoot, relative);
|
|
5189
5741
|
if (!filePath.startsWith(appRoot)) {
|
|
5190
5742
|
res.writeHead(403);
|
|
5191
5743
|
res.end();
|
|
5192
5744
|
return true;
|
|
5193
5745
|
}
|
|
5194
|
-
const target =
|
|
5195
|
-
const ext =
|
|
5746
|
+
const target = fs8.existsSync(filePath) && fs8.statSync(filePath).isFile() ? filePath : indexPath;
|
|
5747
|
+
const ext = path10.extname(target).toLowerCase();
|
|
5196
5748
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
5197
|
-
let content =
|
|
5749
|
+
let content = fs8.readFileSync(target);
|
|
5198
5750
|
if (target === indexPath) {
|
|
5199
5751
|
setTimeout(approveLatest, 2000);
|
|
5200
5752
|
setTimeout(approveLatest, 5000);
|
|
@@ -5283,9 +5835,9 @@ Setup code: ${setupCode}
|
|
|
5283
5835
|
api.registerService({
|
|
5284
5836
|
id: "agentlife-auto-pair",
|
|
5285
5837
|
start: async (ctx) => {
|
|
5286
|
-
const devicesDir =
|
|
5287
|
-
const pendingPath =
|
|
5288
|
-
const pairedPath =
|
|
5838
|
+
const devicesDir = path11.join(os7.homedir(), ".openclaw", "devices");
|
|
5839
|
+
const pendingPath = path11.join(devicesDir, "pending.json");
|
|
5840
|
+
const pairedPath = path11.join(devicesDir, "paired.json");
|
|
5289
5841
|
const pollInterval = 3000;
|
|
5290
5842
|
const withAdmin = (arr) => {
|
|
5291
5843
|
const base = Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
@@ -5383,7 +5935,7 @@ Setup code: ${setupCode}
|
|
|
5383
5935
|
|
|
5384
5936
|
// hooks/bootstrap.ts
|
|
5385
5937
|
var SKIP_GUIDANCE_AGENTS = new Set(["agentlife"]);
|
|
5386
|
-
var SKIP_AGENTS_MD_INJECTION = new Set(["agentlife", "agentlife-builder", "quick", "supervisor"]);
|
|
5938
|
+
var SKIP_AGENTS_MD_INJECTION = new Set(["agentlife", "agentlife-builder", "agentlife-vision", "quick", "supervisor"]);
|
|
5387
5939
|
function registerBootstrapHook(api, state2) {
|
|
5388
5940
|
api.registerService({
|
|
5389
5941
|
id: "agentlife-bootstrap-hook",
|
|
@@ -5537,11 +6089,11 @@ function registerBootstrapHookImpl(api, state2) {
|
|
|
5537
6089
|
}
|
|
5538
6090
|
|
|
5539
6091
|
// tools/widget-push.ts
|
|
5540
|
-
var
|
|
6092
|
+
var PROVISIONED_IDS2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
5541
6093
|
function isKnownAgent(state2, agentId, api) {
|
|
5542
6094
|
if (state2.agentRegistry.size === 0)
|
|
5543
6095
|
return true;
|
|
5544
|
-
if (
|
|
6096
|
+
if (PROVISIONED_IDS2.has(agentId) || state2.agentRegistry.has(agentId))
|
|
5545
6097
|
return true;
|
|
5546
6098
|
try {
|
|
5547
6099
|
const cfg = api.runtime.config.loadConfig();
|
|
@@ -5824,12 +6376,53 @@ function extractWidgetText(meta) {
|
|
|
5824
6376
|
}
|
|
5825
6377
|
|
|
5826
6378
|
// hooks/prompt-state.ts
|
|
6379
|
+
var lastEmittedAnswerByKey = new Map;
|
|
6380
|
+
function lastUserMessage(messages) {
|
|
6381
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
6382
|
+
const m = messages[i];
|
|
6383
|
+
if (m?.role !== "user")
|
|
6384
|
+
continue;
|
|
6385
|
+
if (typeof m.content === "string")
|
|
6386
|
+
return m.content;
|
|
6387
|
+
if (Array.isArray(m.content)) {
|
|
6388
|
+
const parts = [];
|
|
6389
|
+
for (const block of m.content) {
|
|
6390
|
+
if (typeof block === "string")
|
|
6391
|
+
parts.push(block);
|
|
6392
|
+
else if (block?.type === "text" && typeof block.text === "string")
|
|
6393
|
+
parts.push(block.text);
|
|
6394
|
+
}
|
|
6395
|
+
return parts.join(`
|
|
6396
|
+
`);
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
return "";
|
|
6400
|
+
}
|
|
5827
6401
|
function registerPromptStateHook(api, state2) {
|
|
5828
|
-
api.on("before_prompt_build", (
|
|
6402
|
+
api.on("before_prompt_build", (event, ctx) => {
|
|
5829
6403
|
if (state2.disabled)
|
|
5830
6404
|
return;
|
|
5831
6405
|
if (!isAgentlifeSession(ctx?.sessionKey))
|
|
5832
6406
|
return;
|
|
6407
|
+
const sessionKey = ctx?.sessionKey;
|
|
6408
|
+
if (sessionKey) {
|
|
6409
|
+
const messages = event?.messages ?? [];
|
|
6410
|
+
const latest = lastUserMessage(messages);
|
|
6411
|
+
if (latest && lastEmittedAnswerByKey.get(sessionKey) !== latest) {
|
|
6412
|
+
const actionMatch = latest.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
6413
|
+
if (actionMatch) {
|
|
6414
|
+
const labelMatch = latest.match(/\blabel=([^\n]+)/);
|
|
6415
|
+
const valueMatch = latest.match(/\bvalue=([^\n]+)/);
|
|
6416
|
+
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
6417
|
+
lastEmittedAnswerByKey.set(sessionKey, latest);
|
|
6418
|
+
try {
|
|
6419
|
+
emitTrigger(state2, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
6420
|
+
} catch (e) {
|
|
6421
|
+
console.warn("[agentlife] cold-start emit userAnswered (prompt-build) failed: %s", e?.message);
|
|
6422
|
+
}
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
5833
6426
|
const agentId = ctx?.agentId;
|
|
5834
6427
|
const isOrchestrator = agentId === "agentlife";
|
|
5835
6428
|
if (isOrchestrator)
|
|
@@ -5849,8 +6442,8 @@ function registerPromptStateHook(api, state2) {
|
|
|
5849
6442
|
}
|
|
5850
6443
|
|
|
5851
6444
|
// gateway/agents.ts
|
|
5852
|
-
import * as
|
|
5853
|
-
import * as
|
|
6445
|
+
import * as fs9 from "node:fs";
|
|
6446
|
+
import * as path12 from "node:path";
|
|
5854
6447
|
function registerAgentGateway(api, state2) {
|
|
5855
6448
|
api.registerGatewayMethod("agentlife.createAgent", async ({ params, respond }) => {
|
|
5856
6449
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
@@ -5938,6 +6531,16 @@ function registerAgentGateway(api, state2) {
|
|
|
5938
6531
|
configFields.push("subagents");
|
|
5939
6532
|
if (identity)
|
|
5940
6533
|
configFields.push("identity");
|
|
6534
|
+
if (!existing) {
|
|
6535
|
+
const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6536
|
+
if (!provisionedIds.has(id)) {
|
|
6537
|
+
try {
|
|
6538
|
+
emitTrigger(state2, "agentRegistered", { agentId: id, name, model });
|
|
6539
|
+
} catch (e) {
|
|
6540
|
+
console.warn("[agentlife] cold-start emit agentRegistered failed: %s", e?.message);
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
5941
6544
|
respond(true, { status, id, name, model, workspace, description, ...configFields.length ? { configFields } : {} });
|
|
5942
6545
|
});
|
|
5943
6546
|
api.registerGatewayMethod("agentlife.deleteAgent", async ({ params, respond }) => {
|
|
@@ -6000,10 +6603,10 @@ function registerAgentGateway(api, state2) {
|
|
|
6000
6603
|
state2.agentDbs.delete(id);
|
|
6001
6604
|
}
|
|
6002
6605
|
if (state2.dbBaseDir) {
|
|
6003
|
-
const dbPath =
|
|
6606
|
+
const dbPath = path12.join(state2.dbBaseDir, `${id}.db`);
|
|
6004
6607
|
try {
|
|
6005
|
-
if (
|
|
6006
|
-
|
|
6608
|
+
if (fs9.existsSync(dbPath)) {
|
|
6609
|
+
fs9.unlinkSync(dbPath);
|
|
6007
6610
|
cleanup.agentDbDeleted = true;
|
|
6008
6611
|
}
|
|
6009
6612
|
} catch (e) {
|
|
@@ -6029,6 +6632,17 @@ function registerAgentGateway(api, state2) {
|
|
|
6029
6632
|
await saveRegistryToDisk(state2);
|
|
6030
6633
|
}
|
|
6031
6634
|
console.log("[agentlife] deleted agent: %s (config=%s, registry=%s, sessions=%d, cron=%d, historyRows=%d, agentDb=%s)", id, removedFromConfig, removedFromRegistry, cleanup.sessions, cleanup.cronJobs, cleanup.historyRows, cleanup.agentDbDeleted);
|
|
6635
|
+
try {
|
|
6636
|
+
const provisionedIds2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6637
|
+
let remaining = 0;
|
|
6638
|
+
for (const aid of state2.agentRegistry.keys()) {
|
|
6639
|
+
if (!provisionedIds2.has(aid))
|
|
6640
|
+
remaining++;
|
|
6641
|
+
}
|
|
6642
|
+
emitTrigger(state2, "agentDeleted", { agentId: id, remaining });
|
|
6643
|
+
} catch (e) {
|
|
6644
|
+
console.warn("[agentlife] cold-start emit agentDeleted failed: %s", e?.message);
|
|
6645
|
+
}
|
|
6032
6646
|
respond(true, { status: "deleted", id, removedFromConfig, removedFromRegistry, cleanup });
|
|
6033
6647
|
});
|
|
6034
6648
|
api.registerGatewayMethod("agentlife.agents", ({ respond }) => {
|
|
@@ -6329,7 +6943,7 @@ function registerUsageGateway(api, state2) {
|
|
|
6329
6943
|
var guidedDismissSent = new Set;
|
|
6330
6944
|
var warnedMissingOrigin = new Set;
|
|
6331
6945
|
function registerSurfacesGateway(api, state2) {
|
|
6332
|
-
api.registerGatewayMethod("agentlife.surfaces", ({ respond, context }) => {
|
|
6946
|
+
api.registerGatewayMethod("agentlife.surfaces", ({ params, respond, context }) => {
|
|
6333
6947
|
captureBridge(context);
|
|
6334
6948
|
const now = Date.now();
|
|
6335
6949
|
const activeSurfaceIds = [];
|
|
@@ -6338,23 +6952,21 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6338
6952
|
respond(true, { surfaces: [] });
|
|
6339
6953
|
return;
|
|
6340
6954
|
}
|
|
6341
|
-
const
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
if (/\bstate=loading\b/.test(headerLine) && now - meta.updatedAt > LOADING_TTL_MS) {
|
|
6345
|
-
const agentId = state2.surfaceDb.getAgentId(surfaceId);
|
|
6346
|
-
meta.lines[0] = headerLine.replace(/\bstate=loading\b/, "state=error");
|
|
6347
|
-
state2.surfaceDb.set(surfaceId, { ...meta, updatedAt: now });
|
|
6348
|
-
recordSurfaceEvent(state2, surfaceId, "loading_resolved_error", undefined, agentId ?? undefined, JSON.stringify({ reason: "loading surface orphaned (>2min)", resolvedBy: "surfaces_read" }));
|
|
6349
|
-
console.log("[agentlife] resolved stale loading surface %s → error (stuck >2min)", surfaceId);
|
|
6350
|
-
}
|
|
6351
|
-
}
|
|
6955
|
+
const visibility = params?.visibility === "paywall" ? "paywall" : "dashboard";
|
|
6956
|
+
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
6957
|
+
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
6352
6958
|
for (const [surfaceId, meta] of state2.surfaceDb.entries()) {
|
|
6353
6959
|
if (isExpired(meta, now))
|
|
6354
6960
|
continue;
|
|
6355
6961
|
const headerLine = meta.lines[0] ?? "";
|
|
6356
|
-
|
|
6962
|
+
const isInput = /\binput\b/.test(headerLine);
|
|
6963
|
+
if (isInput && !isFlowInput(surfaceId))
|
|
6357
6964
|
continue;
|
|
6965
|
+
if (visibility === "dashboard" && surfaceId.startsWith("vision-")) {
|
|
6966
|
+
const visible = state2.surfaceDb.getDashboardVisible(surfaceId);
|
|
6967
|
+
if (!visible)
|
|
6968
|
+
continue;
|
|
6969
|
+
}
|
|
6358
6970
|
if (meta.lines.length > 0) {
|
|
6359
6971
|
surfaceEntries.push({ surfaceId, dsl: meta.lines.join(`
|
|
6360
6972
|
`) });
|
|
@@ -6492,7 +7104,12 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6492
7104
|
automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
|
|
6493
7105
|
} catch {}
|
|
6494
7106
|
guidedDismissSent.delete(surfaceId);
|
|
6495
|
-
|
|
7107
|
+
const isVision = surfaceId.startsWith("vision-");
|
|
7108
|
+
if (isVision) {
|
|
7109
|
+
state2.surfaceDb.setDashboardVisible(surfaceId, false);
|
|
7110
|
+
} else {
|
|
7111
|
+
state2.surfaceDb.delete(surfaceId);
|
|
7112
|
+
}
|
|
6496
7113
|
broadcastDelete(surfaceId);
|
|
6497
7114
|
try {
|
|
6498
7115
|
enqueueCleanupTasks(state2, surfaceId, agentId, cronId, automations);
|
|
@@ -6500,7 +7117,7 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6500
7117
|
console.error("[agentlife] dismiss: failed to enqueue cleanup tasks for %s: %s", surfaceId, err?.message);
|
|
6501
7118
|
}
|
|
6502
7119
|
const taskCount = (cronId ? 1 : 0) + (agentId ? 1 : 0);
|
|
6503
|
-
console.log("[agentlife] dismiss: %s
|
|
7120
|
+
console.log("[agentlife] dismiss: %s %s, %d cleanup tasks enqueued (reason=%s)", surfaceId, isVision ? "hidden from dashboard (kept for paywall)" : "deleted", taskCount, reason ?? "none");
|
|
6504
7121
|
respond(true, { surfaceId, dismissed: true });
|
|
6505
7122
|
processCleanupTasks(state2, surfaceId).catch((e) => console.warn("[agentlife] dismiss cleanup processor error:", e?.message));
|
|
6506
7123
|
}, { scope: "operator.write" });
|
|
@@ -6652,12 +7269,12 @@ function registerAutomationsGateway(api, state2) {
|
|
|
6652
7269
|
}
|
|
6653
7270
|
|
|
6654
7271
|
// gateway/admin.ts
|
|
6655
|
-
import * as
|
|
7272
|
+
import * as fs10 from "node:fs/promises";
|
|
6656
7273
|
import * as fsSync4 from "node:fs";
|
|
6657
|
-
import * as
|
|
6658
|
-
import * as
|
|
7274
|
+
import * as os8 from "node:os";
|
|
7275
|
+
import * as path13 from "node:path";
|
|
6659
7276
|
function pluginConfigPath() {
|
|
6660
|
-
return
|
|
7277
|
+
return path13.join(os8.homedir(), ".openclaw", "agentlife", "plugin-config.json");
|
|
6661
7278
|
}
|
|
6662
7279
|
function readPluginConfig() {
|
|
6663
7280
|
try {
|
|
@@ -6667,7 +7284,7 @@ function readPluginConfig() {
|
|
|
6667
7284
|
}
|
|
6668
7285
|
}
|
|
6669
7286
|
function writePluginConfig(config2) {
|
|
6670
|
-
const dir =
|
|
7287
|
+
const dir = path13.dirname(pluginConfigPath());
|
|
6671
7288
|
fsSync4.mkdirSync(dir, { recursive: true });
|
|
6672
7289
|
fsSync4.writeFileSync(pluginConfigPath(), JSON.stringify(config2, null, 2));
|
|
6673
7290
|
}
|
|
@@ -6766,7 +7383,7 @@ function registerAdminGateway(api, state2) {
|
|
|
6766
7383
|
api.registerGatewayMethod("agentlife.uninstall", async ({ respond }) => {
|
|
6767
7384
|
try {
|
|
6768
7385
|
const cleaned = [];
|
|
6769
|
-
const baseDir = state2.agentlifeStateDir ??
|
|
7386
|
+
const baseDir = state2.agentlifeStateDir ?? path13.join(os8.homedir(), ".openclaw", "agentlife");
|
|
6770
7387
|
const identity = loadDeviceIdentity();
|
|
6771
7388
|
if (!identity) {
|
|
6772
7389
|
cleaned.push("server deprovision skipped (no device identity)");
|
|
@@ -6831,7 +7448,7 @@ function registerAdminGateway(api, state2) {
|
|
|
6831
7448
|
} catch {
|
|
6832
7449
|
cleaned.push("agent config cleanup skipped");
|
|
6833
7450
|
}
|
|
6834
|
-
const backupPath =
|
|
7451
|
+
const backupPath = path13.join(baseDir, "config-backup.json");
|
|
6835
7452
|
try {
|
|
6836
7453
|
cleaned.push(await restoreConfigFromBackup(api, backupPath));
|
|
6837
7454
|
} catch {
|
|
@@ -6854,35 +7471,35 @@ function registerAdminGateway(api, state2) {
|
|
|
6854
7471
|
dbPath,
|
|
6855
7472
|
dbPath ? `${dbPath}-wal` : null,
|
|
6856
7473
|
dbPath ? `${dbPath}-shm` : null,
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
7474
|
+
path13.join(baseDir, "config-backup.json"),
|
|
7475
|
+
path13.join(baseDir, "notification-config.json"),
|
|
7476
|
+
path13.join(baseDir, "canvas-node-identity.json")
|
|
6860
7477
|
].filter(Boolean);
|
|
6861
7478
|
for (const fp of stateFiles) {
|
|
6862
7479
|
try {
|
|
6863
|
-
await
|
|
6864
|
-
cleaned.push(`deleted ${
|
|
7480
|
+
await fs10.unlink(fp);
|
|
7481
|
+
cleaned.push(`deleted ${path13.basename(fp)}`);
|
|
6865
7482
|
} catch {}
|
|
6866
7483
|
}
|
|
6867
7484
|
if (state2.dbBaseDir) {
|
|
6868
7485
|
try {
|
|
6869
|
-
await
|
|
7486
|
+
await fs10.rm(state2.dbBaseDir, { recursive: true, force: true });
|
|
6870
7487
|
cleaned.push("deleted agent databases");
|
|
6871
7488
|
} catch {}
|
|
6872
7489
|
}
|
|
6873
7490
|
for (const agent of PROVISIONED_AGENTS) {
|
|
6874
7491
|
if (agent.existingAgent)
|
|
6875
7492
|
continue;
|
|
6876
|
-
const wsDir = agent.workspaceDir ??
|
|
7493
|
+
const wsDir = agent.workspaceDir ?? path13.join(os8.homedir(), ".openclaw", `workspace-${agent.id}`);
|
|
6877
7494
|
try {
|
|
6878
|
-
await
|
|
7495
|
+
await fs10.rm(wsDir, { recursive: true, force: true });
|
|
6879
7496
|
cleaned.push(`deleted workspace ${agent.id}`);
|
|
6880
7497
|
} catch {}
|
|
6881
7498
|
}
|
|
6882
7499
|
try {
|
|
6883
|
-
const remaining = await
|
|
7500
|
+
const remaining = await fs10.readdir(baseDir);
|
|
6884
7501
|
if (remaining.length === 0) {
|
|
6885
|
-
await
|
|
7502
|
+
await fs10.rmdir(baseDir);
|
|
6886
7503
|
cleaned.push("deleted agentlife state directory");
|
|
6887
7504
|
}
|
|
6888
7505
|
} catch {}
|
|
@@ -7124,6 +7741,295 @@ function parseOffset2(offset) {
|
|
|
7124
7741
|
}
|
|
7125
7742
|
}
|
|
7126
7743
|
|
|
7744
|
+
// gateway/providers.ts
|
|
7745
|
+
import * as fs11 from "node:fs";
|
|
7746
|
+
import * as os9 from "node:os";
|
|
7747
|
+
import * as path14 from "node:path";
|
|
7748
|
+
import {
|
|
7749
|
+
upsertApiKeyProfile,
|
|
7750
|
+
applyAuthProfileConfig
|
|
7751
|
+
} from "openclaw/plugin-sdk/provider-auth-api-key";
|
|
7752
|
+
var catalogCache = null;
|
|
7753
|
+
var catalogPromise = null;
|
|
7754
|
+
var modelsCache = null;
|
|
7755
|
+
var modelsPromise = null;
|
|
7756
|
+
function configPath() {
|
|
7757
|
+
return path14.join(os9.homedir(), ".openclaw", "openclaw.json");
|
|
7758
|
+
}
|
|
7759
|
+
function readConfig() {
|
|
7760
|
+
try {
|
|
7761
|
+
return JSON.parse(fs11.readFileSync(configPath(), "utf-8"));
|
|
7762
|
+
} catch {
|
|
7763
|
+
return {};
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7766
|
+
function writeConfig(cfg) {
|
|
7767
|
+
fs11.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + `
|
|
7768
|
+
`, "utf-8");
|
|
7769
|
+
}
|
|
7770
|
+
async function fetchCatalog(run) {
|
|
7771
|
+
const result = await run(["openclaw", "infer", "model", "providers", "--json"], { timeoutMs: 60000 });
|
|
7772
|
+
if ((result?.code ?? 0) !== 0)
|
|
7773
|
+
throw new Error(`providers catalog query failed`);
|
|
7774
|
+
const raw = (result?.stdout ?? "").trim();
|
|
7775
|
+
const jsonStart = raw.indexOf("[");
|
|
7776
|
+
if (jsonStart < 0)
|
|
7777
|
+
throw new Error("providers catalog: no JSON array in output");
|
|
7778
|
+
const parsed = JSON.parse(raw.slice(jsonStart));
|
|
7779
|
+
if (!Array.isArray(parsed))
|
|
7780
|
+
throw new Error("providers catalog: not an array");
|
|
7781
|
+
return parsed;
|
|
7782
|
+
}
|
|
7783
|
+
async function ensureCatalog(run) {
|
|
7784
|
+
if (catalogCache)
|
|
7785
|
+
return catalogCache;
|
|
7786
|
+
if (!run)
|
|
7787
|
+
return [];
|
|
7788
|
+
if (!catalogPromise) {
|
|
7789
|
+
catalogPromise = fetchCatalog(run).then((c) => {
|
|
7790
|
+
catalogCache = c;
|
|
7791
|
+
return c;
|
|
7792
|
+
}).catch((err) => {
|
|
7793
|
+
catalogPromise = null;
|
|
7794
|
+
throw err;
|
|
7795
|
+
});
|
|
7796
|
+
}
|
|
7797
|
+
return catalogPromise;
|
|
7798
|
+
}
|
|
7799
|
+
async function fetchModels(run) {
|
|
7800
|
+
const result = await run(["openclaw", "infer", "model", "list", "--json"], { timeoutMs: 60000 });
|
|
7801
|
+
if ((result?.code ?? 0) !== 0)
|
|
7802
|
+
throw new Error(`models list query failed`);
|
|
7803
|
+
const raw = (result?.stdout ?? "").trim();
|
|
7804
|
+
const jsonStart = raw.indexOf("[");
|
|
7805
|
+
if (jsonStart < 0)
|
|
7806
|
+
throw new Error("models list: no JSON array in output");
|
|
7807
|
+
const parsed = JSON.parse(raw.slice(jsonStart));
|
|
7808
|
+
if (!Array.isArray(parsed))
|
|
7809
|
+
throw new Error("models list: not an array");
|
|
7810
|
+
return parsed;
|
|
7811
|
+
}
|
|
7812
|
+
async function ensureModels(run) {
|
|
7813
|
+
if (modelsCache)
|
|
7814
|
+
return modelsCache;
|
|
7815
|
+
if (!run)
|
|
7816
|
+
return [];
|
|
7817
|
+
if (!modelsPromise) {
|
|
7818
|
+
modelsPromise = fetchModels(run).then((m) => {
|
|
7819
|
+
modelsCache = m;
|
|
7820
|
+
return m;
|
|
7821
|
+
}).catch((err) => {
|
|
7822
|
+
modelsPromise = null;
|
|
7823
|
+
throw err;
|
|
7824
|
+
});
|
|
7825
|
+
}
|
|
7826
|
+
return modelsPromise;
|
|
7827
|
+
}
|
|
7828
|
+
function authenticatedProviders() {
|
|
7829
|
+
const cfg = readConfig();
|
|
7830
|
+
const profiles = cfg?.auth?.profiles ?? {};
|
|
7831
|
+
const out = new Set;
|
|
7832
|
+
for (const entry of Object.values(profiles)) {
|
|
7833
|
+
const e = entry;
|
|
7834
|
+
if (typeof e?.provider === "string" && typeof e?.mode === "string") {
|
|
7835
|
+
out.add(e.provider);
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
return out;
|
|
7839
|
+
}
|
|
7840
|
+
function configuredApiKeyProviders() {
|
|
7841
|
+
const cfg = readConfig();
|
|
7842
|
+
const profiles = cfg?.auth?.profiles ?? {};
|
|
7843
|
+
const out = new Set;
|
|
7844
|
+
for (const entry of Object.values(profiles)) {
|
|
7845
|
+
const e = entry;
|
|
7846
|
+
if (e?.mode === "api_key" && typeof e.provider === "string") {
|
|
7847
|
+
out.add(e.provider);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
return out;
|
|
7851
|
+
}
|
|
7852
|
+
async function probeAnthropic(apiKey) {
|
|
7853
|
+
const ctl = new AbortController;
|
|
7854
|
+
const timer = setTimeout(() => ctl.abort(), 1e4);
|
|
7855
|
+
try {
|
|
7856
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
7857
|
+
method: "POST",
|
|
7858
|
+
headers: {
|
|
7859
|
+
"x-api-key": apiKey,
|
|
7860
|
+
"anthropic-version": "2023-06-01",
|
|
7861
|
+
"content-type": "application/json"
|
|
7862
|
+
},
|
|
7863
|
+
body: JSON.stringify({
|
|
7864
|
+
model: "claude-haiku-4-5",
|
|
7865
|
+
max_tokens: 1,
|
|
7866
|
+
messages: [{ role: "user", content: "ping" }]
|
|
7867
|
+
}),
|
|
7868
|
+
signal: ctl.signal
|
|
7869
|
+
});
|
|
7870
|
+
if (res.ok)
|
|
7871
|
+
return { ok: true };
|
|
7872
|
+
const body = await res.text().catch(() => "");
|
|
7873
|
+
return { ok: false, error: `Anthropic ${res.status}: ${body.slice(0, 240) || res.statusText}` };
|
|
7874
|
+
} catch (err) {
|
|
7875
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
7876
|
+
} finally {
|
|
7877
|
+
clearTimeout(timer);
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
async function probeOpenAI(apiKey) {
|
|
7881
|
+
const ctl = new AbortController;
|
|
7882
|
+
const timer = setTimeout(() => ctl.abort(), 1e4);
|
|
7883
|
+
try {
|
|
7884
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
7885
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
7886
|
+
signal: ctl.signal
|
|
7887
|
+
});
|
|
7888
|
+
if (res.ok)
|
|
7889
|
+
return { ok: true };
|
|
7890
|
+
const body = await res.text().catch(() => "");
|
|
7891
|
+
return { ok: false, error: `OpenAI ${res.status}: ${body.slice(0, 240) || res.statusText}` };
|
|
7892
|
+
} catch (err) {
|
|
7893
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
7894
|
+
} finally {
|
|
7895
|
+
clearTimeout(timer);
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
function canProbe(provider) {
|
|
7899
|
+
return provider === "anthropic" || provider === "openai";
|
|
7900
|
+
}
|
|
7901
|
+
async function probe(provider, apiKey) {
|
|
7902
|
+
if (provider === "anthropic")
|
|
7903
|
+
return probeAnthropic(apiKey);
|
|
7904
|
+
if (provider === "openai")
|
|
7905
|
+
return probeOpenAI(apiKey);
|
|
7906
|
+
return { ok: false, error: `test not supported for provider: ${provider}` };
|
|
7907
|
+
}
|
|
7908
|
+
function registerProvidersGateway(api, state2) {
|
|
7909
|
+
const run = state2.runCommand;
|
|
7910
|
+
if (run) {
|
|
7911
|
+
ensureCatalog(run).catch(() => {});
|
|
7912
|
+
ensureModels(run).catch(() => {});
|
|
7913
|
+
}
|
|
7914
|
+
api.registerGatewayMethod("agentlife.providers.list", async ({ respond }) => {
|
|
7915
|
+
try {
|
|
7916
|
+
const [catalog, configured] = await Promise.all([
|
|
7917
|
+
ensureCatalog(run ?? null),
|
|
7918
|
+
Promise.resolve(configuredApiKeyProviders())
|
|
7919
|
+
]);
|
|
7920
|
+
const providers = catalog.map((entry) => ({
|
|
7921
|
+
provider: entry.provider,
|
|
7922
|
+
modelCount: entry.count,
|
|
7923
|
+
available: entry.available,
|
|
7924
|
+
configured: configured.has(entry.provider),
|
|
7925
|
+
canProbe: canProbe(entry.provider)
|
|
7926
|
+
}));
|
|
7927
|
+
respond(true, { providers });
|
|
7928
|
+
} catch (err) {
|
|
7929
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7930
|
+
}
|
|
7931
|
+
}, { scope: "operator.read" });
|
|
7932
|
+
api.registerGatewayMethod("agentlife.providers.set", async ({ params, respond }) => {
|
|
7933
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7934
|
+
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
7935
|
+
if (!provider)
|
|
7936
|
+
return respond(false, { error: "provider is empty" });
|
|
7937
|
+
if (!apiKey)
|
|
7938
|
+
return respond(false, { error: "apiKey is empty" });
|
|
7939
|
+
try {
|
|
7940
|
+
const catalog = await ensureCatalog(run ?? null);
|
|
7941
|
+
if (catalog.length > 0 && !catalog.some((e) => e.provider === provider)) {
|
|
7942
|
+
return respond(false, { error: `unknown provider: ${provider}` });
|
|
7943
|
+
}
|
|
7944
|
+
} catch {}
|
|
7945
|
+
try {
|
|
7946
|
+
const profileId = upsertApiKeyProfile({ provider, input: apiKey });
|
|
7947
|
+
const cfg = readConfig();
|
|
7948
|
+
const nextCfg = applyAuthProfileConfig(cfg, {
|
|
7949
|
+
profileId,
|
|
7950
|
+
provider,
|
|
7951
|
+
mode: "api_key",
|
|
7952
|
+
preferProfileFirst: true
|
|
7953
|
+
});
|
|
7954
|
+
writeConfig(nextCfg);
|
|
7955
|
+
respond(true, { provider, configured: true, profileId });
|
|
7956
|
+
} catch (err) {
|
|
7957
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7958
|
+
}
|
|
7959
|
+
}, { scope: "operator.write" });
|
|
7960
|
+
api.registerGatewayMethod("agentlife.providers.delete", ({ params, respond }) => {
|
|
7961
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7962
|
+
if (!provider)
|
|
7963
|
+
return respond(false, { error: "provider is empty" });
|
|
7964
|
+
try {
|
|
7965
|
+
const cfg = readConfig();
|
|
7966
|
+
const profiles = cfg?.auth?.profiles;
|
|
7967
|
+
if (profiles && typeof profiles === "object") {
|
|
7968
|
+
for (const [id, entry] of Object.entries(profiles)) {
|
|
7969
|
+
if (entry?.provider === provider)
|
|
7970
|
+
delete profiles[id];
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
if (cfg?.auth?.order && typeof cfg.auth.order === "object") {
|
|
7974
|
+
delete cfg.auth.order[provider];
|
|
7975
|
+
}
|
|
7976
|
+
writeConfig(cfg);
|
|
7977
|
+
respond(true, { provider, configured: false });
|
|
7978
|
+
} catch (err) {
|
|
7979
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7980
|
+
}
|
|
7981
|
+
}, { scope: "operator.write" });
|
|
7982
|
+
api.registerGatewayMethod("agentlife.providers.test", async ({ params, respond }) => {
|
|
7983
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7984
|
+
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
7985
|
+
if (!provider)
|
|
7986
|
+
return respond(false, { error: "provider is empty" });
|
|
7987
|
+
if (!apiKey)
|
|
7988
|
+
return respond(true, { ok: false, error: "apiKey required — type a key before testing" });
|
|
7989
|
+
try {
|
|
7990
|
+
const result = await probe(provider, apiKey);
|
|
7991
|
+
respond(true, { ok: result.ok, error: result.error });
|
|
7992
|
+
} catch (err) {
|
|
7993
|
+
respond(true, { ok: false, error: err?.message ?? String(err) });
|
|
7994
|
+
}
|
|
7995
|
+
}, { scope: "operator.read" });
|
|
7996
|
+
api.registerGatewayMethod("agentlife.models.list", async ({ respond }) => {
|
|
7997
|
+
try {
|
|
7998
|
+
const [all, authed] = await Promise.all([
|
|
7999
|
+
ensureModels(run ?? null),
|
|
8000
|
+
Promise.resolve(authenticatedProviders())
|
|
8001
|
+
]);
|
|
8002
|
+
const filtered = all.filter((m) => authed.has(m.provider));
|
|
8003
|
+
const DATE_SUFFIX = /-(?:\d{4}-\d{2}-\d{2}|\d{8})$/;
|
|
8004
|
+
const undatedIds = new Set(filtered.filter((m) => !DATE_SUFFIX.test(m.id)).map((m) => m.id));
|
|
8005
|
+
const deduped = filtered.filter((m) => {
|
|
8006
|
+
if (!DATE_SUFFIX.test(m.id))
|
|
8007
|
+
return true;
|
|
8008
|
+
return !undatedIds.has(m.id.replace(DATE_SUFFIX, ""));
|
|
8009
|
+
});
|
|
8010
|
+
respond(true, { models: deduped });
|
|
8011
|
+
} catch (err) {
|
|
8012
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
8013
|
+
}
|
|
8014
|
+
}, { scope: "operator.read" });
|
|
8015
|
+
api.registerGatewayMethod("agentlife.providers.loginCodex", async ({ respond }) => {
|
|
8016
|
+
if (!run)
|
|
8017
|
+
return respond(false, { error: "runCommand unavailable" });
|
|
8018
|
+
try {
|
|
8019
|
+
const result = await run(["openclaw", "capability", "model", "auth", "login", "--provider", "openai-codex"], { timeoutMs: 180000 });
|
|
8020
|
+
const combined = `${result?.stdout ?? ""}
|
|
8021
|
+
${result?.stderr ?? ""}`;
|
|
8022
|
+
const urlMatch = combined.match(/https:\/\/\S+/);
|
|
8023
|
+
if ((result?.code ?? 0) !== 0 && !urlMatch) {
|
|
8024
|
+
return respond(false, { error: (result?.stderr ?? "").trim() || "codex login failed" });
|
|
8025
|
+
}
|
|
8026
|
+
respond(true, { url: urlMatch?.[0] ?? null, completed: (result?.code ?? 0) === 0 });
|
|
8027
|
+
} catch (err) {
|
|
8028
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
8029
|
+
}
|
|
8030
|
+
}, { scope: "operator.write" });
|
|
8031
|
+
}
|
|
8032
|
+
|
|
7127
8033
|
// index.ts
|
|
7128
8034
|
var currentState = null;
|
|
7129
8035
|
var registered = false;
|
|
@@ -7133,7 +8039,7 @@ var stopQualityCheckPoller = null;
|
|
|
7133
8039
|
var stopCloudflared = null;
|
|
7134
8040
|
function resolveInternalModel(api) {
|
|
7135
8041
|
try {
|
|
7136
|
-
const pluginCfgPath =
|
|
8042
|
+
const pluginCfgPath = path15.join(homedir11(), ".openclaw", "agentlife", "plugin-config.json");
|
|
7137
8043
|
try {
|
|
7138
8044
|
const raw = __require("node:fs").readFileSync(pluginCfgPath, "utf-8");
|
|
7139
8045
|
const pluginCfg = JSON.parse(raw);
|
|
@@ -7169,7 +8075,7 @@ function register(api) {
|
|
|
7169
8075
|
return;
|
|
7170
8076
|
}
|
|
7171
8077
|
registered = true;
|
|
7172
|
-
const fallbackDir =
|
|
8078
|
+
const fallbackDir = path15.join(homedir11(), ".openclaw", "agentlife");
|
|
7173
8079
|
const state2 = {
|
|
7174
8080
|
surfaceDb: null,
|
|
7175
8081
|
agentRegistry: new Map,
|
|
@@ -7179,9 +8085,9 @@ function register(api) {
|
|
|
7179
8085
|
agentDbs: new Map,
|
|
7180
8086
|
historyDb: null,
|
|
7181
8087
|
agentlifeStateDir: fallbackDir,
|
|
7182
|
-
registryFilePath:
|
|
7183
|
-
dbBaseDir:
|
|
7184
|
-
historyDbPath:
|
|
8088
|
+
registryFilePath: path15.join(fallbackDir, "agent-registry.json"),
|
|
8089
|
+
dbBaseDir: path15.join(fallbackDir, "db"),
|
|
8090
|
+
historyDbPath: path15.join(fallbackDir, "agentlife.db"),
|
|
7185
8091
|
runCommand: api.runtime.system?.runCommandWithTimeout ?? null,
|
|
7186
8092
|
enqueueSystemEvent: null,
|
|
7187
8093
|
requestHeartbeatNow: null,
|
|
@@ -7212,6 +8118,7 @@ function register(api) {
|
|
|
7212
8118
|
stopObservability = startObservabilityService(state2);
|
|
7213
8119
|
stopDailySweep = startDailySweepService(state2);
|
|
7214
8120
|
stopQualityCheckPoller = startQualityCheckPoller(state2);
|
|
8121
|
+
await initColdStartMachine(state2, api.runtime, console.log);
|
|
7215
8122
|
}
|
|
7216
8123
|
});
|
|
7217
8124
|
registerConfigOptimizer(api, state2);
|
|
@@ -7238,6 +8145,7 @@ function register(api) {
|
|
|
7238
8145
|
stopCloudflared();
|
|
7239
8146
|
stopCloudflared = null;
|
|
7240
8147
|
}
|
|
8148
|
+
shutdownColdStartMachine();
|
|
7241
8149
|
closeAllDbs(state2);
|
|
7242
8150
|
}
|
|
7243
8151
|
});
|
|
@@ -7254,8 +8162,9 @@ function register(api) {
|
|
|
7254
8162
|
registerAutomationsGateway(api, state2);
|
|
7255
8163
|
registerFollowupsGateway(api, state2);
|
|
7256
8164
|
registerAdminGateway(api, state2);
|
|
8165
|
+
registerProvidersGateway(api, state2);
|
|
7257
8166
|
registerWebApp(api);
|
|
7258
|
-
const notifyConfigPath =
|
|
8167
|
+
const notifyConfigPath = path15.join(fallbackDir, "notification-config.json");
|
|
7259
8168
|
api.registerGatewayMethod("agentlife.notifications.register", ({ params, respond }) => {
|
|
7260
8169
|
const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
|
|
7261
8170
|
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
@@ -7263,35 +8172,45 @@ function register(api) {
|
|
|
7263
8172
|
return respond(false, { error: "missing serverUrl or apiKey" });
|
|
7264
8173
|
}
|
|
7265
8174
|
try {
|
|
7266
|
-
const { writeFileSync:
|
|
7267
|
-
mkdirSync6(
|
|
7268
|
-
|
|
8175
|
+
const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync6 } = __require("node:fs");
|
|
8176
|
+
mkdirSync6(path15.dirname(notifyConfigPath), { recursive: true });
|
|
8177
|
+
writeFileSync8(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
|
|
7269
8178
|
} catch {}
|
|
7270
8179
|
respond(true, { registered: true });
|
|
7271
8180
|
}, { scope: "operator.write" });
|
|
7272
|
-
api.registerGatewayMethod("agentlife.
|
|
8181
|
+
api.registerGatewayMethod("agentlife.coldStart.observe", ({ respond }) => {
|
|
7273
8182
|
try {
|
|
7274
8183
|
if (!currentState) {
|
|
7275
8184
|
return respond(false, { error: "plugin state not initialized" });
|
|
7276
8185
|
}
|
|
7277
|
-
|
|
7278
|
-
respond(true, result);
|
|
8186
|
+
respond(true, observeColdStart(currentState));
|
|
7279
8187
|
} catch (e) {
|
|
7280
8188
|
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
7281
8189
|
}
|
|
7282
8190
|
}, { scope: "operator.read" });
|
|
7283
|
-
api.registerGatewayMethod("agentlife.
|
|
8191
|
+
api.registerGatewayMethod("agentlife.coldStart.retry", async ({ respond }) => {
|
|
7284
8192
|
try {
|
|
7285
8193
|
if (!currentState) {
|
|
7286
8194
|
return respond(false, { error: "plugin state not initialized" });
|
|
7287
8195
|
}
|
|
7288
|
-
|
|
7289
|
-
const result = await ensureVisionPosters(currentState, api.runtime, console.log);
|
|
7290
|
-
respond(true, result);
|
|
8196
|
+
respond(true, await retryColdStart(currentState));
|
|
7291
8197
|
} catch (e) {
|
|
7292
8198
|
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
7293
8199
|
}
|
|
7294
8200
|
}, { scope: "operator.read" });
|
|
8201
|
+
api.registerGatewayMethod("agentlife.coldStart.advance", ({ params, respond }) => {
|
|
8202
|
+
try {
|
|
8203
|
+
if (!currentState)
|
|
8204
|
+
return respond(false, { error: "plugin state not initialized" });
|
|
8205
|
+
const kind = typeof params?.kind === "string" ? params.kind : null;
|
|
8206
|
+
if (!kind)
|
|
8207
|
+
return respond(false, { error: "missing kind" });
|
|
8208
|
+
emitTrigger(currentState, kind, params?.payload ?? {});
|
|
8209
|
+
respond(true, { enqueued: true });
|
|
8210
|
+
} catch (e) {
|
|
8211
|
+
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8212
|
+
}
|
|
8213
|
+
}, { scope: "operator.admin" });
|
|
7295
8214
|
}
|
|
7296
8215
|
export {
|
|
7297
8216
|
register as default,
|