agentlife 1.3.1 → 1.4.0
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 +1384 -444
- 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,28 +1357,158 @@ 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."
|
|
1361
|
+
\`\`\`
|
|
1362
|
+
|
|
1363
|
+
Translate button labels into the user's locale if known. Do not vary the surfaceId \`onboarding-domain\` — the state machine observes it.
|
|
1364
|
+
|
|
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.**
|
|
1366
|
+
|
|
1367
|
+
Required call sequence for the post-choice creation turn:
|
|
1368
|
+
|
|
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
|
+
|
|
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.
|
|
1380
|
+
|
|
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.
|
|
1382
|
+
|
|
1383
|
+
### \`[system:dashboard-bootstrap]\`
|
|
1384
|
+
|
|
1385
|
+
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**:
|
|
1386
|
+
|
|
1387
|
+
1. Push 1-4 \`vision-*\` posters quoted from USER DATA, then respond \`done\`.
|
|
1388
|
+
2. Respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1389
|
+
|
|
1390
|
+
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.
|
|
1391
|
+
|
|
1392
|
+
**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.
|
|
1393
|
+
|
|
1394
|
+
**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\`.
|
|
1395
|
+
|
|
1396
|
+
**Step 3 — Push posters (only if Step 2 said yes).** For each quoted phrase (max 4), push one vision poster. Use the user's actual words. Each poster covers a DIFFERENT life area (body, money, place, work, relationships, identity, craft).
|
|
1397
|
+
|
|
1398
|
+
**Templates.** Every template renders emoji + headline + support + a bottom element. Copy ONE template exactly per poster. Do not add or remove components. Do not change order.
|
|
1399
|
+
|
|
1400
|
+
TEMPLATE A — Statement:
|
|
1401
|
+
\`\`\`
|
|
1402
|
+
surface vision-{slug} size=l
|
|
1403
|
+
card
|
|
1404
|
+
column align=center
|
|
1405
|
+
text "{EMOJI}" h1
|
|
1406
|
+
text "{HEADLINE}" h1 color={ACCENT}
|
|
1407
|
+
text "{SUPPORT}" body
|
|
1408
|
+
text "{MOMENT}" h4 color={ACCENT}
|
|
1409
|
+
goal: Vision poster — {slug}
|
|
1410
|
+
followup: +30d "Refresh this vision poster if the user's data has shifted."
|
|
1411
|
+
\`\`\`
|
|
1412
|
+
|
|
1413
|
+
TEMPLATE B — Destination:
|
|
1414
|
+
\`\`\`
|
|
1415
|
+
surface vision-{slug} size=l
|
|
1416
|
+
card
|
|
1417
|
+
column align=center
|
|
1418
|
+
text "{EMOJI}" h1
|
|
1419
|
+
text "{HEADLINE}" h1 color={ACCENT}
|
|
1420
|
+
text "{SUPPORT}" body
|
|
1421
|
+
badge "{PLACE}" color={ACCENT} outlined
|
|
1422
|
+
goal: Vision poster — {slug}
|
|
1423
|
+
followup: +30d "Refresh this vision poster if the user's data has shifted."
|
|
1424
|
+
\`\`\`
|
|
1425
|
+
|
|
1426
|
+
TEMPLATE C — Outcome:
|
|
1427
|
+
\`\`\`
|
|
1428
|
+
surface vision-{slug} size=l
|
|
1429
|
+
card
|
|
1430
|
+
column align=center
|
|
1431
|
+
text "{EMOJI}" h1
|
|
1432
|
+
text "{HEADLINE}" h1 color={ACCENT}
|
|
1433
|
+
text "{SUPPORT}" body
|
|
1434
|
+
row distribute=spaceEvenly
|
|
1435
|
+
text "{C1}" h4 color={ACCENT}
|
|
1436
|
+
text "·" h4
|
|
1437
|
+
text "{C2}" h4 color={ACCENT}
|
|
1438
|
+
text "·" h4
|
|
1439
|
+
text "{C3}" h4 color={ACCENT}
|
|
1440
|
+
goal: Vision poster — {slug}
|
|
1441
|
+
followup: +30d "Refresh this vision poster if the user's data has shifted."
|
|
1442
|
+
\`\`\`
|
|
1443
|
+
|
|
1444
|
+
Placeholder rules:
|
|
1445
|
+
- \`{slug}\`: short kebab-case slug for the life area
|
|
1446
|
+
- \`{ACCENT}\`: hex color. Vary across posters: \`#10B981\`, \`#7C3AED\`, \`#3B82F6\`, \`#F59E0B\`, \`#EC4899\`, \`#14B8A6\`
|
|
1447
|
+
- \`{EMOJI}\`: a single emoji capturing the emotional core — visceral, not literal
|
|
1448
|
+
- \`{HEADLINE}\`: 3-5 words, declarative, first-person if natural. The dream version, not the next checkpoint.
|
|
1449
|
+
- \`{SUPPORT}\`: 6-12 words, one short sentence. Concrete supporting context in the user's actual language.
|
|
1450
|
+
- \`{MOMENT}\` (Template A): 1-3 words, a time anchor
|
|
1451
|
+
- \`{PLACE}\` (Template B): 1-3 words, a real geographic place name (not a date)
|
|
1452
|
+
- \`{C1}, {C2}, {C3}\` (Template C): 1-2 words each, concept beats from the user's vocabulary
|
|
1453
|
+
|
|
1454
|
+
Variety rule: across the 3-4 posters, use AT LEAST 2 different templates.
|
|
1455
|
+
|
|
1456
|
+
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's content names tools/agents instead of outcomes, it is wrong — drop it.
|
|
1457
|
+
|
|
1458
|
+
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.
|
|
1459
|
+
|
|
1460
|
+
After all pushes, respond \`done\` and STOP. If you cannot find signal, respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1461
|
+
|
|
1462
|
+
### \`[system:ask-dream]\`
|
|
1463
|
+
|
|
1464
|
+
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.
|
|
1465
|
+
|
|
1466
|
+
\`\`\`
|
|
1467
|
+
surface awaiting-dream-input input
|
|
1468
|
+
card
|
|
1469
|
+
column
|
|
1470
|
+
text "Tell me one dream or goal you want to track" h4
|
|
1471
|
+
button "Body & health" action=choice
|
|
1472
|
+
button "Money & work" action=choice
|
|
1473
|
+
button "Place & life chapter" action=choice
|
|
1474
|
+
button "Craft or identity" action=choice
|
|
1475
|
+
goal: Capture the user's dream so the dashboard can synthesize a vision poster
|
|
1476
|
+
followup: +30m "User did not respond to dream-ask. Say done."
|
|
1362
1477
|
\`\`\`
|
|
1363
1478
|
|
|
1364
|
-
|
|
1479
|
+
Translate labels into the user's locale if known. Do not vary the surfaceId \`awaiting-dream-input\` — the state machine observes it.
|
|
1365
1480
|
|
|
1366
|
-
###
|
|
1481
|
+
### \`[system:dismiss-requested] surfaceId=vision-*\`
|
|
1367
1482
|
|
|
1368
|
-
|
|
1483
|
+
User dismissed a vision poster. Push the fixed dismiss-alt surface below. Wording is fixed: do not paraphrase, translate, or reorder buttons.
|
|
1369
1484
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1485
|
+
\`\`\`
|
|
1486
|
+
surface dismiss-alt-vision-{slug} input
|
|
1487
|
+
card
|
|
1488
|
+
column
|
|
1489
|
+
text "Before I remove this:" h4
|
|
1490
|
+
button "This isn't my dream anymore" action=choice
|
|
1491
|
+
button "Wrong framing" action=choice
|
|
1492
|
+
button "Already achieved" action=choice
|
|
1493
|
+
button "Don't show on dashboard" action=choice
|
|
1494
|
+
button "Remove it" action=choice
|
|
1495
|
+
goal: Capture vision dismiss reason for vision-{slug}
|
|
1496
|
+
followup: +1m "User did not respond to vision dismiss alternatives. Say done."
|
|
1497
|
+
\`\`\`
|
|
1375
1498
|
|
|
1376
|
-
|
|
1499
|
+
After pushing, respond \`done\`.
|
|
1377
1500
|
|
|
1378
|
-
|
|
1501
|
+
When \`[action:choice]\` arrives on \`dismiss-alt-vision-{slug}\`, append one line to your feedback memory via exec, then \`done\`:
|
|
1379
1502
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
-
|
|
1503
|
+
\`\`\`
|
|
1504
|
+
exec mkdir -p ~/.openclaw/workspace-agentlife-builder/memory && \\
|
|
1505
|
+
echo "- $(date -u +%Y-%m-%dT%H:%M:%SZ) vision-{slug}: {reason}" \\
|
|
1506
|
+
>> ~/.openclaw/workspace-agentlife-builder/memory/vision-feedback.md
|
|
1507
|
+
\`\`\`
|
|
1508
|
+
|
|
1509
|
+
This file is injected as "Previously rejected dreams" into the next \`[system:dashboard-bootstrap]\` so future synthesis avoids the same misreads.
|
|
1510
|
+
|
|
1511
|
+
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\`.
|
|
1383
1512
|
|
|
1384
1513
|
## What You Are Not
|
|
1385
1514
|
|
|
@@ -1604,212 +1733,6 @@ ${capped}`);
|
|
|
1604
1733
|
|
|
1605
1734
|
`) };
|
|
1606
1735
|
}
|
|
1607
|
-
var VISION_MIN_MEMORY_CHARS = 200;
|
|
1608
|
-
var BOOTSTRAP_COOLDOWN_MS = 10 * 60 * 1000;
|
|
1609
|
-
var lastBootstrapSentAt = 0;
|
|
1610
|
-
var lastWarmupSentAt = 0;
|
|
1611
|
-
async function ensureVisionPosters(state, runtime, log, options = {}) {
|
|
1612
|
-
if (!state.surfaceDb) {
|
|
1613
|
-
log("[agentlife] ensureVisionPosters: skipped — surfaceDb not initialized");
|
|
1614
|
-
return { status: "skipped", reason: "no_surface_db" };
|
|
1615
|
-
}
|
|
1616
|
-
let visionCount = 0;
|
|
1617
|
-
for (const surfaceId of state.surfaceDb.keys()) {
|
|
1618
|
-
if (surfaceId.startsWith("vision-"))
|
|
1619
|
-
visionCount++;
|
|
1620
|
-
}
|
|
1621
|
-
if (visionCount > 0) {
|
|
1622
|
-
log(`[agentlife] ensureVisionPosters: already_exists — ${visionCount} vision surface(s) in surfaceDb`);
|
|
1623
|
-
return { status: "already_exists", count: visionCount };
|
|
1624
|
-
}
|
|
1625
|
-
const SKIP_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
1626
|
-
const finalList = runtime.config.loadConfig().agents?.list ?? [];
|
|
1627
|
-
const specialistCount = [...state.agentRegistry.keys()].filter((id) => !SKIP_IDS.has(id)).length;
|
|
1628
|
-
if (specialistCount === 0) {
|
|
1629
|
-
log("[agentlife] ensureVisionPosters: skipped — specialistCount=0 (no user agents registered)");
|
|
1630
|
-
return { status: "skipped", reason: "no_specialists" };
|
|
1631
|
-
}
|
|
1632
|
-
if (!state.runCommand) {
|
|
1633
|
-
log("[agentlife] ensureVisionPosters: skipped — runCommand not available on runtime");
|
|
1634
|
-
return { status: "skipped", reason: "no_run_command" };
|
|
1635
|
-
}
|
|
1636
|
-
const { totalChars, sections } = await gatherAllAgentMemory(state, finalList, SKIP_IDS);
|
|
1637
|
-
if (totalChars < VISION_MIN_MEMORY_CHARS) {
|
|
1638
|
-
const warmupNow = Date.now();
|
|
1639
|
-
if (state.runCommand && warmupNow - lastWarmupSentAt >= BOOTSTRAP_COOLDOWN_MS) {
|
|
1640
|
-
const specialistId = [...state.agentRegistry.keys()].find((id) => !SKIP_IDS.has(id));
|
|
1641
|
-
if (specialistId) {
|
|
1642
|
-
lastWarmupSentAt = warmupNow;
|
|
1643
|
-
const warmupKey = buildAgentSessionKey(specialistId);
|
|
1644
|
-
const warmupMsg = [
|
|
1645
|
-
`[system:warmup] You were just created and have no user data yet.`,
|
|
1646
|
-
``,
|
|
1647
|
-
`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.`,
|
|
1648
|
-
`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.`,
|
|
1649
|
-
`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.`,
|
|
1650
|
-
`4. Use the user's language.`,
|
|
1651
|
-
``,
|
|
1652
|
-
`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.`
|
|
1653
|
-
].join(`
|
|
1654
|
-
`);
|
|
1655
|
-
const warmupParams = JSON.stringify({
|
|
1656
|
-
sessionKey: warmupKey,
|
|
1657
|
-
message: warmupMsg,
|
|
1658
|
-
idempotencyKey: `warmup-${specialistId}-${Date.now()}`
|
|
1659
|
-
});
|
|
1660
|
-
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", warmupParams], { timeoutMs: 120000 }).then(() => {
|
|
1661
|
-
log(`[agentlife] ensureVisionPosters: warmup sent to ${specialistId}`);
|
|
1662
|
-
}).catch((e) => {
|
|
1663
|
-
log(`[agentlife] ensureVisionPosters: warmup failed for ${specialistId}: ${e?.message}`);
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
log(`[agentlife] ensureVisionPosters: skipped — totalChars=${totalChars} below threshold ${VISION_MIN_MEMORY_CHARS} (memory too thin)`);
|
|
1668
|
-
return { status: "skipped", reason: "thin_memory", details: `totalChars=${totalChars}` };
|
|
1669
|
-
}
|
|
1670
|
-
const homeDir = options.homedirOverride ?? os.homedir();
|
|
1671
|
-
const builderWorkspace = path3.join(homeDir, ".openclaw", "workspace-agentlife-builder");
|
|
1672
|
-
const feedbackFile = path3.join(builderWorkspace, "memory", "vision-feedback.md");
|
|
1673
|
-
let rejectedDreams = "";
|
|
1674
|
-
try {
|
|
1675
|
-
const content = await fs2.readFile(feedbackFile, "utf-8");
|
|
1676
|
-
if (content.trim())
|
|
1677
|
-
rejectedDreams = content.trim().slice(0, 4000);
|
|
1678
|
-
} catch {}
|
|
1679
|
-
const now = Date.now();
|
|
1680
|
-
if (now - lastBootstrapSentAt < BOOTSTRAP_COOLDOWN_MS) {
|
|
1681
|
-
log(`[agentlife] ensureVisionPosters: skipped — bootstrap cooldown (${Math.round((BOOTSTRAP_COOLDOWN_MS - (now - lastBootstrapSentAt)) / 1000)}s remaining)`);
|
|
1682
|
-
return { status: "skipped", reason: "cooldown", details: `${Math.round((BOOTSTRAP_COOLDOWN_MS - (now - lastBootstrapSentAt)) / 1000)}s remaining` };
|
|
1683
|
-
}
|
|
1684
|
-
const builderKey = buildAgentSessionKey("agentlife-builder");
|
|
1685
|
-
const bootstrapMsg = [
|
|
1686
|
-
`[system:dashboard-bootstrap] The dashboard is empty. Push 3-4 vision posters with agentlife_push.`,
|
|
1687
|
-
``,
|
|
1688
|
-
`## What you do`,
|
|
1689
|
-
``,
|
|
1690
|
-
`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.`,
|
|
1691
|
-
``,
|
|
1692
|
-
`## Picking the dreams`,
|
|
1693
|
-
``,
|
|
1694
|
-
`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.`,
|
|
1695
|
-
``,
|
|
1696
|
-
`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.`,
|
|
1697
|
-
``,
|
|
1698
|
-
`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.`,
|
|
1699
|
-
``,
|
|
1700
|
-
`## Quality bar`,
|
|
1701
|
-
``,
|
|
1702
|
-
`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.`,
|
|
1703
|
-
``,
|
|
1704
|
-
`## Language consistency`,
|
|
1705
|
-
``,
|
|
1706
|
-
`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.`,
|
|
1707
|
-
``,
|
|
1708
|
-
`## The 3 templates`,
|
|
1709
|
-
``,
|
|
1710
|
-
`Every template renders emoji + headline + support + a bottom element. The bottom element changes per template.`,
|
|
1711
|
-
``,
|
|
1712
|
-
`### TEMPLATE A — Statement (best for identity, body, declarative truths)`,
|
|
1713
|
-
``,
|
|
1714
|
-
`surface vision-{slug} size=l`,
|
|
1715
|
-
` card`,
|
|
1716
|
-
` column align=center`,
|
|
1717
|
-
` text "{EMOJI}" h1`,
|
|
1718
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1719
|
-
` text "{SUPPORT}" body`,
|
|
1720
|
-
` text "{MOMENT}" h4 color={ACCENT}`,
|
|
1721
|
-
`goal: Vision poster — {slug}`,
|
|
1722
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1723
|
-
``,
|
|
1724
|
-
`### TEMPLATE B — Destination (best for place, move, new chapter)`,
|
|
1725
|
-
``,
|
|
1726
|
-
`surface vision-{slug} size=l`,
|
|
1727
|
-
` card`,
|
|
1728
|
-
` column align=center`,
|
|
1729
|
-
` text "{EMOJI}" h1`,
|
|
1730
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1731
|
-
` text "{SUPPORT}" body`,
|
|
1732
|
-
` badge "{PLACE}" color={ACCENT} outlined`,
|
|
1733
|
-
`goal: Vision poster — {slug}`,
|
|
1734
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1735
|
-
``,
|
|
1736
|
-
`### TEMPLATE C — Outcome (best for work, money, craft)`,
|
|
1737
|
-
``,
|
|
1738
|
-
`surface vision-{slug} size=l`,
|
|
1739
|
-
` card`,
|
|
1740
|
-
` column align=center`,
|
|
1741
|
-
` text "{EMOJI}" h1`,
|
|
1742
|
-
` text "{HEADLINE}" h1 color={ACCENT}`,
|
|
1743
|
-
` text "{SUPPORT}" body`,
|
|
1744
|
-
` row distribute=spaceEvenly`,
|
|
1745
|
-
` text "{C1}" h4 color={ACCENT}`,
|
|
1746
|
-
` text "·" h4`,
|
|
1747
|
-
` text "{C2}" h4 color={ACCENT}`,
|
|
1748
|
-
` text "·" h4`,
|
|
1749
|
-
` text "{C3}" h4 color={ACCENT}`,
|
|
1750
|
-
`goal: Vision poster — {slug}`,
|
|
1751
|
-
`followup: +30d "Refresh this vision poster if the user's data has shifted."`,
|
|
1752
|
-
``,
|
|
1753
|
-
`## Placeholder rules`,
|
|
1754
|
-
``,
|
|
1755
|
-
`- {slug}: short kebab-case slug for the life area`,
|
|
1756
|
-
`- {ACCENT}: hex color. Vary across posters: try #10B981, #7C3AED, #3B82F6, #F59E0B, #EC4899, #14B8A6`,
|
|
1757
|
-
`- {EMOJI}: a single emoji capturing the emotional core. Visceral, not literal.`,
|
|
1758
|
-
`- {HEADLINE}: 3-5 words, declarative, first-person if natural, periods welcome. The dream version, not the next checkpoint.`,
|
|
1759
|
-
`- {SUPPORT}: 6-12 words, one short sentence. Concrete supporting context in the user's actual language.`,
|
|
1760
|
-
`- {MOMENT} (Template A): 1-3 words, a time anchor — season, year, phase`,
|
|
1761
|
-
`- {PLACE} (Template B): 1-3 words, must be a real geographic place name (not a date)`,
|
|
1762
|
-
`- {C1}, {C2}, {C3} (Template C): 1-2 words each, concept beats from the user's vocabulary`,
|
|
1763
|
-
``,
|
|
1764
|
-
`## Variety rule`,
|
|
1765
|
-
``,
|
|
1766
|
-
`Across the 3-4 posters, use AT LEAST 2 different templates.`,
|
|
1767
|
-
``,
|
|
1768
|
-
`## Hard constraints`,
|
|
1769
|
-
``,
|
|
1770
|
-
`- Copy ONE of the 3 templates above exactly. Do not add components. Do not remove components. Do not change order.`,
|
|
1771
|
-
`- Use the user's actual language from the data — never invent.`,
|
|
1772
|
-
``,
|
|
1773
|
-
...rejectedDreams ? [
|
|
1774
|
-
`## Previously rejected dreams`,
|
|
1775
|
-
``,
|
|
1776
|
-
`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.`,
|
|
1777
|
-
``,
|
|
1778
|
-
rejectedDreams,
|
|
1779
|
-
``
|
|
1780
|
-
] : [],
|
|
1781
|
-
`## USER DATA`,
|
|
1782
|
-
``,
|
|
1783
|
-
sections.slice(0, 12000),
|
|
1784
|
-
``,
|
|
1785
|
-
`## Output`,
|
|
1786
|
-
``,
|
|
1787
|
-
`Push each poster with agentlife_push. After the last push, respond "Done".`
|
|
1788
|
-
].join(`
|
|
1789
|
-
`);
|
|
1790
|
-
const params = JSON.stringify({
|
|
1791
|
-
sessionKey: builderKey,
|
|
1792
|
-
message: bootstrapMsg,
|
|
1793
|
-
idempotencyKey: `dashboard-bootstrap-${Date.now()}`
|
|
1794
|
-
});
|
|
1795
|
-
lastBootstrapSentAt = Date.now();
|
|
1796
|
-
const delayMs = options.delayMs ?? 0;
|
|
1797
|
-
const feedbackChars = rejectedDreams.length;
|
|
1798
|
-
const runBootstrap = () => {
|
|
1799
|
-
log(`[agentlife] ensureVisionPosters: synthesizing (${totalChars} chars memory across ${specialistCount} specialists${feedbackChars > 0 ? `, ${feedbackChars} chars prior feedback` : ""})`);
|
|
1800
|
-
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", params], { timeoutMs: 300000 }).then(() => {
|
|
1801
|
-
log("[agentlife] ensureVisionPosters: dashboard bootstrap complete");
|
|
1802
|
-
}).catch((e) => {
|
|
1803
|
-
log(`[agentlife] ensureVisionPosters: dashboard bootstrap failed: ${e?.message}`);
|
|
1804
|
-
});
|
|
1805
|
-
};
|
|
1806
|
-
if (delayMs > 0) {
|
|
1807
|
-
setTimeout(runBootstrap, delayMs);
|
|
1808
|
-
} else {
|
|
1809
|
-
runBootstrap();
|
|
1810
|
-
}
|
|
1811
|
-
return { status: "synthesizing", specialistCount, totalChars, feedbackChars };
|
|
1812
|
-
}
|
|
1813
1736
|
async function provisionAgents(state, cfg, runtime, log) {
|
|
1814
1737
|
const home = os.homedir();
|
|
1815
1738
|
const currentList = [...cfg.agents?.list ?? []];
|
|
@@ -1876,10 +1799,12 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1876
1799
|
log(`[agentlife] backfilled subagents for ${agent.id}`);
|
|
1877
1800
|
}
|
|
1878
1801
|
}
|
|
1879
|
-
const
|
|
1802
|
+
const configPath = path3.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
1803
|
+
const rawCfgForVisibility = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1804
|
+
const backupPath = path3.join(os.homedir(), ".openclaw", "agentlife", "config-backup.json");
|
|
1805
|
+
let visibilityWritten = false;
|
|
1880
1806
|
const currentVisibility = rawCfgForVisibility?.tools?.sessions?.visibility;
|
|
1881
1807
|
if (currentVisibility !== "all") {
|
|
1882
|
-
const backupPath = path3.join(os.homedir(), ".openclaw", "agentlife", "config-backup.json");
|
|
1883
1808
|
try {
|
|
1884
1809
|
let backup = {};
|
|
1885
1810
|
try {
|
|
@@ -1894,14 +1819,40 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1894
1819
|
if (!rawCfgForVisibility.tools.sessions)
|
|
1895
1820
|
rawCfgForVisibility.tools.sessions = {};
|
|
1896
1821
|
rawCfgForVisibility.tools.sessions.visibility = "all";
|
|
1897
|
-
|
|
1822
|
+
visibilityWritten = true;
|
|
1823
|
+
}
|
|
1824
|
+
const currentAllow = rawCfgForVisibility?.tools?.allow;
|
|
1825
|
+
let globalAllowWritten = false;
|
|
1826
|
+
if (Array.isArray(currentAllow) && currentAllow.length > 0 && !currentAllow.includes("*")) {
|
|
1827
|
+
const onlyPluginTool = currentAllow.length === 1 && currentAllow[0] === "agentlife_push";
|
|
1828
|
+
if (onlyPluginTool) {
|
|
1829
|
+
try {
|
|
1830
|
+
let backup = {};
|
|
1831
|
+
try {
|
|
1832
|
+
backup = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
1833
|
+
} catch {}
|
|
1834
|
+
if (backup.toolsAllow === undefined) {
|
|
1835
|
+
backup.toolsAllow = [...currentAllow];
|
|
1836
|
+
writeFileSync(backupPath, JSON.stringify(backup, null, 2) + `
|
|
1837
|
+
`, "utf-8");
|
|
1838
|
+
}
|
|
1839
|
+
} catch {}
|
|
1840
|
+
rawCfgForVisibility.tools.allow = ["*", "agentlife_push"];
|
|
1841
|
+
globalAllowWritten = true;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (visibilityWritten || globalAllowWritten) {
|
|
1845
|
+
writeFileSync(configPath, JSON.stringify(rawCfgForVisibility, null, 2) + `
|
|
1898
1846
|
`, "utf-8");
|
|
1899
1847
|
configChanged = true;
|
|
1900
|
-
|
|
1848
|
+
if (visibilityWritten)
|
|
1849
|
+
log("[agentlife] set tools.sessions.visibility=all (cross-agent delegation)");
|
|
1850
|
+
if (globalAllowWritten)
|
|
1851
|
+
log('[agentlife] added "*" to tools.allow (unblock exec/read/write for provisioned agents)');
|
|
1901
1852
|
}
|
|
1902
1853
|
if (configChanged) {
|
|
1903
|
-
const
|
|
1904
|
-
const rawCfg = JSON.parse(readFileSync(
|
|
1854
|
+
const configPath2 = path3.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
1855
|
+
const rawCfg = JSON.parse(readFileSync(configPath2, "utf-8"));
|
|
1905
1856
|
const updatedCfg = {
|
|
1906
1857
|
...rawCfg,
|
|
1907
1858
|
agents: {
|
|
@@ -1909,7 +1860,7 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1909
1860
|
list: currentList
|
|
1910
1861
|
}
|
|
1911
1862
|
};
|
|
1912
|
-
writeFileSync(
|
|
1863
|
+
writeFileSync(configPath2, JSON.stringify(updatedCfg, null, 2) + `
|
|
1913
1864
|
`, "utf-8");
|
|
1914
1865
|
log("[agentlife] config updated with provisioned agents");
|
|
1915
1866
|
}
|
|
@@ -1987,12 +1938,538 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1987
1938
|
}, 1e4);
|
|
1988
1939
|
}
|
|
1989
1940
|
}
|
|
1990
|
-
await ensureVisionPosters(state, runtime, log, { delayMs: 15000 });
|
|
1991
1941
|
log("[agentlife] agent provisioning complete");
|
|
1992
1942
|
}
|
|
1993
1943
|
|
|
1994
|
-
//
|
|
1944
|
+
// cold-start.ts
|
|
1945
|
+
import * as fs3 from "node:fs/promises";
|
|
1946
|
+
import * as nodeFs from "node:fs";
|
|
1947
|
+
import * as os2 from "node:os";
|
|
1995
1948
|
import * as path4 from "node:path";
|
|
1949
|
+
var DEADLINE_MS = {
|
|
1950
|
+
AWAITING_AGENT: 30 * 60 * 1000,
|
|
1951
|
+
AWAITING_BASELINE: 30 * 60 * 1000,
|
|
1952
|
+
SYNTHESIZING: 3 * 60 * 1000,
|
|
1953
|
+
AWAITING_DREAM: 30 * 60 * 1000
|
|
1954
|
+
};
|
|
1955
|
+
var VISION_MIN_MEMORY_CHARS = 200;
|
|
1956
|
+
var PHASE_BLANK = "BLANK";
|
|
1957
|
+
function readState(state) {
|
|
1958
|
+
const db = getOrCreateHistoryDb(state);
|
|
1959
|
+
const row = db.prepare("SELECT * FROM cold_start_state WHERE id = 1").get();
|
|
1960
|
+
if (!row)
|
|
1961
|
+
return null;
|
|
1962
|
+
let warnings = [];
|
|
1963
|
+
if (row.contractWarnings) {
|
|
1964
|
+
try {
|
|
1965
|
+
warnings = JSON.parse(row.contractWarnings);
|
|
1966
|
+
} catch {}
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
phase: row.phase,
|
|
1970
|
+
enteredAt: row.enteredAt,
|
|
1971
|
+
lastActionAt: row.lastActionAt ?? null,
|
|
1972
|
+
ackAt: row.ackAt ?? null,
|
|
1973
|
+
deadlineAt: row.deadlineAt ?? null,
|
|
1974
|
+
retryCount: row.retryCount ?? 0,
|
|
1975
|
+
failureReason: row.failureReason ?? null,
|
|
1976
|
+
lastTrigger: row.lastTrigger ?? null,
|
|
1977
|
+
actionSessionKey: row.actionSessionKey ?? null,
|
|
1978
|
+
contractWarnings: warnings,
|
|
1979
|
+
pollerLastTickAt: row.pollerLastTickAt ?? null,
|
|
1980
|
+
updatedAt: row.updatedAt
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function ensureInitialRow(state) {
|
|
1984
|
+
const existing = readState(state);
|
|
1985
|
+
if (existing)
|
|
1986
|
+
return existing;
|
|
1987
|
+
const now = Date.now();
|
|
1988
|
+
const db = getOrCreateHistoryDb(state);
|
|
1989
|
+
db.prepare(`
|
|
1990
|
+
INSERT OR IGNORE INTO cold_start_state
|
|
1991
|
+
(id, phase, enteredAt, retryCount, contractWarnings, updatedAt)
|
|
1992
|
+
VALUES (1, ?, ?, 0, '[]', ?)
|
|
1993
|
+
`).run(PHASE_BLANK, now, now);
|
|
1994
|
+
return readState(state);
|
|
1995
|
+
}
|
|
1996
|
+
function writeState(state, partial) {
|
|
1997
|
+
const current = ensureInitialRow(state);
|
|
1998
|
+
const next = { ...current, ...partial, updatedAt: Date.now() };
|
|
1999
|
+
const db = getOrCreateHistoryDb(state);
|
|
2000
|
+
db.prepare(`
|
|
2001
|
+
UPDATE cold_start_state SET
|
|
2002
|
+
phase = ?,
|
|
2003
|
+
enteredAt = ?,
|
|
2004
|
+
lastActionAt = ?,
|
|
2005
|
+
ackAt = ?,
|
|
2006
|
+
deadlineAt = ?,
|
|
2007
|
+
retryCount = ?,
|
|
2008
|
+
failureReason = ?,
|
|
2009
|
+
lastTrigger = ?,
|
|
2010
|
+
actionSessionKey = ?,
|
|
2011
|
+
contractWarnings = ?,
|
|
2012
|
+
pollerLastTickAt = ?,
|
|
2013
|
+
updatedAt = ?
|
|
2014
|
+
WHERE id = 1
|
|
2015
|
+
`).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);
|
|
2016
|
+
return next;
|
|
2017
|
+
}
|
|
2018
|
+
function emitTrigger(state, kind, payload) {
|
|
2019
|
+
if (state.disabled)
|
|
2020
|
+
return;
|
|
2021
|
+
try {
|
|
2022
|
+
const db = getOrCreateHistoryDb(state);
|
|
2023
|
+
db.prepare(`
|
|
2024
|
+
INSERT INTO cold_start_triggers (kind, payload, createdAt) VALUES (?, ?, ?)
|
|
2025
|
+
`).run(kind, JSON.stringify(payload), Date.now());
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
console.warn("[cold-start] emitTrigger failed:", e?.message);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function drainTriggers(state, limit = 50) {
|
|
2031
|
+
const db = getOrCreateHistoryDb(state);
|
|
2032
|
+
const rows = db.prepare("SELECT id, kind, payload FROM cold_start_triggers ORDER BY createdAt ASC LIMIT ?").all(limit);
|
|
2033
|
+
if (rows.length === 0)
|
|
2034
|
+
return [];
|
|
2035
|
+
const ids = rows.map((r) => r.id);
|
|
2036
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
2037
|
+
db.prepare(`DELETE FROM cold_start_triggers WHERE id IN (${placeholders})`).run(...ids);
|
|
2038
|
+
return rows.map((r) => ({
|
|
2039
|
+
kind: r.kind,
|
|
2040
|
+
payload: safeJson(r.payload)
|
|
2041
|
+
}));
|
|
2042
|
+
}
|
|
2043
|
+
function safeJson(s) {
|
|
2044
|
+
try {
|
|
2045
|
+
return JSON.parse(s);
|
|
2046
|
+
} catch {
|
|
2047
|
+
return {};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
function purgeStaleTriggers(state) {
|
|
2051
|
+
const db = getOrCreateHistoryDb(state);
|
|
2052
|
+
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
2053
|
+
const r = db.prepare("DELETE FROM cold_start_triggers WHERE createdAt < ?").run(cutoff);
|
|
2054
|
+
return r.changes;
|
|
2055
|
+
}
|
|
2056
|
+
function pendingTriggerCount(state) {
|
|
2057
|
+
const db = getOrCreateHistoryDb(state);
|
|
2058
|
+
const row = db.prepare("SELECT COUNT(*) as c FROM cold_start_triggers").get();
|
|
2059
|
+
return row?.c ?? 0;
|
|
2060
|
+
}
|
|
2061
|
+
var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
2062
|
+
function listSpecialistIds(state, runtime) {
|
|
2063
|
+
const cfg = runtime.config.loadConfig();
|
|
2064
|
+
const list = cfg?.agents?.list ?? [];
|
|
2065
|
+
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2066
|
+
}
|
|
2067
|
+
async function specialistMemoryTotal(state, runtime) {
|
|
2068
|
+
const cfg = runtime.config.loadConfig();
|
|
2069
|
+
const list = cfg?.agents?.list ?? [];
|
|
2070
|
+
const { totalChars, sections } = await gatherAllAgentMemory(state, list, PROVISIONED_IDS);
|
|
2071
|
+
return { total: totalChars, sections };
|
|
2072
|
+
}
|
|
2073
|
+
function visionSurfaceCount(state) {
|
|
2074
|
+
if (!state.surfaceDb)
|
|
2075
|
+
return 0;
|
|
2076
|
+
let n = 0;
|
|
2077
|
+
for (const id of state.surfaceDb.keys())
|
|
2078
|
+
if (id.startsWith("vision-"))
|
|
2079
|
+
n++;
|
|
2080
|
+
return n;
|
|
2081
|
+
}
|
|
2082
|
+
async function sendSystemMessage(state, agentId, message, idempotencyKey, log) {
|
|
2083
|
+
if (!state.runCommand) {
|
|
2084
|
+
log(`[cold-start] sendSystemMessage ${idempotencyKey}: skipped — runCommand not available`);
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
const sessionKey = buildAgentSessionKey(agentId);
|
|
2088
|
+
const chatParams = JSON.stringify({ sessionKey, message, idempotencyKey });
|
|
2089
|
+
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 120000 }).then(() => {
|
|
2090
|
+
log(`[cold-start] sent ${idempotencyKey} → ${sessionKey}`);
|
|
2091
|
+
}).catch((e) => {
|
|
2092
|
+
log(`[cold-start] sendSystemMessage ${idempotencyKey} failed: ${e?.message}`);
|
|
2093
|
+
});
|
|
2094
|
+
return sessionKey;
|
|
2095
|
+
}
|
|
2096
|
+
async function composeBootstrapMessage(state, runtime, userDream) {
|
|
2097
|
+
const { sections } = await specialistMemoryTotal(state, runtime);
|
|
2098
|
+
const rejected = await readRejectedDreams();
|
|
2099
|
+
const parts = ["[system:dashboard-bootstrap]", "", "## USER DATA", "", sections.slice(0, 12000)];
|
|
2100
|
+
if (rejected)
|
|
2101
|
+
parts.push("", "## Previously rejected dreams", "", rejected);
|
|
2102
|
+
if (userDream)
|
|
2103
|
+
parts.push("", "## userDream", "", userDream);
|
|
2104
|
+
return parts.join(`
|
|
2105
|
+
`);
|
|
2106
|
+
}
|
|
2107
|
+
async function readRejectedDreams() {
|
|
2108
|
+
const builderWorkspace = path4.join(os2.homedir(), ".openclaw", "workspace-agentlife-builder");
|
|
2109
|
+
const feedbackFile = path4.join(builderWorkspace, "memory", "vision-feedback.md");
|
|
2110
|
+
try {
|
|
2111
|
+
const content = await fs3.readFile(feedbackFile, "utf-8");
|
|
2112
|
+
return content.trim().slice(0, 4000);
|
|
2113
|
+
} catch {
|
|
2114
|
+
return "";
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
async function computeStartingPhase(state, runtime) {
|
|
2118
|
+
if (visionSurfaceCount(state) > 0) {
|
|
2119
|
+
return { phase: "READY", details: "vision surfaces already exist" };
|
|
2120
|
+
}
|
|
2121
|
+
const specialists = listSpecialistIds(state, runtime);
|
|
2122
|
+
if (specialists.length === 0) {
|
|
2123
|
+
return { phase: "BLANK", details: "no specialists" };
|
|
2124
|
+
}
|
|
2125
|
+
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2126
|
+
if (total < VISION_MIN_MEMORY_CHARS) {
|
|
2127
|
+
return { phase: "AWAITING_BASELINE", details: `totalChars=${total} below ${VISION_MIN_MEMORY_CHARS}` };
|
|
2128
|
+
}
|
|
2129
|
+
return { phase: "SYNTHESIZING", details: `totalChars=${total} ready for synthesis` };
|
|
2130
|
+
}
|
|
2131
|
+
async function transition(state, runtime, log, trigger) {
|
|
2132
|
+
const cur = ensureInitialRow(state);
|
|
2133
|
+
log(`[cold-start] trigger=${trigger.kind} phase=${cur.phase}`);
|
|
2134
|
+
switch (trigger.kind) {
|
|
2135
|
+
case "provisioned":
|
|
2136
|
+
case "userRetried": {
|
|
2137
|
+
const { phase, details } = await computeStartingPhase(state, runtime);
|
|
2138
|
+
log(`[cold-start] recompute → ${phase} (${details})`);
|
|
2139
|
+
return enterPhase(state, runtime, log, phase, {
|
|
2140
|
+
bumpRetry: trigger.kind === "userRetried"
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
case "agentRegistered": {
|
|
2144
|
+
if (cur.phase === "BLANK" || cur.phase === "AWAITING_AGENT") {
|
|
2145
|
+
return enterPhase(state, runtime, log, "AWAITING_BASELINE", {
|
|
2146
|
+
specialistId: trigger.payload.agentId
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
return cur;
|
|
2150
|
+
}
|
|
2151
|
+
case "agentDeleted": {
|
|
2152
|
+
const remaining = trigger.payload.remaining;
|
|
2153
|
+
if (remaining === 0) {
|
|
2154
|
+
return enterPhase(state, runtime, log, "BLANK");
|
|
2155
|
+
}
|
|
2156
|
+
return cur;
|
|
2157
|
+
}
|
|
2158
|
+
case "memoryWritten": {
|
|
2159
|
+
if (cur.phase !== "AWAITING_BASELINE")
|
|
2160
|
+
return cur;
|
|
2161
|
+
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2162
|
+
if (total >= VISION_MIN_MEMORY_CHARS) {
|
|
2163
|
+
return enterPhase(state, runtime, log, "SYNTHESIZING");
|
|
2164
|
+
}
|
|
2165
|
+
log(`[cold-start] memory still thin: ${total}/${VISION_MIN_MEMORY_CHARS}`);
|
|
2166
|
+
return cur;
|
|
2167
|
+
}
|
|
2168
|
+
case "surfacePushed": {
|
|
2169
|
+
return cur;
|
|
2170
|
+
}
|
|
2171
|
+
case "agentReplied": {
|
|
2172
|
+
const sessionKey = trigger.payload.sessionKey;
|
|
2173
|
+
const marker = trigger.payload.marker;
|
|
2174
|
+
const lastText = trigger.payload.lastText ?? "";
|
|
2175
|
+
if (cur.actionSessionKey && sessionKey && sessionKey !== cur.actionSessionKey) {
|
|
2176
|
+
return cur;
|
|
2177
|
+
}
|
|
2178
|
+
if (cur.phase === "SYNTHESIZING") {
|
|
2179
|
+
if (marker === "NO_SIGNAL" || /\bNO_SIGNAL\b/.test(lastText)) {
|
|
2180
|
+
return enterPhase(state, runtime, log, "AWAITING_DREAM");
|
|
2181
|
+
}
|
|
2182
|
+
if (visionSurfaceCount(state) >= 1) {
|
|
2183
|
+
return enterPhase(state, runtime, log, "READY");
|
|
2184
|
+
}
|
|
2185
|
+
return cur;
|
|
2186
|
+
}
|
|
2187
|
+
if (cur.phase === "AWAITING_BASELINE") {
|
|
2188
|
+
return cur;
|
|
2189
|
+
}
|
|
2190
|
+
return cur;
|
|
2191
|
+
}
|
|
2192
|
+
case "userAnswered": {
|
|
2193
|
+
const surfaceId = trigger.payload.surfaceId;
|
|
2194
|
+
const answer = trigger.payload.answer ?? "";
|
|
2195
|
+
if (cur.phase === "AWAITING_DREAM" && surfaceId === "awaiting-dream-input") {
|
|
2196
|
+
return enterPhase(state, runtime, log, "SYNTHESIZING", { userDream: answer });
|
|
2197
|
+
}
|
|
2198
|
+
return cur;
|
|
2199
|
+
}
|
|
2200
|
+
case "deadlineElapsed": {
|
|
2201
|
+
const elapsedPhase = trigger.payload.phase;
|
|
2202
|
+
if (elapsedPhase !== cur.phase)
|
|
2203
|
+
return cur;
|
|
2204
|
+
const reason = `${cur.phase.toLowerCase()}_timeout`;
|
|
2205
|
+
return enterFailed(state, log, reason);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
async function enterPhase(state, runtime, log, next, opts = {}) {
|
|
2210
|
+
const cur = ensureInitialRow(state);
|
|
2211
|
+
const now = Date.now();
|
|
2212
|
+
const deadlineMs = DEADLINE_MS[next];
|
|
2213
|
+
const deadlineAt = deadlineMs ? now + deadlineMs : null;
|
|
2214
|
+
const retryCount = opts.bumpRetry ? cur.retryCount + 1 : cur.retryCount;
|
|
2215
|
+
let actionSessionKey = null;
|
|
2216
|
+
let lastActionAt = null;
|
|
2217
|
+
writeState(state, {
|
|
2218
|
+
phase: next,
|
|
2219
|
+
enteredAt: now,
|
|
2220
|
+
deadlineAt,
|
|
2221
|
+
retryCount,
|
|
2222
|
+
failureReason: next === "FAILED" ? cur.failureReason : null,
|
|
2223
|
+
actionSessionKey: null,
|
|
2224
|
+
lastActionAt: null,
|
|
2225
|
+
ackAt: null
|
|
2226
|
+
});
|
|
2227
|
+
log(`[cold-start] enter ${next} (deadline=${deadlineAt ? new Date(deadlineAt).toISOString() : "none"}, retry=${retryCount})`);
|
|
2228
|
+
if (next === "BLANK") {
|
|
2229
|
+
const idem = `cold-start-onboarding-${now}-${retryCount}`;
|
|
2230
|
+
const msg = [
|
|
2231
|
+
"[system:onboarding]",
|
|
2232
|
+
"",
|
|
2233
|
+
"User just installed AgentLife and has zero specialist agents. Push the",
|
|
2234
|
+
"onboarding-domain input surface EXACTLY as specified in the [system:onboarding]",
|
|
2235
|
+
"section of your AGENTS.md, then respond `done`. Do NOT ask the user questions",
|
|
2236
|
+
"as chat text — only via the input surface. Do NOT create any specialist agent",
|
|
2237
|
+
"yet; wait for the user's choice on the surface."
|
|
2238
|
+
].join(`
|
|
2239
|
+
`);
|
|
2240
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-builder", msg, idem, log);
|
|
2241
|
+
lastActionAt = Date.now();
|
|
2242
|
+
writeState(state, {
|
|
2243
|
+
phase: "AWAITING_AGENT",
|
|
2244
|
+
enteredAt: now,
|
|
2245
|
+
deadlineAt: now + (DEADLINE_MS.AWAITING_AGENT ?? 0),
|
|
2246
|
+
actionSessionKey,
|
|
2247
|
+
lastActionAt
|
|
2248
|
+
});
|
|
2249
|
+
scheduleDeadline(state, runtime, log, "AWAITING_AGENT", now + (DEADLINE_MS.AWAITING_AGENT ?? 0));
|
|
2250
|
+
return readState(state);
|
|
2251
|
+
}
|
|
2252
|
+
if (next === "AWAITING_BASELINE") {
|
|
2253
|
+
const specialistId = opts.specialistId ?? listSpecialistIds(state, runtime)[0];
|
|
2254
|
+
if (!specialistId) {
|
|
2255
|
+
log("[cold-start] AWAITING_BASELINE has no specialist to warm up — falling back to BLANK");
|
|
2256
|
+
return enterPhase(state, runtime, log, "BLANK");
|
|
2257
|
+
}
|
|
2258
|
+
const idem = `cold-start-start-${specialistId}-${now}-${retryCount}`;
|
|
2259
|
+
const msg = [
|
|
2260
|
+
"[system:start]",
|
|
2261
|
+
"",
|
|
2262
|
+
"You were just created by the builder. You have zero memory about this user.",
|
|
2263
|
+
"Follow the [system:start] section of your AGENTS.md: push a loading widget",
|
|
2264
|
+
"introducing yourself per IDENTITY.md, then push ONE warmup-* input surface",
|
|
2265
|
+
"asking the most important baseline question for your domain, then respond",
|
|
2266
|
+
"`done`. Do NOT ask multiple questions in chat text — only via input surfaces."
|
|
2267
|
+
].join(`
|
|
2268
|
+
`);
|
|
2269
|
+
actionSessionKey = await sendSystemMessage(state, specialistId, msg, idem, log);
|
|
2270
|
+
lastActionAt = Date.now();
|
|
2271
|
+
}
|
|
2272
|
+
if (next === "SYNTHESIZING") {
|
|
2273
|
+
const msg = await composeBootstrapMessage(state, runtime, opts.userDream);
|
|
2274
|
+
const idem = `cold-start-bootstrap-${now}-${retryCount}`;
|
|
2275
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-builder", msg, idem, log);
|
|
2276
|
+
lastActionAt = Date.now();
|
|
2277
|
+
}
|
|
2278
|
+
if (next === "AWAITING_DREAM") {
|
|
2279
|
+
const idem = `cold-start-ask-dream-${now}-${retryCount}`;
|
|
2280
|
+
const msg = [
|
|
2281
|
+
"[system:ask-dream]",
|
|
2282
|
+
"",
|
|
2283
|
+
"Bootstrap reported NO_SIGNAL — the user's data has no aspirational content yet.",
|
|
2284
|
+
"Push the awaiting-dream-input input surface EXACTLY as specified in the",
|
|
2285
|
+
"[system:ask-dream] section of your AGENTS.md, then respond `done`. Do NOT push",
|
|
2286
|
+
"any vision posters from this turn — the plugin re-fires [system:dashboard-bootstrap]",
|
|
2287
|
+
"with userDream: injected after the user answers."
|
|
2288
|
+
].join(`
|
|
2289
|
+
`);
|
|
2290
|
+
actionSessionKey = await sendSystemMessage(state, "agentlife-builder", msg, idem, log);
|
|
2291
|
+
lastActionAt = Date.now();
|
|
2292
|
+
}
|
|
2293
|
+
writeState(state, { actionSessionKey, lastActionAt });
|
|
2294
|
+
if (deadlineAt)
|
|
2295
|
+
scheduleDeadline(state, runtime, log, next, deadlineAt);
|
|
2296
|
+
return readState(state);
|
|
2297
|
+
}
|
|
2298
|
+
function enterFailed(state, log, reason) {
|
|
2299
|
+
log(`[cold-start] FAILED: ${reason}`);
|
|
2300
|
+
return writeState(state, {
|
|
2301
|
+
phase: "FAILED",
|
|
2302
|
+
enteredAt: Date.now(),
|
|
2303
|
+
failureReason: reason,
|
|
2304
|
+
deadlineAt: null,
|
|
2305
|
+
actionSessionKey: null
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
var deadlineTimers = new Map;
|
|
2309
|
+
function scheduleDeadline(state, runtime, log, phase, deadlineAt) {
|
|
2310
|
+
const key = `${phase}-${deadlineAt}`;
|
|
2311
|
+
for (const [k, t] of deadlineTimers.entries()) {
|
|
2312
|
+
if (k.startsWith(`${phase}-`)) {
|
|
2313
|
+
clearTimeout(t);
|
|
2314
|
+
deadlineTimers.delete(k);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const remaining = Math.max(0, deadlineAt - Date.now());
|
|
2318
|
+
const timer = setTimeout(() => {
|
|
2319
|
+
deadlineTimers.delete(key);
|
|
2320
|
+
emitTrigger(state, "deadlineElapsed", { phase });
|
|
2321
|
+
}, remaining);
|
|
2322
|
+
deadlineTimers.set(key, timer);
|
|
2323
|
+
log(`[cold-start] scheduled ${phase} deadline in ${Math.round(remaining / 1000)}s`);
|
|
2324
|
+
}
|
|
2325
|
+
function rearmDeadlines(state, runtime, log) {
|
|
2326
|
+
const cur = ensureInitialRow(state);
|
|
2327
|
+
if (cur.deadlineAt && cur.phase !== "READY" && cur.phase !== "FAILED") {
|
|
2328
|
+
if (cur.deadlineAt <= Date.now()) {
|
|
2329
|
+
emitTrigger(state, "deadlineElapsed", { phase: cur.phase });
|
|
2330
|
+
} else {
|
|
2331
|
+
scheduleDeadline(state, runtime, log, cur.phase, cur.deadlineAt);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
var pollerTimer = null;
|
|
2336
|
+
var POLLER_INTERVAL_MS = 1000;
|
|
2337
|
+
var POLLER_STUCK_THRESHOLD_MS = 30000;
|
|
2338
|
+
var machineRuntime = null;
|
|
2339
|
+
var machineLog = null;
|
|
2340
|
+
var machineState = null;
|
|
2341
|
+
function startPoller(state, runtime, log) {
|
|
2342
|
+
if (pollerTimer)
|
|
2343
|
+
return;
|
|
2344
|
+
let lastDrainAt = Date.now();
|
|
2345
|
+
let lastWarnedAt = 0;
|
|
2346
|
+
pollerTimer = setInterval(async () => {
|
|
2347
|
+
if (state.disabled)
|
|
2348
|
+
return;
|
|
2349
|
+
try {
|
|
2350
|
+
const triggers = drainTriggers(state, 50);
|
|
2351
|
+
const now = Date.now();
|
|
2352
|
+
writeState(state, { pollerLastTickAt: now });
|
|
2353
|
+
if (triggers.length > 0) {
|
|
2354
|
+
lastDrainAt = now;
|
|
2355
|
+
for (const trigger of triggers) {
|
|
2356
|
+
try {
|
|
2357
|
+
await transition(state, runtime, log, trigger);
|
|
2358
|
+
} catch (e) {
|
|
2359
|
+
log(`[cold-start] transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
purgeStaleTriggers(state);
|
|
2363
|
+
} else {
|
|
2364
|
+
const pending = pendingTriggerCount(state);
|
|
2365
|
+
if (pending > 0 && now - lastDrainAt > POLLER_STUCK_THRESHOLD_MS && now - lastWarnedAt > POLLER_STUCK_THRESHOLD_MS) {
|
|
2366
|
+
log(`[cold-start] WARNING: poller has not drained ${pending} pending trigger(s) for ${Math.round((now - lastDrainAt) / 1000)}s`);
|
|
2367
|
+
lastWarnedAt = now;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
} catch (e) {
|
|
2371
|
+
log(`[cold-start] poller tick error: ${e?.message ?? e}`);
|
|
2372
|
+
}
|
|
2373
|
+
}, POLLER_INTERVAL_MS);
|
|
2374
|
+
}
|
|
2375
|
+
function stopPoller() {
|
|
2376
|
+
if (pollerTimer) {
|
|
2377
|
+
clearInterval(pollerTimer);
|
|
2378
|
+
pollerTimer = null;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function runRuntimeContractProbe(state, runtime, log) {
|
|
2382
|
+
const PLUGIN_REGISTERED_TOOLS = ["agentlife_push"];
|
|
2383
|
+
let list = [];
|
|
2384
|
+
try {
|
|
2385
|
+
const raw = nodeFs.readFileSync(path4.join(os2.homedir(), ".openclaw", "openclaw.json"), "utf-8");
|
|
2386
|
+
const parsed = JSON.parse(raw);
|
|
2387
|
+
list = parsed?.agents?.list ?? [];
|
|
2388
|
+
} catch {
|
|
2389
|
+
list = runtime.config.loadConfig()?.agents?.list ?? [];
|
|
2390
|
+
}
|
|
2391
|
+
const warnings = [];
|
|
2392
|
+
for (const provisioned of PROVISIONED_AGENTS) {
|
|
2393
|
+
if (provisioned.existingAgent)
|
|
2394
|
+
continue;
|
|
2395
|
+
const live = list.find((a) => a?.id === provisioned.id);
|
|
2396
|
+
const liveTools = live?.tools ?? {};
|
|
2397
|
+
const allow = Array.isArray(liveTools.allow) ? liveTools.allow : [];
|
|
2398
|
+
const alsoAllow = Array.isArray(liveTools.alsoAllow) ? liveTools.alsoAllow : [];
|
|
2399
|
+
const deny = Array.isArray(liveTools.deny) ? liveTools.deny : [];
|
|
2400
|
+
for (const tool of PLUGIN_REGISTERED_TOOLS) {
|
|
2401
|
+
const escaped = tool.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
2402
|
+
const re = new RegExp("`[^`]*\\b" + escaped + "\\b[^`]*`");
|
|
2403
|
+
if (!re.test(provisioned.agentsMd))
|
|
2404
|
+
continue;
|
|
2405
|
+
const granted = (allow.includes(tool) || alsoAllow.includes(tool)) && !deny.includes(tool);
|
|
2406
|
+
if (!granted) {
|
|
2407
|
+
const w = `precondition_tool_missing:${provisioned.id}:${tool}`;
|
|
2408
|
+
warnings.push(w);
|
|
2409
|
+
log(`[cold-start] contract warning: ${w}`);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
return warnings;
|
|
2414
|
+
}
|
|
2415
|
+
async function initColdStartMachine(state, runtime, log) {
|
|
2416
|
+
machineState = state;
|
|
2417
|
+
machineRuntime = runtime;
|
|
2418
|
+
machineLog = log;
|
|
2419
|
+
ensureInitialRow(state);
|
|
2420
|
+
const warnings = runRuntimeContractProbe(state, runtime, log);
|
|
2421
|
+
writeState(state, { contractWarnings: warnings });
|
|
2422
|
+
rearmDeadlines(state, runtime, log);
|
|
2423
|
+
startPoller(state, runtime, log);
|
|
2424
|
+
const cur = readState(state);
|
|
2425
|
+
if (cur.phase === "READY" && visionSurfaceCount(state) === 0) {
|
|
2426
|
+
log("[cold-start] READY with 0 vision surfaces — demoting to FAILED(ready_no_surfaces) on init");
|
|
2427
|
+
enterFailed(state, log, "ready_no_surfaces");
|
|
2428
|
+
log("[cold-start] machine initialized");
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (cur.phase !== "BLANK") {
|
|
2432
|
+
log(`[cold-start] restart recovery — preserving persisted phase=${cur.phase} (no provisioned re-fire)`);
|
|
2433
|
+
log("[cold-start] machine initialized");
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
emitTrigger(state, "provisioned", { source: "init" });
|
|
2437
|
+
log("[cold-start] machine initialized");
|
|
2438
|
+
}
|
|
2439
|
+
function shutdownColdStartMachine() {
|
|
2440
|
+
stopPoller();
|
|
2441
|
+
for (const t of deadlineTimers.values())
|
|
2442
|
+
clearTimeout(t);
|
|
2443
|
+
deadlineTimers.clear();
|
|
2444
|
+
}
|
|
2445
|
+
function observeColdStart(state) {
|
|
2446
|
+
const s = ensureInitialRow(state);
|
|
2447
|
+
return {
|
|
2448
|
+
phase: s.phase,
|
|
2449
|
+
failureReason: s.failureReason,
|
|
2450
|
+
retryCount: s.retryCount,
|
|
2451
|
+
contractWarnings: s.contractWarnings,
|
|
2452
|
+
enteredAt: s.enteredAt,
|
|
2453
|
+
deadlineAt: s.deadlineAt
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
async function retryColdStart(state) {
|
|
2457
|
+
emitTrigger(state, "userRetried", { ts: Date.now() });
|
|
2458
|
+
if (machineRuntime && machineLog) {
|
|
2459
|
+
const triggers = drainTriggers(state, 50);
|
|
2460
|
+
for (const trigger of triggers) {
|
|
2461
|
+
try {
|
|
2462
|
+
await transition(state, machineRuntime, machineLog, trigger);
|
|
2463
|
+
} catch (e) {
|
|
2464
|
+
machineLog(`[cold-start] retry-drain transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return observeColdStart(state);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// services/surfaces-init.ts
|
|
2472
|
+
import * as path5 from "node:path";
|
|
1996
2473
|
|
|
1997
2474
|
// activity.ts
|
|
1998
2475
|
function recordSurfaceEvent(state, surfaceId, event, dsl, agentId, metadata) {
|
|
@@ -2090,8 +2567,8 @@ function broadcastInput(message, sessionKey) {
|
|
|
2090
2567
|
}
|
|
2091
2568
|
|
|
2092
2569
|
// dashboard-state.ts
|
|
2093
|
-
import { readFileSync as
|
|
2094
|
-
import { homedir as
|
|
2570
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2571
|
+
import { homedir as homedir3 } from "node:os";
|
|
2095
2572
|
function extractTitleAndDetail(meta) {
|
|
2096
2573
|
let title = null;
|
|
2097
2574
|
let detail = null;
|
|
@@ -2304,9 +2781,9 @@ ${enhanced.formatted}`;
|
|
|
2304
2781
|
const agentFiles = [];
|
|
2305
2782
|
for (const w of warnings) {
|
|
2306
2783
|
const aid = w.agentId;
|
|
2307
|
-
const filePath = `${
|
|
2784
|
+
const filePath = `${homedir3()}/.openclaw/workspace-${aid}/AGENTS.md`;
|
|
2308
2785
|
try {
|
|
2309
|
-
const content =
|
|
2786
|
+
const content = readFileSync3(filePath, "utf-8").slice(0, 3000);
|
|
2310
2787
|
agentFiles.push(`### ${aid} AGENTS.md (${w.cnt} warnings)
|
|
2311
2788
|
${content}`);
|
|
2312
2789
|
} catch {}
|
|
@@ -2839,6 +3316,22 @@ function processDslBlock(state, dsl) {
|
|
|
2839
3316
|
missingCardStructure: validation.missingCardStructure ? true : undefined
|
|
2840
3317
|
});
|
|
2841
3318
|
recordSurfaceEvent(state, sid, existing ? "updated" : "created", block, state.surfaceDb.getAgentId(sid));
|
|
3319
|
+
if (!existing) {
|
|
3320
|
+
let kind = null;
|
|
3321
|
+
if (sid.startsWith("vision-"))
|
|
3322
|
+
kind = "vision";
|
|
3323
|
+
else if (sid === "awaiting-dream-input")
|
|
3324
|
+
kind = "dream-input";
|
|
3325
|
+
else if (sid === "onboarding-domain")
|
|
3326
|
+
kind = "onboarding";
|
|
3327
|
+
if (kind) {
|
|
3328
|
+
try {
|
|
3329
|
+
emitTrigger(state, "surfacePushed", { surfaceId: sid, kind, agentId: state.surfaceDb.getAgentId(sid) ?? null });
|
|
3330
|
+
} catch (e) {
|
|
3331
|
+
console.warn("[agentlife] cold-start emit surfacePushed failed: %s", e?.message);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
2842
3335
|
}
|
|
2843
3336
|
return results;
|
|
2844
3337
|
}
|
|
@@ -2982,23 +3475,27 @@ function registerSurfacesService(api, state) {
|
|
|
2982
3475
|
api.registerService({
|
|
2983
3476
|
id: "agentlife-surfaces",
|
|
2984
3477
|
start: async (ctx) => {
|
|
2985
|
-
const agentlifeDir =
|
|
3478
|
+
const agentlifeDir = path5.join(ctx.stateDir, "agentlife");
|
|
2986
3479
|
state.agentlifeStateDir = agentlifeDir;
|
|
2987
|
-
state.registryFilePath =
|
|
2988
|
-
state.dbBaseDir =
|
|
2989
|
-
state.historyDbPath =
|
|
3480
|
+
state.registryFilePath = path5.join(agentlifeDir, "agent-registry.json");
|
|
3481
|
+
state.dbBaseDir = path5.join(agentlifeDir, "db");
|
|
3482
|
+
state.historyDbPath = path5.join(agentlifeDir, "agentlife.db");
|
|
2990
3483
|
if (!state.surfaceDb) {
|
|
2991
3484
|
const db = getOrCreateHistoryDb(state);
|
|
2992
3485
|
state.surfaceDb = new SurfaceDb(db);
|
|
2993
3486
|
}
|
|
2994
3487
|
runStartupPurge(state);
|
|
3488
|
+
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
3489
|
+
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
2995
3490
|
let inputPurged = 0;
|
|
2996
3491
|
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
2997
3492
|
const headerLine = meta.lines[0] ?? "";
|
|
2998
|
-
if (
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3493
|
+
if (!/\binput\b/.test(headerLine))
|
|
3494
|
+
continue;
|
|
3495
|
+
if (isFlowInput(surfaceId))
|
|
3496
|
+
continue;
|
|
3497
|
+
state.surfaceDb.delete(surfaceId);
|
|
3498
|
+
inputPurged++;
|
|
3002
3499
|
}
|
|
3003
3500
|
if (inputPurged > 0) {
|
|
3004
3501
|
console.log("[agentlife] purged %d stale input surfaces on startup", inputPurged);
|
|
@@ -3051,22 +3548,22 @@ function registerSurfacesService(api, state) {
|
|
|
3051
3548
|
}
|
|
3052
3549
|
|
|
3053
3550
|
// services/config-optimizer.ts
|
|
3054
|
-
import * as
|
|
3055
|
-
import * as
|
|
3056
|
-
import * as
|
|
3551
|
+
import * as fs4 from "node:fs/promises";
|
|
3552
|
+
import * as path6 from "node:path";
|
|
3553
|
+
import * as os3 from "node:os";
|
|
3057
3554
|
var TARGET_MAX_PING_PONG = 1;
|
|
3058
3555
|
var TARGET_MAX_CONCURRENT = 10;
|
|
3059
|
-
var CONFIG_PATH =
|
|
3556
|
+
var CONFIG_PATH = path6.join(os3.homedir(), ".openclaw", "openclaw.json");
|
|
3060
3557
|
async function readRawConfigFromDisk() {
|
|
3061
|
-
const raw = await
|
|
3558
|
+
const raw = await fs4.readFile(CONFIG_PATH, "utf-8");
|
|
3062
3559
|
return JSON.parse(raw);
|
|
3063
3560
|
}
|
|
3064
3561
|
async function writeRawConfigToDisk(cfg) {
|
|
3065
|
-
await
|
|
3562
|
+
await fs4.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2) + `
|
|
3066
3563
|
`, "utf-8");
|
|
3067
3564
|
}
|
|
3068
3565
|
async function restoreConfigFromBackup(api, backupPath) {
|
|
3069
|
-
const raw = await
|
|
3566
|
+
const raw = await fs4.readFile(backupPath, "utf-8");
|
|
3070
3567
|
const backup = JSON.parse(raw);
|
|
3071
3568
|
const fileCfg = await readRawConfigFromDisk();
|
|
3072
3569
|
if (backup.maxPingPongTurns != null) {
|
|
@@ -3080,7 +3577,7 @@ async function restoreConfigFromBackup(api, backupPath) {
|
|
|
3080
3577
|
fileCfg.agents.defaults.maxConcurrent = backup.maxConcurrent;
|
|
3081
3578
|
}
|
|
3082
3579
|
await writeRawConfigToDisk(fileCfg);
|
|
3083
|
-
await
|
|
3580
|
+
await fs4.unlink(backupPath);
|
|
3084
3581
|
console.log("[agentlife] restored config: maxPingPongTurns=%s, maxConcurrent=%s", backup.maxPingPongTurns ?? "default", backup.maxConcurrent ?? "default");
|
|
3085
3582
|
return `config restored`;
|
|
3086
3583
|
}
|
|
@@ -3088,8 +3585,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3088
3585
|
api.registerService({
|
|
3089
3586
|
id: "agentlife-config-optimizer",
|
|
3090
3587
|
start: async (ctx) => {
|
|
3091
|
-
const configBackupDir =
|
|
3092
|
-
const configBackupPath =
|
|
3588
|
+
const configBackupDir = path6.join(ctx.stateDir, "agentlife");
|
|
3589
|
+
const configBackupPath = path6.join(configBackupDir, "config-backup.json");
|
|
3093
3590
|
const cfg = api.runtime.config.loadConfig();
|
|
3094
3591
|
const currentPPT = cfg.session?.agentToAgent?.maxPingPongTurns;
|
|
3095
3592
|
const currentMC = cfg.agents?.defaults?.maxConcurrent;
|
|
@@ -3099,8 +3596,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3099
3596
|
console.log("[agentlife] config already optimized, skipping");
|
|
3100
3597
|
return;
|
|
3101
3598
|
}
|
|
3102
|
-
await
|
|
3103
|
-
await
|
|
3599
|
+
await fs4.mkdir(configBackupDir, { recursive: true });
|
|
3600
|
+
await fs4.writeFile(configBackupPath, JSON.stringify({
|
|
3104
3601
|
maxPingPongTurns: currentPPT ?? null,
|
|
3105
3602
|
maxConcurrent: currentMC ?? null
|
|
3106
3603
|
}));
|
|
@@ -3125,7 +3622,7 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3125
3622
|
},
|
|
3126
3623
|
stop: async (ctx) => {
|
|
3127
3624
|
try {
|
|
3128
|
-
const backupPath =
|
|
3625
|
+
const backupPath = path6.join(ctx.stateDir, "agentlife", "config-backup.json");
|
|
3129
3626
|
await restoreConfigFromBackup(api, backupPath);
|
|
3130
3627
|
} catch {}
|
|
3131
3628
|
}
|
|
@@ -3576,8 +4073,8 @@ function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
|
|
|
3576
4073
|
}
|
|
3577
4074
|
|
|
3578
4075
|
// notifications.ts
|
|
3579
|
-
import * as
|
|
3580
|
-
import * as
|
|
4076
|
+
import * as fs5 from "node:fs";
|
|
4077
|
+
import * as path7 from "node:path";
|
|
3581
4078
|
var config;
|
|
3582
4079
|
var recentNotifications = new Map;
|
|
3583
4080
|
var DEBOUNCE_MS = 60000;
|
|
@@ -3585,8 +4082,8 @@ function loadConfig() {
|
|
|
3585
4082
|
if (config !== undefined)
|
|
3586
4083
|
return config;
|
|
3587
4084
|
try {
|
|
3588
|
-
const configPath =
|
|
3589
|
-
const raw =
|
|
4085
|
+
const configPath = path7.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
4086
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
3590
4087
|
const parsed = JSON.parse(raw);
|
|
3591
4088
|
if (parsed.serverUrl && parsed.apiKey) {
|
|
3592
4089
|
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
@@ -3817,6 +4314,17 @@ function registerActivityHooks(api, state) {
|
|
|
3817
4314
|
console.log("[agentlife] action redirected to %s for %s (origin=%s)", actionSessionKey, actionSurfaceId, origin ? "set" : "legacy");
|
|
3818
4315
|
}
|
|
3819
4316
|
}
|
|
4317
|
+
const dreamMatch = message.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
4318
|
+
if (dreamMatch) {
|
|
4319
|
+
const labelMatch = message.match(/\blabel=([^\n]+)/);
|
|
4320
|
+
const valueMatch = message.match(/\bvalue=([^\n]+)/);
|
|
4321
|
+
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
4322
|
+
try {
|
|
4323
|
+
emitTrigger(state, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
4324
|
+
} catch (e) {
|
|
4325
|
+
console.warn("[agentlife] cold-start emit userAnswered failed: %s", e?.message);
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
3820
4328
|
});
|
|
3821
4329
|
api.on("llm_output", (event, ctx) => {
|
|
3822
4330
|
if (state.disabled)
|
|
@@ -3849,6 +4357,39 @@ function registerActivityHooks(api, state) {
|
|
|
3849
4357
|
})
|
|
3850
4358
|
});
|
|
3851
4359
|
}
|
|
4360
|
+
let isCurrentColdStartSession = false;
|
|
4361
|
+
if (agentId) {
|
|
4362
|
+
try {
|
|
4363
|
+
const cs = readState(state);
|
|
4364
|
+
if (cs && cs.actionSessionKey === sessionKey && cs.phase !== "READY" && cs.phase !== "FAILED") {
|
|
4365
|
+
isCurrentColdStartSession = true;
|
|
4366
|
+
}
|
|
4367
|
+
} catch {}
|
|
4368
|
+
}
|
|
4369
|
+
if (isCurrentColdStartSession && agentId) {
|
|
4370
|
+
const lastText = texts.length > 0 ? texts[texts.length - 1].trim() : "";
|
|
4371
|
+
const lastLine = lastText.split(`
|
|
4372
|
+
`).pop()?.trim() ?? "";
|
|
4373
|
+
let marker = null;
|
|
4374
|
+
if (/\bNO_SIGNAL\b/.test(lastText)) {
|
|
4375
|
+
marker = "NO_SIGNAL";
|
|
4376
|
+
} else if (TERMINAL_MARKERS.has(lastLine)) {
|
|
4377
|
+
marker = lastLine;
|
|
4378
|
+
} else if (TERMINAL_MARKERS.has(lastText)) {
|
|
4379
|
+
marker = lastText;
|
|
4380
|
+
}
|
|
4381
|
+
try {
|
|
4382
|
+
emitTrigger(state, "agentReplied", {
|
|
4383
|
+
agentId,
|
|
4384
|
+
sessionKey,
|
|
4385
|
+
marker,
|
|
4386
|
+
lastText: lastText.slice(0, 4000),
|
|
4387
|
+
terminal: marker !== null
|
|
4388
|
+
});
|
|
4389
|
+
} catch (e) {
|
|
4390
|
+
console.warn("[agentlife] cold-start emit agentReplied failed: %s", e?.message);
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
3852
4393
|
}, { priority: 100 });
|
|
3853
4394
|
api.on("before_tool_call", (event, ctx) => {
|
|
3854
4395
|
if (state.disabled)
|
|
@@ -3893,6 +4434,26 @@ function registerActivityHooks(api, state) {
|
|
|
3893
4434
|
if (event.toolName === "agentlife_push" && typeof event.params?.dsl === "string") {
|
|
3894
4435
|
canvasDsl = event.params.dsl;
|
|
3895
4436
|
}
|
|
4437
|
+
if (!isError && agentId) {
|
|
4438
|
+
let touchedMemory = false;
|
|
4439
|
+
if (toolName === "exec") {
|
|
4440
|
+
const command = event.params?.command ?? event.params?.script ?? "";
|
|
4441
|
+
if (typeof command === "string" && /memory\/[\w.\-]+\.md/.test(command) && /(>>?|tee\b|cp\b|mv\b)/.test(command)) {
|
|
4442
|
+
touchedMemory = true;
|
|
4443
|
+
}
|
|
4444
|
+
} else if (toolName === "write" || toolName === "edit" || toolName === "str_replace" || toolName === "apply_patch") {
|
|
4445
|
+
const filePath = event.params?.path ?? event.params?.file_path ?? "";
|
|
4446
|
+
if (/memory\/[\w.\-]+\.md$/.test(filePath))
|
|
4447
|
+
touchedMemory = true;
|
|
4448
|
+
}
|
|
4449
|
+
if (touchedMemory) {
|
|
4450
|
+
try {
|
|
4451
|
+
emitTrigger(state, "memoryWritten", { agentId, sessionKey });
|
|
4452
|
+
} catch (e) {
|
|
4453
|
+
console.warn("[agentlife] cold-start emit memoryWritten failed: %s", e?.message);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
3896
4457
|
const resultStr = typeof event.result === "string" ? event.result : event.result != null ? JSON.stringify(event.result) : null;
|
|
3897
4458
|
recordActivity(state, "tool_end", sessionKey, agentId, {
|
|
3898
4459
|
toolName,
|
|
@@ -4678,31 +5239,31 @@ function startDailySweepService(state) {
|
|
|
4678
5239
|
import * as crypto4 from "node:crypto";
|
|
4679
5240
|
import * as fsSync3 from "node:fs";
|
|
4680
5241
|
import { createRequire as createRequire3 } from "node:module";
|
|
4681
|
-
import * as
|
|
4682
|
-
import * as
|
|
5242
|
+
import * as os7 from "node:os";
|
|
5243
|
+
import * as path11 from "node:path";
|
|
4683
5244
|
|
|
4684
5245
|
// gateway/web-app.ts
|
|
4685
5246
|
import * as crypto3 from "node:crypto";
|
|
4686
|
-
import * as
|
|
4687
|
-
import * as
|
|
4688
|
-
import * as
|
|
5247
|
+
import * as os6 from "node:os";
|
|
5248
|
+
import * as path10 from "node:path";
|
|
5249
|
+
import * as fs8 from "node:fs";
|
|
4689
5250
|
|
|
4690
5251
|
// services/pairing-access-token.ts
|
|
4691
5252
|
import * as crypto from "node:crypto";
|
|
4692
|
-
import * as
|
|
4693
|
-
import * as
|
|
4694
|
-
import * as
|
|
5253
|
+
import * as fs6 from "node:fs";
|
|
5254
|
+
import * as os4 from "node:os";
|
|
5255
|
+
import * as path8 from "node:path";
|
|
4695
5256
|
var cachedToken = null;
|
|
4696
5257
|
function pairingAccessPath() {
|
|
4697
|
-
return
|
|
5258
|
+
return path8.join(os4.homedir(), ".openclaw", "agentlife", "pairing-access.json");
|
|
4698
5259
|
}
|
|
4699
5260
|
function loadOrCreatePairingAccessToken() {
|
|
4700
5261
|
if (cachedToken)
|
|
4701
5262
|
return cachedToken;
|
|
4702
5263
|
const filePath = pairingAccessPath();
|
|
4703
5264
|
try {
|
|
4704
|
-
if (
|
|
4705
|
-
const raw =
|
|
5265
|
+
if (fs6.existsSync(filePath)) {
|
|
5266
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
4706
5267
|
const obj = JSON.parse(raw);
|
|
4707
5268
|
const token2 = typeof obj?.token === "string" ? obj.token : null;
|
|
4708
5269
|
if (token2 && token2.length >= 32) {
|
|
@@ -4711,12 +5272,12 @@ function loadOrCreatePairingAccessToken() {
|
|
|
4711
5272
|
}
|
|
4712
5273
|
}
|
|
4713
5274
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
4714
|
-
const dir =
|
|
4715
|
-
if (!
|
|
4716
|
-
|
|
4717
|
-
|
|
5275
|
+
const dir = path8.dirname(filePath);
|
|
5276
|
+
if (!fs6.existsSync(dir))
|
|
5277
|
+
fs6.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5278
|
+
fs6.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
|
|
4718
5279
|
try {
|
|
4719
|
-
|
|
5280
|
+
fs6.chmodSync(filePath, 384);
|
|
4720
5281
|
} catch {}
|
|
4721
5282
|
cachedToken = token;
|
|
4722
5283
|
return token;
|
|
@@ -4729,15 +5290,15 @@ function loadOrCreatePairingAccessToken() {
|
|
|
4729
5290
|
// services/cloudflared-supervisor.ts
|
|
4730
5291
|
import { spawn, spawnSync } from "node:child_process";
|
|
4731
5292
|
import * as crypto2 from "node:crypto";
|
|
4732
|
-
import * as
|
|
4733
|
-
import * as
|
|
4734
|
-
import * as
|
|
4735
|
-
var AGENTLIFE_DIR =
|
|
4736
|
-
var DEVICE_FILE =
|
|
4737
|
-
var TUNNEL_FILE =
|
|
4738
|
-
var BIN_DIR =
|
|
4739
|
-
var CLOUDFLARED_BIN =
|
|
4740
|
-
var CLOUDFLARED_PATH =
|
|
5293
|
+
import * as fs7 from "node:fs";
|
|
5294
|
+
import * as os5 from "node:os";
|
|
5295
|
+
import * as path9 from "node:path";
|
|
5296
|
+
var AGENTLIFE_DIR = path9.join(os5.homedir(), ".openclaw", "agentlife");
|
|
5297
|
+
var DEVICE_FILE = path9.join(AGENTLIFE_DIR, "device.json");
|
|
5298
|
+
var TUNNEL_FILE = path9.join(AGENTLIFE_DIR, "tunnel.json");
|
|
5299
|
+
var BIN_DIR = path9.join(AGENTLIFE_DIR, "bin");
|
|
5300
|
+
var CLOUDFLARED_BIN = os5.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
5301
|
+
var CLOUDFLARED_PATH = path9.join(BIN_DIR, CLOUDFLARED_BIN);
|
|
4741
5302
|
var API_BASE = process.env.AGENTLIFE_API_BASE || "https://api.agentlife.app";
|
|
4742
5303
|
var RESTART_DELAY_MS = 5000;
|
|
4743
5304
|
var STABLE_RUNTIME_MS = 60000;
|
|
@@ -4814,16 +5375,16 @@ async function doBootstrap() {
|
|
|
4814
5375
|
return tunnelInfo;
|
|
4815
5376
|
}
|
|
4816
5377
|
function ensureDirs() {
|
|
4817
|
-
if (!
|
|
4818
|
-
|
|
4819
|
-
if (!
|
|
4820
|
-
|
|
5378
|
+
if (!fs7.existsSync(AGENTLIFE_DIR))
|
|
5379
|
+
fs7.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
5380
|
+
if (!fs7.existsSync(BIN_DIR))
|
|
5381
|
+
fs7.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
|
|
4821
5382
|
}
|
|
4822
5383
|
function loadDeviceIdentity() {
|
|
4823
|
-
if (!
|
|
5384
|
+
if (!fs7.existsSync(DEVICE_FILE))
|
|
4824
5385
|
return null;
|
|
4825
5386
|
try {
|
|
4826
|
-
const raw =
|
|
5387
|
+
const raw = fs7.readFileSync(DEVICE_FILE, "utf-8");
|
|
4827
5388
|
const parsed = JSON.parse(raw);
|
|
4828
5389
|
if (typeof parsed.deviceId === "string" && typeof parsed.deviceSecret === "string") {
|
|
4829
5390
|
return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
|
|
@@ -4841,19 +5402,19 @@ function loadOrCreateDeviceIdentity() {
|
|
|
4841
5402
|
deviceId: crypto2.randomUUID(),
|
|
4842
5403
|
deviceSecret: crypto2.randomBytes(32).toString("base64url")
|
|
4843
5404
|
};
|
|
4844
|
-
|
|
5405
|
+
fs7.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
4845
5406
|
try {
|
|
4846
|
-
|
|
5407
|
+
fs7.chmodSync(DEVICE_FILE, 384);
|
|
4847
5408
|
} catch {}
|
|
4848
5409
|
console.log(`[cloudflared-supervisor] generated new device identity (deviceId=${identity.deviceId})`);
|
|
4849
5410
|
return identity;
|
|
4850
5411
|
}
|
|
4851
5412
|
var AGENTLIFE_API_BASE = API_BASE;
|
|
4852
5413
|
function loadCachedTunnel() {
|
|
4853
|
-
if (!
|
|
5414
|
+
if (!fs7.existsSync(TUNNEL_FILE))
|
|
4854
5415
|
return null;
|
|
4855
5416
|
try {
|
|
4856
|
-
const raw =
|
|
5417
|
+
const raw = fs7.readFileSync(TUNNEL_FILE, "utf-8");
|
|
4857
5418
|
const parsed = JSON.parse(raw);
|
|
4858
5419
|
if (typeof parsed.subdomain === "string" && typeof parsed.hostname === "string" && typeof parsed.tunnelUrl === "string" && typeof parsed.tunnelToken === "string" && typeof parsed.provisionedAt === "number") {
|
|
4859
5420
|
return parsed;
|
|
@@ -4862,9 +5423,9 @@ function loadCachedTunnel() {
|
|
|
4862
5423
|
return null;
|
|
4863
5424
|
}
|
|
4864
5425
|
function persistTunnel(info) {
|
|
4865
|
-
|
|
5426
|
+
fs7.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
|
|
4866
5427
|
try {
|
|
4867
|
-
|
|
5428
|
+
fs7.chmodSync(TUNNEL_FILE, 384);
|
|
4868
5429
|
} catch {}
|
|
4869
5430
|
}
|
|
4870
5431
|
async function provisionTunnel(identity) {
|
|
@@ -4901,14 +5462,14 @@ async function provisionTunnel(identity) {
|
|
|
4901
5462
|
}
|
|
4902
5463
|
}
|
|
4903
5464
|
function ensureCloudflaredBinary() {
|
|
4904
|
-
if (
|
|
5465
|
+
if (fs7.existsSync(CLOUDFLARED_PATH)) {
|
|
4905
5466
|
try {
|
|
4906
|
-
|
|
5467
|
+
fs7.accessSync(CLOUDFLARED_PATH, fs7.constants.X_OK);
|
|
4907
5468
|
return CLOUDFLARED_PATH;
|
|
4908
5469
|
} catch {}
|
|
4909
5470
|
}
|
|
4910
|
-
const platform2 =
|
|
4911
|
-
const arch2 =
|
|
5471
|
+
const platform2 = os5.platform();
|
|
5472
|
+
const arch2 = os5.arch();
|
|
4912
5473
|
const release = detectCloudflaredRelease(platform2, arch2);
|
|
4913
5474
|
if (!release) {
|
|
4914
5475
|
console.warn(`[cloudflared-supervisor] unsupported platform: ${platform2}/${arch2}`);
|
|
@@ -4921,28 +5482,28 @@ function ensureCloudflaredBinary() {
|
|
|
4921
5482
|
if (release.kind === "tgz") {
|
|
4922
5483
|
const extracted = extractTgzCloudflared(release.tempPath, BIN_DIR);
|
|
4923
5484
|
try {
|
|
4924
|
-
|
|
5485
|
+
fs7.unlinkSync(release.tempPath);
|
|
4925
5486
|
} catch {}
|
|
4926
5487
|
if (!extracted)
|
|
4927
5488
|
return null;
|
|
4928
5489
|
} else {
|
|
4929
5490
|
try {
|
|
4930
|
-
|
|
5491
|
+
fs7.renameSync(release.tempPath, CLOUDFLARED_PATH);
|
|
4931
5492
|
} catch (err) {
|
|
4932
5493
|
console.warn(`[cloudflared-supervisor] rename failed: ${err?.message}`);
|
|
4933
5494
|
return null;
|
|
4934
5495
|
}
|
|
4935
5496
|
}
|
|
4936
5497
|
try {
|
|
4937
|
-
|
|
5498
|
+
fs7.chmodSync(CLOUDFLARED_PATH, 493);
|
|
4938
5499
|
} catch {}
|
|
4939
|
-
return
|
|
5500
|
+
return fs7.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
|
|
4940
5501
|
}
|
|
4941
5502
|
function detectCloudflaredRelease(platform2, arch2) {
|
|
4942
5503
|
const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
4943
5504
|
if (platform2 === "darwin") {
|
|
4944
5505
|
const asset = arch2 === "arm64" ? "cloudflared-darwin-arm64.tgz" : "cloudflared-darwin-amd64.tgz";
|
|
4945
|
-
return { url: `${base}/${asset}`, kind: "tgz", tempPath:
|
|
5506
|
+
return { url: `${base}/${asset}`, kind: "tgz", tempPath: path9.join(BIN_DIR, asset) };
|
|
4946
5507
|
}
|
|
4947
5508
|
if (platform2 === "linux") {
|
|
4948
5509
|
const asset = arch2 === "arm64" ? "cloudflared-linux-arm64" : arch2 === "arm" ? "cloudflared-linux-arm" : arch2 === "x64" ? "cloudflared-linux-amd64" : null;
|
|
@@ -4966,11 +5527,11 @@ function downloadWithCurl(url, dest) {
|
|
|
4966
5527
|
if (result.status !== 0) {
|
|
4967
5528
|
console.warn(`[cloudflared-supervisor] curl failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
4968
5529
|
try {
|
|
4969
|
-
|
|
5530
|
+
fs7.unlinkSync(dest);
|
|
4970
5531
|
} catch {}
|
|
4971
5532
|
return false;
|
|
4972
5533
|
}
|
|
4973
|
-
return
|
|
5534
|
+
return fs7.existsSync(dest) && fs7.statSync(dest).size > 0;
|
|
4974
5535
|
}
|
|
4975
5536
|
function extractTgzCloudflared(tgzPath, destDir) {
|
|
4976
5537
|
const result = spawnSync("tar", ["-xzf", tgzPath, "-C", destDir], { stdio: "pipe" });
|
|
@@ -4978,7 +5539,7 @@ function extractTgzCloudflared(tgzPath, destDir) {
|
|
|
4978
5539
|
console.warn(`[cloudflared-supervisor] tar extract failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
4979
5540
|
return false;
|
|
4980
5541
|
}
|
|
4981
|
-
return
|
|
5542
|
+
return fs7.existsSync(CLOUDFLARED_PATH);
|
|
4982
5543
|
}
|
|
4983
5544
|
function startCloudflaredProcess(binPath, tunnelToken) {
|
|
4984
5545
|
if (state.stopped)
|
|
@@ -5035,11 +5596,11 @@ var MIME_TYPES = {
|
|
|
5035
5596
|
};
|
|
5036
5597
|
function mintBootstrapToken() {
|
|
5037
5598
|
const bootstrapToken = crypto3.randomBytes(32).toString("base64url");
|
|
5038
|
-
const devicesDir =
|
|
5039
|
-
const bootstrapPath =
|
|
5040
|
-
if (!
|
|
5041
|
-
|
|
5042
|
-
const registry =
|
|
5599
|
+
const devicesDir = path10.join(os6.homedir(), ".openclaw", "devices");
|
|
5600
|
+
const bootstrapPath = path10.join(devicesDir, "bootstrap.json");
|
|
5601
|
+
if (!fs8.existsSync(devicesDir))
|
|
5602
|
+
fs8.mkdirSync(devicesDir, { recursive: true });
|
|
5603
|
+
const registry = fs8.existsSync(bootstrapPath) ? JSON.parse(fs8.readFileSync(bootstrapPath, "utf-8")) : {};
|
|
5043
5604
|
registry[bootstrapToken] = {
|
|
5044
5605
|
token: bootstrapToken,
|
|
5045
5606
|
profile: {
|
|
@@ -5048,15 +5609,15 @@ function mintBootstrapToken() {
|
|
|
5048
5609
|
},
|
|
5049
5610
|
issuedAtMs: Date.now()
|
|
5050
5611
|
};
|
|
5051
|
-
|
|
5612
|
+
fs8.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
|
|
5052
5613
|
return bootstrapToken;
|
|
5053
5614
|
}
|
|
5054
5615
|
function registerWebApp(api) {
|
|
5055
|
-
const pluginRoot =
|
|
5056
|
-
const appRoot =
|
|
5057
|
-
const hasWebBuild =
|
|
5058
|
-
const indexPath = hasWebBuild ?
|
|
5059
|
-
const hasIndex = !!(indexPath &&
|
|
5616
|
+
const pluginRoot = path10.resolve(path10.dirname(api.source), "..");
|
|
5617
|
+
const appRoot = path10.join(pluginRoot, "web-build");
|
|
5618
|
+
const hasWebBuild = fs8.existsSync(appRoot);
|
|
5619
|
+
const indexPath = hasWebBuild ? path10.join(appRoot, "index.html") : null;
|
|
5620
|
+
const hasIndex = !!(indexPath && fs8.existsSync(indexPath));
|
|
5060
5621
|
if (!hasWebBuild) {
|
|
5061
5622
|
api.logger.info("[agentlife] web-build/ not found — /agentlife/pair will still register; static dashboard disabled");
|
|
5062
5623
|
} else if (!hasIndex) {
|
|
@@ -5064,8 +5625,8 @@ function registerWebApp(api) {
|
|
|
5064
5625
|
}
|
|
5065
5626
|
let gatewayToken = "";
|
|
5066
5627
|
try {
|
|
5067
|
-
const configPath =
|
|
5068
|
-
const raw = JSON.parse(
|
|
5628
|
+
const configPath = path10.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
|
|
5629
|
+
const raw = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
5069
5630
|
gatewayToken = raw?.gateway?.auth?.token || "";
|
|
5070
5631
|
} catch {}
|
|
5071
5632
|
loadOrCreatePairingAccessToken();
|
|
@@ -5181,16 +5742,16 @@ function registerWebApp(api) {
|
|
|
5181
5742
|
return true;
|
|
5182
5743
|
}
|
|
5183
5744
|
const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
|
|
5184
|
-
const filePath =
|
|
5745
|
+
const filePath = path10.resolve(appRoot, relative);
|
|
5185
5746
|
if (!filePath.startsWith(appRoot)) {
|
|
5186
5747
|
res.writeHead(403);
|
|
5187
5748
|
res.end();
|
|
5188
5749
|
return true;
|
|
5189
5750
|
}
|
|
5190
|
-
const target =
|
|
5191
|
-
const ext =
|
|
5751
|
+
const target = fs8.existsSync(filePath) && fs8.statSync(filePath).isFile() ? filePath : indexPath;
|
|
5752
|
+
const ext = path10.extname(target).toLowerCase();
|
|
5192
5753
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
5193
|
-
let content =
|
|
5754
|
+
let content = fs8.readFileSync(target);
|
|
5194
5755
|
if (target === indexPath) {
|
|
5195
5756
|
setTimeout(approveLatest, 2000);
|
|
5196
5757
|
setTimeout(approveLatest, 5000);
|
|
@@ -5279,9 +5840,9 @@ Setup code: ${setupCode}
|
|
|
5279
5840
|
api.registerService({
|
|
5280
5841
|
id: "agentlife-auto-pair",
|
|
5281
5842
|
start: async (ctx) => {
|
|
5282
|
-
const devicesDir =
|
|
5283
|
-
const pendingPath =
|
|
5284
|
-
const pairedPath =
|
|
5843
|
+
const devicesDir = path11.join(os7.homedir(), ".openclaw", "devices");
|
|
5844
|
+
const pendingPath = path11.join(devicesDir, "pending.json");
|
|
5845
|
+
const pairedPath = path11.join(devicesDir, "paired.json");
|
|
5285
5846
|
const pollInterval = 3000;
|
|
5286
5847
|
const withAdmin = (arr) => {
|
|
5287
5848
|
const base = Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
@@ -5533,11 +6094,11 @@ function registerBootstrapHookImpl(api, state2) {
|
|
|
5533
6094
|
}
|
|
5534
6095
|
|
|
5535
6096
|
// tools/widget-push.ts
|
|
5536
|
-
var
|
|
6097
|
+
var PROVISIONED_IDS2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
5537
6098
|
function isKnownAgent(state2, agentId, api) {
|
|
5538
6099
|
if (state2.agentRegistry.size === 0)
|
|
5539
6100
|
return true;
|
|
5540
|
-
if (
|
|
6101
|
+
if (PROVISIONED_IDS2.has(agentId) || state2.agentRegistry.has(agentId))
|
|
5541
6102
|
return true;
|
|
5542
6103
|
try {
|
|
5543
6104
|
const cfg = api.runtime.config.loadConfig();
|
|
@@ -5820,12 +6381,53 @@ function extractWidgetText(meta) {
|
|
|
5820
6381
|
}
|
|
5821
6382
|
|
|
5822
6383
|
// hooks/prompt-state.ts
|
|
6384
|
+
var lastEmittedAnswerByKey = new Map;
|
|
6385
|
+
function lastUserMessage(messages) {
|
|
6386
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
6387
|
+
const m = messages[i];
|
|
6388
|
+
if (m?.role !== "user")
|
|
6389
|
+
continue;
|
|
6390
|
+
if (typeof m.content === "string")
|
|
6391
|
+
return m.content;
|
|
6392
|
+
if (Array.isArray(m.content)) {
|
|
6393
|
+
const parts = [];
|
|
6394
|
+
for (const block of m.content) {
|
|
6395
|
+
if (typeof block === "string")
|
|
6396
|
+
parts.push(block);
|
|
6397
|
+
else if (block?.type === "text" && typeof block.text === "string")
|
|
6398
|
+
parts.push(block.text);
|
|
6399
|
+
}
|
|
6400
|
+
return parts.join(`
|
|
6401
|
+
`);
|
|
6402
|
+
}
|
|
6403
|
+
}
|
|
6404
|
+
return "";
|
|
6405
|
+
}
|
|
5823
6406
|
function registerPromptStateHook(api, state2) {
|
|
5824
|
-
api.on("before_prompt_build", (
|
|
6407
|
+
api.on("before_prompt_build", (event, ctx) => {
|
|
5825
6408
|
if (state2.disabled)
|
|
5826
6409
|
return;
|
|
5827
6410
|
if (!isAgentlifeSession(ctx?.sessionKey))
|
|
5828
6411
|
return;
|
|
6412
|
+
const sessionKey = ctx?.sessionKey;
|
|
6413
|
+
if (sessionKey) {
|
|
6414
|
+
const messages = event?.messages ?? [];
|
|
6415
|
+
const latest = lastUserMessage(messages);
|
|
6416
|
+
if (latest && lastEmittedAnswerByKey.get(sessionKey) !== latest) {
|
|
6417
|
+
const actionMatch = latest.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
6418
|
+
if (actionMatch) {
|
|
6419
|
+
const labelMatch = latest.match(/\blabel=([^\n]+)/);
|
|
6420
|
+
const valueMatch = latest.match(/\bvalue=([^\n]+)/);
|
|
6421
|
+
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
6422
|
+
lastEmittedAnswerByKey.set(sessionKey, latest);
|
|
6423
|
+
try {
|
|
6424
|
+
emitTrigger(state2, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
6425
|
+
} catch (e) {
|
|
6426
|
+
console.warn("[agentlife] cold-start emit userAnswered (prompt-build) failed: %s", e?.message);
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
5829
6431
|
const agentId = ctx?.agentId;
|
|
5830
6432
|
const isOrchestrator = agentId === "agentlife";
|
|
5831
6433
|
if (isOrchestrator)
|
|
@@ -5845,8 +6447,8 @@ function registerPromptStateHook(api, state2) {
|
|
|
5845
6447
|
}
|
|
5846
6448
|
|
|
5847
6449
|
// gateway/agents.ts
|
|
5848
|
-
import * as
|
|
5849
|
-
import * as
|
|
6450
|
+
import * as fs9 from "node:fs";
|
|
6451
|
+
import * as path12 from "node:path";
|
|
5850
6452
|
function registerAgentGateway(api, state2) {
|
|
5851
6453
|
api.registerGatewayMethod("agentlife.createAgent", async ({ params, respond }) => {
|
|
5852
6454
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
@@ -5934,6 +6536,16 @@ function registerAgentGateway(api, state2) {
|
|
|
5934
6536
|
configFields.push("subagents");
|
|
5935
6537
|
if (identity)
|
|
5936
6538
|
configFields.push("identity");
|
|
6539
|
+
if (!existing) {
|
|
6540
|
+
const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6541
|
+
if (!provisionedIds.has(id)) {
|
|
6542
|
+
try {
|
|
6543
|
+
emitTrigger(state2, "agentRegistered", { agentId: id, name, model });
|
|
6544
|
+
} catch (e) {
|
|
6545
|
+
console.warn("[agentlife] cold-start emit agentRegistered failed: %s", e?.message);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
5937
6549
|
respond(true, { status, id, name, model, workspace, description, ...configFields.length ? { configFields } : {} });
|
|
5938
6550
|
});
|
|
5939
6551
|
api.registerGatewayMethod("agentlife.deleteAgent", async ({ params, respond }) => {
|
|
@@ -5996,10 +6608,10 @@ function registerAgentGateway(api, state2) {
|
|
|
5996
6608
|
state2.agentDbs.delete(id);
|
|
5997
6609
|
}
|
|
5998
6610
|
if (state2.dbBaseDir) {
|
|
5999
|
-
const dbPath =
|
|
6611
|
+
const dbPath = path12.join(state2.dbBaseDir, `${id}.db`);
|
|
6000
6612
|
try {
|
|
6001
|
-
if (
|
|
6002
|
-
|
|
6613
|
+
if (fs9.existsSync(dbPath)) {
|
|
6614
|
+
fs9.unlinkSync(dbPath);
|
|
6003
6615
|
cleanup.agentDbDeleted = true;
|
|
6004
6616
|
}
|
|
6005
6617
|
} catch (e) {
|
|
@@ -6025,6 +6637,17 @@ function registerAgentGateway(api, state2) {
|
|
|
6025
6637
|
await saveRegistryToDisk(state2);
|
|
6026
6638
|
}
|
|
6027
6639
|
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);
|
|
6640
|
+
try {
|
|
6641
|
+
const provisionedIds2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6642
|
+
let remaining = 0;
|
|
6643
|
+
for (const aid of state2.agentRegistry.keys()) {
|
|
6644
|
+
if (!provisionedIds2.has(aid))
|
|
6645
|
+
remaining++;
|
|
6646
|
+
}
|
|
6647
|
+
emitTrigger(state2, "agentDeleted", { agentId: id, remaining });
|
|
6648
|
+
} catch (e) {
|
|
6649
|
+
console.warn("[agentlife] cold-start emit agentDeleted failed: %s", e?.message);
|
|
6650
|
+
}
|
|
6028
6651
|
respond(true, { status: "deleted", id, removedFromConfig, removedFromRegistry, cleanup });
|
|
6029
6652
|
});
|
|
6030
6653
|
api.registerGatewayMethod("agentlife.agents", ({ respond }) => {
|
|
@@ -6325,7 +6948,7 @@ function registerUsageGateway(api, state2) {
|
|
|
6325
6948
|
var guidedDismissSent = new Set;
|
|
6326
6949
|
var warnedMissingOrigin = new Set;
|
|
6327
6950
|
function registerSurfacesGateway(api, state2) {
|
|
6328
|
-
api.registerGatewayMethod("agentlife.surfaces", ({ respond, context }) => {
|
|
6951
|
+
api.registerGatewayMethod("agentlife.surfaces", ({ params, respond, context }) => {
|
|
6329
6952
|
captureBridge(context);
|
|
6330
6953
|
const now = Date.now();
|
|
6331
6954
|
const activeSurfaceIds = [];
|
|
@@ -6334,23 +6957,21 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6334
6957
|
respond(true, { surfaces: [] });
|
|
6335
6958
|
return;
|
|
6336
6959
|
}
|
|
6337
|
-
const
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
if (/\bstate=loading\b/.test(headerLine) && now - meta.updatedAt > LOADING_TTL_MS) {
|
|
6341
|
-
const agentId = state2.surfaceDb.getAgentId(surfaceId);
|
|
6342
|
-
meta.lines[0] = headerLine.replace(/\bstate=loading\b/, "state=error");
|
|
6343
|
-
state2.surfaceDb.set(surfaceId, { ...meta, updatedAt: now });
|
|
6344
|
-
recordSurfaceEvent(state2, surfaceId, "loading_resolved_error", undefined, agentId ?? undefined, JSON.stringify({ reason: "loading surface orphaned (>2min)", resolvedBy: "surfaces_read" }));
|
|
6345
|
-
console.log("[agentlife] resolved stale loading surface %s → error (stuck >2min)", surfaceId);
|
|
6346
|
-
}
|
|
6347
|
-
}
|
|
6960
|
+
const visibility = params?.visibility === "paywall" ? "paywall" : "dashboard";
|
|
6961
|
+
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
6962
|
+
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
6348
6963
|
for (const [surfaceId, meta] of state2.surfaceDb.entries()) {
|
|
6349
6964
|
if (isExpired(meta, now))
|
|
6350
6965
|
continue;
|
|
6351
6966
|
const headerLine = meta.lines[0] ?? "";
|
|
6352
|
-
|
|
6967
|
+
const isInput = /\binput\b/.test(headerLine);
|
|
6968
|
+
if (isInput && !isFlowInput(surfaceId))
|
|
6353
6969
|
continue;
|
|
6970
|
+
if (visibility === "dashboard" && surfaceId.startsWith("vision-")) {
|
|
6971
|
+
const visible = state2.surfaceDb.getDashboardVisible(surfaceId);
|
|
6972
|
+
if (!visible)
|
|
6973
|
+
continue;
|
|
6974
|
+
}
|
|
6354
6975
|
if (meta.lines.length > 0) {
|
|
6355
6976
|
surfaceEntries.push({ surfaceId, dsl: meta.lines.join(`
|
|
6356
6977
|
`) });
|
|
@@ -6488,7 +7109,12 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6488
7109
|
automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
|
|
6489
7110
|
} catch {}
|
|
6490
7111
|
guidedDismissSent.delete(surfaceId);
|
|
6491
|
-
|
|
7112
|
+
const isVision = surfaceId.startsWith("vision-");
|
|
7113
|
+
if (isVision) {
|
|
7114
|
+
state2.surfaceDb.setDashboardVisible(surfaceId, false);
|
|
7115
|
+
} else {
|
|
7116
|
+
state2.surfaceDb.delete(surfaceId);
|
|
7117
|
+
}
|
|
6492
7118
|
broadcastDelete(surfaceId);
|
|
6493
7119
|
try {
|
|
6494
7120
|
enqueueCleanupTasks(state2, surfaceId, agentId, cronId, automations);
|
|
@@ -6496,7 +7122,7 @@ function registerSurfacesGateway(api, state2) {
|
|
|
6496
7122
|
console.error("[agentlife] dismiss: failed to enqueue cleanup tasks for %s: %s", surfaceId, err?.message);
|
|
6497
7123
|
}
|
|
6498
7124
|
const taskCount = (cronId ? 1 : 0) + (agentId ? 1 : 0);
|
|
6499
|
-
console.log("[agentlife] dismiss: %s
|
|
7125
|
+
console.log("[agentlife] dismiss: %s %s, %d cleanup tasks enqueued (reason=%s)", surfaceId, isVision ? "hidden from dashboard (kept for paywall)" : "deleted", taskCount, reason ?? "none");
|
|
6500
7126
|
respond(true, { surfaceId, dismissed: true });
|
|
6501
7127
|
processCleanupTasks(state2, surfaceId).catch((e) => console.warn("[agentlife] dismiss cleanup processor error:", e?.message));
|
|
6502
7128
|
}, { scope: "operator.write" });
|
|
@@ -6648,12 +7274,12 @@ function registerAutomationsGateway(api, state2) {
|
|
|
6648
7274
|
}
|
|
6649
7275
|
|
|
6650
7276
|
// gateway/admin.ts
|
|
6651
|
-
import * as
|
|
7277
|
+
import * as fs10 from "node:fs/promises";
|
|
6652
7278
|
import * as fsSync4 from "node:fs";
|
|
6653
|
-
import * as
|
|
6654
|
-
import * as
|
|
7279
|
+
import * as os8 from "node:os";
|
|
7280
|
+
import * as path13 from "node:path";
|
|
6655
7281
|
function pluginConfigPath() {
|
|
6656
|
-
return
|
|
7282
|
+
return path13.join(os8.homedir(), ".openclaw", "agentlife", "plugin-config.json");
|
|
6657
7283
|
}
|
|
6658
7284
|
function readPluginConfig() {
|
|
6659
7285
|
try {
|
|
@@ -6663,7 +7289,7 @@ function readPluginConfig() {
|
|
|
6663
7289
|
}
|
|
6664
7290
|
}
|
|
6665
7291
|
function writePluginConfig(config2) {
|
|
6666
|
-
const dir =
|
|
7292
|
+
const dir = path13.dirname(pluginConfigPath());
|
|
6667
7293
|
fsSync4.mkdirSync(dir, { recursive: true });
|
|
6668
7294
|
fsSync4.writeFileSync(pluginConfigPath(), JSON.stringify(config2, null, 2));
|
|
6669
7295
|
}
|
|
@@ -6762,7 +7388,7 @@ function registerAdminGateway(api, state2) {
|
|
|
6762
7388
|
api.registerGatewayMethod("agentlife.uninstall", async ({ respond }) => {
|
|
6763
7389
|
try {
|
|
6764
7390
|
const cleaned = [];
|
|
6765
|
-
const baseDir = state2.agentlifeStateDir ??
|
|
7391
|
+
const baseDir = state2.agentlifeStateDir ?? path13.join(os8.homedir(), ".openclaw", "agentlife");
|
|
6766
7392
|
const identity = loadDeviceIdentity();
|
|
6767
7393
|
if (!identity) {
|
|
6768
7394
|
cleaned.push("server deprovision skipped (no device identity)");
|
|
@@ -6827,7 +7453,7 @@ function registerAdminGateway(api, state2) {
|
|
|
6827
7453
|
} catch {
|
|
6828
7454
|
cleaned.push("agent config cleanup skipped");
|
|
6829
7455
|
}
|
|
6830
|
-
const backupPath =
|
|
7456
|
+
const backupPath = path13.join(baseDir, "config-backup.json");
|
|
6831
7457
|
try {
|
|
6832
7458
|
cleaned.push(await restoreConfigFromBackup(api, backupPath));
|
|
6833
7459
|
} catch {
|
|
@@ -6850,35 +7476,35 @@ function registerAdminGateway(api, state2) {
|
|
|
6850
7476
|
dbPath,
|
|
6851
7477
|
dbPath ? `${dbPath}-wal` : null,
|
|
6852
7478
|
dbPath ? `${dbPath}-shm` : null,
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
7479
|
+
path13.join(baseDir, "config-backup.json"),
|
|
7480
|
+
path13.join(baseDir, "notification-config.json"),
|
|
7481
|
+
path13.join(baseDir, "canvas-node-identity.json")
|
|
6856
7482
|
].filter(Boolean);
|
|
6857
7483
|
for (const fp of stateFiles) {
|
|
6858
7484
|
try {
|
|
6859
|
-
await
|
|
6860
|
-
cleaned.push(`deleted ${
|
|
7485
|
+
await fs10.unlink(fp);
|
|
7486
|
+
cleaned.push(`deleted ${path13.basename(fp)}`);
|
|
6861
7487
|
} catch {}
|
|
6862
7488
|
}
|
|
6863
7489
|
if (state2.dbBaseDir) {
|
|
6864
7490
|
try {
|
|
6865
|
-
await
|
|
7491
|
+
await fs10.rm(state2.dbBaseDir, { recursive: true, force: true });
|
|
6866
7492
|
cleaned.push("deleted agent databases");
|
|
6867
7493
|
} catch {}
|
|
6868
7494
|
}
|
|
6869
7495
|
for (const agent of PROVISIONED_AGENTS) {
|
|
6870
7496
|
if (agent.existingAgent)
|
|
6871
7497
|
continue;
|
|
6872
|
-
const wsDir = agent.workspaceDir ??
|
|
7498
|
+
const wsDir = agent.workspaceDir ?? path13.join(os8.homedir(), ".openclaw", `workspace-${agent.id}`);
|
|
6873
7499
|
try {
|
|
6874
|
-
await
|
|
7500
|
+
await fs10.rm(wsDir, { recursive: true, force: true });
|
|
6875
7501
|
cleaned.push(`deleted workspace ${agent.id}`);
|
|
6876
7502
|
} catch {}
|
|
6877
7503
|
}
|
|
6878
7504
|
try {
|
|
6879
|
-
const remaining = await
|
|
7505
|
+
const remaining = await fs10.readdir(baseDir);
|
|
6880
7506
|
if (remaining.length === 0) {
|
|
6881
|
-
await
|
|
7507
|
+
await fs10.rmdir(baseDir);
|
|
6882
7508
|
cleaned.push("deleted agentlife state directory");
|
|
6883
7509
|
}
|
|
6884
7510
|
} catch {}
|
|
@@ -7120,6 +7746,295 @@ function parseOffset2(offset) {
|
|
|
7120
7746
|
}
|
|
7121
7747
|
}
|
|
7122
7748
|
|
|
7749
|
+
// gateway/providers.ts
|
|
7750
|
+
import * as fs11 from "node:fs";
|
|
7751
|
+
import * as os9 from "node:os";
|
|
7752
|
+
import * as path14 from "node:path";
|
|
7753
|
+
import {
|
|
7754
|
+
upsertApiKeyProfile,
|
|
7755
|
+
applyAuthProfileConfig
|
|
7756
|
+
} from "openclaw/plugin-sdk/provider-auth-api-key";
|
|
7757
|
+
var catalogCache = null;
|
|
7758
|
+
var catalogPromise = null;
|
|
7759
|
+
var modelsCache = null;
|
|
7760
|
+
var modelsPromise = null;
|
|
7761
|
+
function configPath() {
|
|
7762
|
+
return path14.join(os9.homedir(), ".openclaw", "openclaw.json");
|
|
7763
|
+
}
|
|
7764
|
+
function readConfig() {
|
|
7765
|
+
try {
|
|
7766
|
+
return JSON.parse(fs11.readFileSync(configPath(), "utf-8"));
|
|
7767
|
+
} catch {
|
|
7768
|
+
return {};
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
function writeConfig(cfg) {
|
|
7772
|
+
fs11.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + `
|
|
7773
|
+
`, "utf-8");
|
|
7774
|
+
}
|
|
7775
|
+
async function fetchCatalog(run) {
|
|
7776
|
+
const result = await run(["openclaw", "infer", "model", "providers", "--json"], { timeoutMs: 60000 });
|
|
7777
|
+
if ((result?.code ?? 0) !== 0)
|
|
7778
|
+
throw new Error(`providers catalog query failed`);
|
|
7779
|
+
const raw = (result?.stdout ?? "").trim();
|
|
7780
|
+
const jsonStart = raw.indexOf("[");
|
|
7781
|
+
if (jsonStart < 0)
|
|
7782
|
+
throw new Error("providers catalog: no JSON array in output");
|
|
7783
|
+
const parsed = JSON.parse(raw.slice(jsonStart));
|
|
7784
|
+
if (!Array.isArray(parsed))
|
|
7785
|
+
throw new Error("providers catalog: not an array");
|
|
7786
|
+
return parsed;
|
|
7787
|
+
}
|
|
7788
|
+
async function ensureCatalog(run) {
|
|
7789
|
+
if (catalogCache)
|
|
7790
|
+
return catalogCache;
|
|
7791
|
+
if (!run)
|
|
7792
|
+
return [];
|
|
7793
|
+
if (!catalogPromise) {
|
|
7794
|
+
catalogPromise = fetchCatalog(run).then((c) => {
|
|
7795
|
+
catalogCache = c;
|
|
7796
|
+
return c;
|
|
7797
|
+
}).catch((err) => {
|
|
7798
|
+
catalogPromise = null;
|
|
7799
|
+
throw err;
|
|
7800
|
+
});
|
|
7801
|
+
}
|
|
7802
|
+
return catalogPromise;
|
|
7803
|
+
}
|
|
7804
|
+
async function fetchModels(run) {
|
|
7805
|
+
const result = await run(["openclaw", "infer", "model", "list", "--json"], { timeoutMs: 60000 });
|
|
7806
|
+
if ((result?.code ?? 0) !== 0)
|
|
7807
|
+
throw new Error(`models list query failed`);
|
|
7808
|
+
const raw = (result?.stdout ?? "").trim();
|
|
7809
|
+
const jsonStart = raw.indexOf("[");
|
|
7810
|
+
if (jsonStart < 0)
|
|
7811
|
+
throw new Error("models list: no JSON array in output");
|
|
7812
|
+
const parsed = JSON.parse(raw.slice(jsonStart));
|
|
7813
|
+
if (!Array.isArray(parsed))
|
|
7814
|
+
throw new Error("models list: not an array");
|
|
7815
|
+
return parsed;
|
|
7816
|
+
}
|
|
7817
|
+
async function ensureModels(run) {
|
|
7818
|
+
if (modelsCache)
|
|
7819
|
+
return modelsCache;
|
|
7820
|
+
if (!run)
|
|
7821
|
+
return [];
|
|
7822
|
+
if (!modelsPromise) {
|
|
7823
|
+
modelsPromise = fetchModels(run).then((m) => {
|
|
7824
|
+
modelsCache = m;
|
|
7825
|
+
return m;
|
|
7826
|
+
}).catch((err) => {
|
|
7827
|
+
modelsPromise = null;
|
|
7828
|
+
throw err;
|
|
7829
|
+
});
|
|
7830
|
+
}
|
|
7831
|
+
return modelsPromise;
|
|
7832
|
+
}
|
|
7833
|
+
function authenticatedProviders() {
|
|
7834
|
+
const cfg = readConfig();
|
|
7835
|
+
const profiles = cfg?.auth?.profiles ?? {};
|
|
7836
|
+
const out = new Set;
|
|
7837
|
+
for (const entry of Object.values(profiles)) {
|
|
7838
|
+
const e = entry;
|
|
7839
|
+
if (typeof e?.provider === "string" && typeof e?.mode === "string") {
|
|
7840
|
+
out.add(e.provider);
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
return out;
|
|
7844
|
+
}
|
|
7845
|
+
function configuredApiKeyProviders() {
|
|
7846
|
+
const cfg = readConfig();
|
|
7847
|
+
const profiles = cfg?.auth?.profiles ?? {};
|
|
7848
|
+
const out = new Set;
|
|
7849
|
+
for (const entry of Object.values(profiles)) {
|
|
7850
|
+
const e = entry;
|
|
7851
|
+
if (e?.mode === "api_key" && typeof e.provider === "string") {
|
|
7852
|
+
out.add(e.provider);
|
|
7853
|
+
}
|
|
7854
|
+
}
|
|
7855
|
+
return out;
|
|
7856
|
+
}
|
|
7857
|
+
async function probeAnthropic(apiKey) {
|
|
7858
|
+
const ctl = new AbortController;
|
|
7859
|
+
const timer = setTimeout(() => ctl.abort(), 1e4);
|
|
7860
|
+
try {
|
|
7861
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
7862
|
+
method: "POST",
|
|
7863
|
+
headers: {
|
|
7864
|
+
"x-api-key": apiKey,
|
|
7865
|
+
"anthropic-version": "2023-06-01",
|
|
7866
|
+
"content-type": "application/json"
|
|
7867
|
+
},
|
|
7868
|
+
body: JSON.stringify({
|
|
7869
|
+
model: "claude-haiku-4-5",
|
|
7870
|
+
max_tokens: 1,
|
|
7871
|
+
messages: [{ role: "user", content: "ping" }]
|
|
7872
|
+
}),
|
|
7873
|
+
signal: ctl.signal
|
|
7874
|
+
});
|
|
7875
|
+
if (res.ok)
|
|
7876
|
+
return { ok: true };
|
|
7877
|
+
const body = await res.text().catch(() => "");
|
|
7878
|
+
return { ok: false, error: `Anthropic ${res.status}: ${body.slice(0, 240) || res.statusText}` };
|
|
7879
|
+
} catch (err) {
|
|
7880
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
7881
|
+
} finally {
|
|
7882
|
+
clearTimeout(timer);
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
async function probeOpenAI(apiKey) {
|
|
7886
|
+
const ctl = new AbortController;
|
|
7887
|
+
const timer = setTimeout(() => ctl.abort(), 1e4);
|
|
7888
|
+
try {
|
|
7889
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
7890
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
7891
|
+
signal: ctl.signal
|
|
7892
|
+
});
|
|
7893
|
+
if (res.ok)
|
|
7894
|
+
return { ok: true };
|
|
7895
|
+
const body = await res.text().catch(() => "");
|
|
7896
|
+
return { ok: false, error: `OpenAI ${res.status}: ${body.slice(0, 240) || res.statusText}` };
|
|
7897
|
+
} catch (err) {
|
|
7898
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
7899
|
+
} finally {
|
|
7900
|
+
clearTimeout(timer);
|
|
7901
|
+
}
|
|
7902
|
+
}
|
|
7903
|
+
function canProbe(provider) {
|
|
7904
|
+
return provider === "anthropic" || provider === "openai";
|
|
7905
|
+
}
|
|
7906
|
+
async function probe(provider, apiKey) {
|
|
7907
|
+
if (provider === "anthropic")
|
|
7908
|
+
return probeAnthropic(apiKey);
|
|
7909
|
+
if (provider === "openai")
|
|
7910
|
+
return probeOpenAI(apiKey);
|
|
7911
|
+
return { ok: false, error: `test not supported for provider: ${provider}` };
|
|
7912
|
+
}
|
|
7913
|
+
function registerProvidersGateway(api, state2) {
|
|
7914
|
+
const run = state2.runCommand;
|
|
7915
|
+
if (run) {
|
|
7916
|
+
ensureCatalog(run).catch(() => {});
|
|
7917
|
+
ensureModels(run).catch(() => {});
|
|
7918
|
+
}
|
|
7919
|
+
api.registerGatewayMethod("agentlife.providers.list", async ({ respond }) => {
|
|
7920
|
+
try {
|
|
7921
|
+
const [catalog, configured] = await Promise.all([
|
|
7922
|
+
ensureCatalog(run ?? null),
|
|
7923
|
+
Promise.resolve(configuredApiKeyProviders())
|
|
7924
|
+
]);
|
|
7925
|
+
const providers = catalog.map((entry) => ({
|
|
7926
|
+
provider: entry.provider,
|
|
7927
|
+
modelCount: entry.count,
|
|
7928
|
+
available: entry.available,
|
|
7929
|
+
configured: configured.has(entry.provider),
|
|
7930
|
+
canProbe: canProbe(entry.provider)
|
|
7931
|
+
}));
|
|
7932
|
+
respond(true, { providers });
|
|
7933
|
+
} catch (err) {
|
|
7934
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7935
|
+
}
|
|
7936
|
+
}, { scope: "operator.read" });
|
|
7937
|
+
api.registerGatewayMethod("agentlife.providers.set", async ({ params, respond }) => {
|
|
7938
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7939
|
+
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
7940
|
+
if (!provider)
|
|
7941
|
+
return respond(false, { error: "provider is empty" });
|
|
7942
|
+
if (!apiKey)
|
|
7943
|
+
return respond(false, { error: "apiKey is empty" });
|
|
7944
|
+
try {
|
|
7945
|
+
const catalog = await ensureCatalog(run ?? null);
|
|
7946
|
+
if (catalog.length > 0 && !catalog.some((e) => e.provider === provider)) {
|
|
7947
|
+
return respond(false, { error: `unknown provider: ${provider}` });
|
|
7948
|
+
}
|
|
7949
|
+
} catch {}
|
|
7950
|
+
try {
|
|
7951
|
+
const profileId = upsertApiKeyProfile({ provider, input: apiKey });
|
|
7952
|
+
const cfg = readConfig();
|
|
7953
|
+
const nextCfg = applyAuthProfileConfig(cfg, {
|
|
7954
|
+
profileId,
|
|
7955
|
+
provider,
|
|
7956
|
+
mode: "api_key",
|
|
7957
|
+
preferProfileFirst: true
|
|
7958
|
+
});
|
|
7959
|
+
writeConfig(nextCfg);
|
|
7960
|
+
respond(true, { provider, configured: true, profileId });
|
|
7961
|
+
} catch (err) {
|
|
7962
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7963
|
+
}
|
|
7964
|
+
}, { scope: "operator.write" });
|
|
7965
|
+
api.registerGatewayMethod("agentlife.providers.delete", ({ params, respond }) => {
|
|
7966
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7967
|
+
if (!provider)
|
|
7968
|
+
return respond(false, { error: "provider is empty" });
|
|
7969
|
+
try {
|
|
7970
|
+
const cfg = readConfig();
|
|
7971
|
+
const profiles = cfg?.auth?.profiles;
|
|
7972
|
+
if (profiles && typeof profiles === "object") {
|
|
7973
|
+
for (const [id, entry] of Object.entries(profiles)) {
|
|
7974
|
+
if (entry?.provider === provider)
|
|
7975
|
+
delete profiles[id];
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
if (cfg?.auth?.order && typeof cfg.auth.order === "object") {
|
|
7979
|
+
delete cfg.auth.order[provider];
|
|
7980
|
+
}
|
|
7981
|
+
writeConfig(cfg);
|
|
7982
|
+
respond(true, { provider, configured: false });
|
|
7983
|
+
} catch (err) {
|
|
7984
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7985
|
+
}
|
|
7986
|
+
}, { scope: "operator.write" });
|
|
7987
|
+
api.registerGatewayMethod("agentlife.providers.test", async ({ params, respond }) => {
|
|
7988
|
+
const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
|
|
7989
|
+
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
7990
|
+
if (!provider)
|
|
7991
|
+
return respond(false, { error: "provider is empty" });
|
|
7992
|
+
if (!apiKey)
|
|
7993
|
+
return respond(true, { ok: false, error: "apiKey required — type a key before testing" });
|
|
7994
|
+
try {
|
|
7995
|
+
const result = await probe(provider, apiKey);
|
|
7996
|
+
respond(true, { ok: result.ok, error: result.error });
|
|
7997
|
+
} catch (err) {
|
|
7998
|
+
respond(true, { ok: false, error: err?.message ?? String(err) });
|
|
7999
|
+
}
|
|
8000
|
+
}, { scope: "operator.read" });
|
|
8001
|
+
api.registerGatewayMethod("agentlife.models.list", async ({ respond }) => {
|
|
8002
|
+
try {
|
|
8003
|
+
const [all, authed] = await Promise.all([
|
|
8004
|
+
ensureModels(run ?? null),
|
|
8005
|
+
Promise.resolve(authenticatedProviders())
|
|
8006
|
+
]);
|
|
8007
|
+
const filtered = all.filter((m) => authed.has(m.provider));
|
|
8008
|
+
const DATE_SUFFIX = /-(?:\d{4}-\d{2}-\d{2}|\d{8})$/;
|
|
8009
|
+
const undatedIds = new Set(filtered.filter((m) => !DATE_SUFFIX.test(m.id)).map((m) => m.id));
|
|
8010
|
+
const deduped = filtered.filter((m) => {
|
|
8011
|
+
if (!DATE_SUFFIX.test(m.id))
|
|
8012
|
+
return true;
|
|
8013
|
+
return !undatedIds.has(m.id.replace(DATE_SUFFIX, ""));
|
|
8014
|
+
});
|
|
8015
|
+
respond(true, { models: deduped });
|
|
8016
|
+
} catch (err) {
|
|
8017
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
8018
|
+
}
|
|
8019
|
+
}, { scope: "operator.read" });
|
|
8020
|
+
api.registerGatewayMethod("agentlife.providers.loginCodex", async ({ respond }) => {
|
|
8021
|
+
if (!run)
|
|
8022
|
+
return respond(false, { error: "runCommand unavailable" });
|
|
8023
|
+
try {
|
|
8024
|
+
const result = await run(["openclaw", "capability", "model", "auth", "login", "--provider", "openai-codex"], { timeoutMs: 180000 });
|
|
8025
|
+
const combined = `${result?.stdout ?? ""}
|
|
8026
|
+
${result?.stderr ?? ""}`;
|
|
8027
|
+
const urlMatch = combined.match(/https:\/\/\S+/);
|
|
8028
|
+
if ((result?.code ?? 0) !== 0 && !urlMatch) {
|
|
8029
|
+
return respond(false, { error: (result?.stderr ?? "").trim() || "codex login failed" });
|
|
8030
|
+
}
|
|
8031
|
+
respond(true, { url: urlMatch?.[0] ?? null, completed: (result?.code ?? 0) === 0 });
|
|
8032
|
+
} catch (err) {
|
|
8033
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
8034
|
+
}
|
|
8035
|
+
}, { scope: "operator.write" });
|
|
8036
|
+
}
|
|
8037
|
+
|
|
7123
8038
|
// index.ts
|
|
7124
8039
|
var currentState = null;
|
|
7125
8040
|
var registered = false;
|
|
@@ -7129,7 +8044,7 @@ var stopQualityCheckPoller = null;
|
|
|
7129
8044
|
var stopCloudflared = null;
|
|
7130
8045
|
function resolveInternalModel(api) {
|
|
7131
8046
|
try {
|
|
7132
|
-
const pluginCfgPath =
|
|
8047
|
+
const pluginCfgPath = path15.join(homedir11(), ".openclaw", "agentlife", "plugin-config.json");
|
|
7133
8048
|
try {
|
|
7134
8049
|
const raw = __require("node:fs").readFileSync(pluginCfgPath, "utf-8");
|
|
7135
8050
|
const pluginCfg = JSON.parse(raw);
|
|
@@ -7165,7 +8080,7 @@ function register(api) {
|
|
|
7165
8080
|
return;
|
|
7166
8081
|
}
|
|
7167
8082
|
registered = true;
|
|
7168
|
-
const fallbackDir =
|
|
8083
|
+
const fallbackDir = path15.join(homedir11(), ".openclaw", "agentlife");
|
|
7169
8084
|
const state2 = {
|
|
7170
8085
|
surfaceDb: null,
|
|
7171
8086
|
agentRegistry: new Map,
|
|
@@ -7175,9 +8090,9 @@ function register(api) {
|
|
|
7175
8090
|
agentDbs: new Map,
|
|
7176
8091
|
historyDb: null,
|
|
7177
8092
|
agentlifeStateDir: fallbackDir,
|
|
7178
|
-
registryFilePath:
|
|
7179
|
-
dbBaseDir:
|
|
7180
|
-
historyDbPath:
|
|
8093
|
+
registryFilePath: path15.join(fallbackDir, "agent-registry.json"),
|
|
8094
|
+
dbBaseDir: path15.join(fallbackDir, "db"),
|
|
8095
|
+
historyDbPath: path15.join(fallbackDir, "agentlife.db"),
|
|
7181
8096
|
runCommand: api.runtime.system?.runCommandWithTimeout ?? null,
|
|
7182
8097
|
enqueueSystemEvent: null,
|
|
7183
8098
|
requestHeartbeatNow: null,
|
|
@@ -7208,6 +8123,7 @@ function register(api) {
|
|
|
7208
8123
|
stopObservability = startObservabilityService(state2);
|
|
7209
8124
|
stopDailySweep = startDailySweepService(state2);
|
|
7210
8125
|
stopQualityCheckPoller = startQualityCheckPoller(state2);
|
|
8126
|
+
await initColdStartMachine(state2, api.runtime, console.log);
|
|
7211
8127
|
}
|
|
7212
8128
|
});
|
|
7213
8129
|
registerConfigOptimizer(api, state2);
|
|
@@ -7234,6 +8150,7 @@ function register(api) {
|
|
|
7234
8150
|
stopCloudflared();
|
|
7235
8151
|
stopCloudflared = null;
|
|
7236
8152
|
}
|
|
8153
|
+
shutdownColdStartMachine();
|
|
7237
8154
|
closeAllDbs(state2);
|
|
7238
8155
|
}
|
|
7239
8156
|
});
|
|
@@ -7250,8 +8167,9 @@ function register(api) {
|
|
|
7250
8167
|
registerAutomationsGateway(api, state2);
|
|
7251
8168
|
registerFollowupsGateway(api, state2);
|
|
7252
8169
|
registerAdminGateway(api, state2);
|
|
8170
|
+
registerProvidersGateway(api, state2);
|
|
7253
8171
|
registerWebApp(api);
|
|
7254
|
-
const notifyConfigPath =
|
|
8172
|
+
const notifyConfigPath = path15.join(fallbackDir, "notification-config.json");
|
|
7255
8173
|
api.registerGatewayMethod("agentlife.notifications.register", ({ params, respond }) => {
|
|
7256
8174
|
const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
|
|
7257
8175
|
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
@@ -7259,23 +8177,45 @@ function register(api) {
|
|
|
7259
8177
|
return respond(false, { error: "missing serverUrl or apiKey" });
|
|
7260
8178
|
}
|
|
7261
8179
|
try {
|
|
7262
|
-
const { writeFileSync:
|
|
7263
|
-
mkdirSync6(
|
|
7264
|
-
|
|
8180
|
+
const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync6 } = __require("node:fs");
|
|
8181
|
+
mkdirSync6(path15.dirname(notifyConfigPath), { recursive: true });
|
|
8182
|
+
writeFileSync8(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
|
|
7265
8183
|
} catch {}
|
|
7266
8184
|
respond(true, { registered: true });
|
|
7267
8185
|
}, { scope: "operator.write" });
|
|
7268
|
-
api.registerGatewayMethod("agentlife.
|
|
8186
|
+
api.registerGatewayMethod("agentlife.coldStart.observe", ({ respond }) => {
|
|
7269
8187
|
try {
|
|
7270
8188
|
if (!currentState) {
|
|
7271
8189
|
return respond(false, { error: "plugin state not initialized" });
|
|
7272
8190
|
}
|
|
7273
|
-
|
|
7274
|
-
|
|
8191
|
+
respond(true, observeColdStart(currentState));
|
|
8192
|
+
} catch (e) {
|
|
8193
|
+
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8194
|
+
}
|
|
8195
|
+
}, { scope: "operator.read" });
|
|
8196
|
+
api.registerGatewayMethod("agentlife.coldStart.retry", async ({ respond }) => {
|
|
8197
|
+
try {
|
|
8198
|
+
if (!currentState) {
|
|
8199
|
+
return respond(false, { error: "plugin state not initialized" });
|
|
8200
|
+
}
|
|
8201
|
+
respond(true, await retryColdStart(currentState));
|
|
7275
8202
|
} catch (e) {
|
|
7276
8203
|
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
7277
8204
|
}
|
|
7278
8205
|
}, { scope: "operator.read" });
|
|
8206
|
+
api.registerGatewayMethod("agentlife.coldStart.advance", ({ params, respond }) => {
|
|
8207
|
+
try {
|
|
8208
|
+
if (!currentState)
|
|
8209
|
+
return respond(false, { error: "plugin state not initialized" });
|
|
8210
|
+
const kind = typeof params?.kind === "string" ? params.kind : null;
|
|
8211
|
+
if (!kind)
|
|
8212
|
+
return respond(false, { error: "missing kind" });
|
|
8213
|
+
emitTrigger(currentState, kind, params?.payload ?? {});
|
|
8214
|
+
respond(true, { enqueued: true });
|
|
8215
|
+
} catch (e) {
|
|
8216
|
+
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8217
|
+
}
|
|
8218
|
+
}, { scope: "operator.admin" });
|
|
7279
8219
|
}
|
|
7280
8220
|
export {
|
|
7281
8221
|
register as default,
|