agentlife 1.5.3 → 2.0.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 +1033 -1404
- 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 homedir12 } from "node:os";
|
|
6
|
+
import * as path16 from "node:path";
|
|
7
7
|
import { existsSync as existsSync6 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// db.ts
|
|
@@ -193,31 +193,6 @@ 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
|
-
|
|
221
196
|
`);
|
|
222
197
|
try {
|
|
223
198
|
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN cronId TEXT");
|
|
@@ -661,20 +636,7 @@ When you need structured user input, push a surface with the \`input\` keyword i
|
|
|
661
636
|
4. User responds → you receive \`[action:choice]\` with \`label=<their selection>\` (same format for button clicks and free text)
|
|
662
637
|
5. Delete the input surface immediately. Then update parent widget. Push next input surface only if more questions needed. When all answers are collected, delete the input surface BEFORE starting work — never leave an input surface open while processing.
|
|
663
638
|
|
|
664
|
-
**Input surface
|
|
665
|
-
\`\`\`
|
|
666
|
-
surface <id> input
|
|
667
|
-
card
|
|
668
|
-
column
|
|
669
|
-
text "<question>" h4
|
|
670
|
-
button "<option>" action=choice
|
|
671
|
-
button "<option>" action=choice
|
|
672
|
-
textfield placeholder="<free text hint>"
|
|
673
|
-
goal: <what this question collects>
|
|
674
|
-
followup: +<timeout> "<what to do if user doesn't respond>"
|
|
675
|
-
\`\`\`
|
|
676
|
-
|
|
677
|
-
**Input surfaces are widgets.** They have a surfaceId, goal, followup, and context — same rules as any widget. The followup handles the case where the user walks away.
|
|
639
|
+
**Input surfaces are widgets.** They have a surfaceId, goal, followup, and context — same rules as any widget. The followup handles the case where the user walks away. Structure: the input keyword goes on the surface header; the body is a card with a column containing an h3 question, two or more buttons, and optionally a textfield to customize the native input bar's placeholder.
|
|
678
640
|
|
|
679
641
|
**User controls:**
|
|
680
642
|
- Close [×] = "not now." You'll see \`input_closed\` — push again on next followup if still relevant.
|
|
@@ -682,11 +644,17 @@ followup: +<timeout> "<what to do if user doesn't respond>"
|
|
|
682
644
|
|
|
683
645
|
**Rules:**
|
|
684
646
|
- Never push input surfaces unsolicited — always after a user action on a widget
|
|
685
|
-
-
|
|
686
|
-
-
|
|
647
|
+
- **Input surface buttons are constrained.** The input bar only renders two shapes:
|
|
648
|
+
- \`action=choice\` or \`action=select\` → numbered single-select list (pick one).
|
|
649
|
+
- \`action=toggle\`, \`action=check\`, or \`action=multichoice\` → checkbox multi-select list.
|
|
650
|
+
|
|
651
|
+
No other \`action=\` values are allowed inside an \`input\` surface. Custom names (e.g. \`action=domain\`, \`action=rating\`) render as plain outlined buttons and break the input-bar UX — the platform emits a QUALITY ERROR.
|
|
652
|
+
|
|
653
|
+
Custom action names (\`action=approve\`, \`action=investigate\`, \`action=reset\`) are for **regular dashboard widgets** only, not input surfaces.
|
|
654
|
+
- **Dispatch format.** \`[action:choice] label=<label>\` for single-select (one tap fires the action). \`[action:toggle] label=<A>; <B>; <C>\` for multi-select (labels joined with \`; \` — the user picks boxes locally, then one tap of send dispatches all selections at once). Your handler routes on the action name and reads \`label\`. For toggle, split \`label\` on \`"; "\` to recover the individual selections.
|
|
687
655
|
- The platform auto-numbers choice buttons — don't add numbers to labels
|
|
688
656
|
- For multi-step flows: delete the old input surface, push a new one for each step
|
|
689
|
-
-
|
|
657
|
+
- The native input bar is always available for free text — users can always type. Declare \`textfield placeholder="..."\` to customize the placeholder hint (e.g. "Something else"). Without it, the default placeholder is used.
|
|
690
658
|
|
|
691
659
|
### context: — Lookup Keys for the Next Followup
|
|
692
660
|
|
|
@@ -732,22 +700,12 @@ The alternatives must be useful to the user, not feedback collection. You know t
|
|
|
732
700
|
- Common alternative types: adjust scope (broader/narrower), change timing (more/less frequent), shift focus (different aspect of the same domain), reschedule (not now, try later with specific time).
|
|
733
701
|
- 2-3 options maximum. More is decision fatigue.
|
|
734
702
|
- "Remove it" is always last. Use \`action=choice\` so it renders as a numbered list.
|
|
735
|
-
|
|
736
|
-
\`\`\`
|
|
737
|
-
surface dismiss-alt-{id} input
|
|
738
|
-
card
|
|
739
|
-
column
|
|
740
|
-
text "Before I remove this:" h4
|
|
741
|
-
button "<contextual alternative 1>" action=choice
|
|
742
|
-
button "<contextual alternative 2>" action=choice
|
|
743
|
-
button "Remove it" action=choice
|
|
744
|
-
goal: Offer alternatives before dismissing {surfaceId}
|
|
745
|
-
followup: +1m "User did not respond to dismiss alternatives. Say done."
|
|
746
|
-
\`\`\`
|
|
703
|
+
- Use surfaceId \`dismiss-alt-{parent-surfaceId}\`, set goal to reference the parent, set a short followup (~1m) so the flow doesn't stall if the user walks away.
|
|
747
704
|
|
|
748
705
|
On final dismiss:
|
|
749
706
|
1. Clean up any custom infra (scripts, webhooks, background processes)
|
|
750
707
|
2. Say "done" when finished
|
|
708
|
+
3. Delete the widget
|
|
751
709
|
|
|
752
710
|
### Autonomy & Approval
|
|
753
711
|
|
|
@@ -771,28 +729,6 @@ When receiving via sessions_send: push widgets with results. If nothing to push,
|
|
|
771
729
|
|
|
772
730
|
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.
|
|
773
731
|
|
|
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
|
-
|
|
796
732
|
`;
|
|
797
733
|
var INTERNAL_AGENTS_MD = `
|
|
798
734
|
## Internal Session — Self-Improvement Protocol
|
|
@@ -1153,352 +1089,61 @@ If a question looks like it belongs to a life domain (health, finance, business)
|
|
|
1153
1089
|
`;
|
|
1154
1090
|
var BUILDER_AGENTS_MD = `# AgentLife Builder
|
|
1155
1091
|
|
|
1156
|
-
You create and improve agents for the AgentLife dashboard
|
|
1157
|
-
|
|
1158
|
-
## What an Agent Is
|
|
1159
|
-
|
|
1160
|
-
An agent = workspace directory + workspace files + a config entry + initialized data.
|
|
1161
|
-
|
|
1162
|
-
### Workspace Files
|
|
1163
|
-
|
|
1164
|
-
The gateway loads ALL workspace files into the agent's system prompt at session start. Every file you write reaches the agent. TOOLS.md is the exception — the platform replaces it with the widget/canvas reference at runtime. You still write it as a placeholder.
|
|
1092
|
+
You create and improve specialist agents for the AgentLife dashboard.
|
|
1165
1093
|
|
|
1166
|
-
|
|
1167
|
-
|------|---------|----------------|
|
|
1168
|
-
| **AGENTS.md** | Domain logic, knowledge strategy, data schemas, environment, operational rules. The core behavioral file. | ALWAYS |
|
|
1169
|
-
| **SOUL.md** | Personality and communication style ONLY. Gets wrapped under a "Personality" subsection — keep it to tone, formality, language. One short paragraph. | ALWAYS |
|
|
1170
|
-
| **IDENTITY.md** | Agent's self-identity — name, emoji, avatar, vibe. The agent reads this to know who it is. | ALWAYS — fill in agent name, emoji, and personality vibe |
|
|
1171
|
-
| **USER.md** | Domain-specific facts about the user that this agent needs. Health gets diet/metrics, home-automation gets network/devices, etc. | When the domain needs user context |
|
|
1172
|
-
| **HEARTBEAT.md** | Periodic check instructions. Empty = no heartbeat. Add tasks when the agent should poll. | When the agent needs periodic autonomous checks |
|
|
1173
|
-
|
|
1174
|
-
**Config entry** — registered via \`agentlife.createAgent\` with tool access and identity.
|
|
1175
|
-
**Data schema in AGENTS.md** — CREATE TABLE IF NOT EXISTS statements so the agent self-initializes on first use.
|
|
1176
|
-
|
|
1177
|
-
## One Agent Per Life Domain
|
|
1178
|
-
|
|
1179
|
-
Agents map to life domains — not business functions. One business agent handles marketing, sales, operations, analytics — everything. Splitting creates context fragmentation.
|
|
1094
|
+
## Rules
|
|
1180
1095
|
|
|
1181
|
-
|
|
1096
|
+
- One agent per life domain (health, finance, home, etc.). Check \`agents_list\` first — expand an existing agent's AGENTS.md rather than duplicate its domain.
|
|
1097
|
+
- NEVER push a confirmation / "ready" / "created" widget. The deterministic intro widget from the plugin IS the confirmation.
|
|
1098
|
+
- Emit \`done\` ONLY after \`agentlife.createAgent\` has succeeded.
|
|
1182
1099
|
|
|
1183
1100
|
## Creating a New Agent
|
|
1184
1101
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
2. **SOUL.md** — one short paragraph: how the agent speaks. Not what it does.
|
|
1214
|
-
Good: "Precise and minimal. Confirm actions in one line. Match the user's language."
|
|
1215
|
-
Bad: "Device discovery: 1. Check devices.json 2. Run discover.py" — operational, belongs in AGENTS.md.
|
|
1216
|
-
3. **IDENTITY.md** — fill in the agent's identity:
|
|
1217
|
-
\`\`\`
|
|
1218
|
-
# IDENTITY.md — {Agent Name}
|
|
1219
|
-
- **Agent ID:** {agentId}
|
|
1220
|
-
- **Name:** {Display Name}
|
|
1221
|
-
- **Emoji:** {emoji}
|
|
1222
|
-
- **Vibe:** {how the agent comes across — sharp? warm? philosophical? precise?}
|
|
1223
|
-
- **Avatar:** {path if provided, otherwise omit}
|
|
1224
|
-
\`\`\`
|
|
1225
|
-
4. **USER.md** — domain-specific user info. Include name, timezone, language, and facts relevant to THIS agent's domain. Read the user's other workspace USER.md files to get their identity — don't guess.
|
|
1226
|
-
5. **HEARTBEAT.md** — if the agent needs periodic checks, list them. Otherwise write an empty placeholder:
|
|
1227
|
-
\`\`\`
|
|
1228
|
-
# HEARTBEAT.md
|
|
1229
|
-
# Keep empty to skip heartbeat checks.
|
|
1230
|
-
\`\`\`
|
|
1231
|
-
|
|
1232
|
-
### Step 4: Create Scripts (if needed)
|
|
1233
|
-
|
|
1234
|
-
If the domain needs automation scripts:
|
|
1235
|
-
1. Write the script, then **test it with exec** — run it and verify output before moving on.
|
|
1236
|
-
2. Config data (scenes, endpoints, device lists) lives in data files (JSON), not hardcoded in scripts AND AGENTS.md. Single source of truth.
|
|
1237
|
-
3. Verify dependencies: \`which nmap\`, \`python3 -c "import json"\`.
|
|
1238
|
-
|
|
1239
|
-
### Step 5: Register the Agent
|
|
1240
|
-
|
|
1241
|
-
Call gateway tool with method "agentlife.createAgent":
|
|
1242
|
-
\`\`\`json
|
|
1243
|
-
{"id": "{agentId}", "name": "Display Name", "model": "{model}", "workspace": "/full/path", "description": "One sentence — specific domains, actions, data types.", "tools": {"profile": "full"}, "identity": {"name": "Display Name", "emoji": "{emoji}"}}
|
|
1244
|
-
\`\`\`
|
|
1245
|
-
|
|
1246
|
-
Use $HOME, not ~. Description is critical — the orchestrator routes by it.
|
|
1247
|
-
|
|
1248
|
-
**tools config:**
|
|
1249
|
-
- \`{"profile": "full"}\` — agent gets all tools (web_search, exec, read, write, agentlife_push, etc.). Use for agents that need web research, script execution, or file operations.
|
|
1250
|
-
- \`{"allow": ["agentlife_push"]}\` — restricted to specific tools only. Use for lightweight agents that only push widgets.
|
|
1251
|
-
- If the agent needs exec (scripts, image generation), web_search (research), or write (file operations), it MUST have \`profile: "full"\`.
|
|
1252
|
-
|
|
1253
|
-
**identity config:** Sets the agent's display name and emoji in the platform UI. Match what you wrote in IDENTITY.md.
|
|
1254
|
-
|
|
1255
|
-
### Step 6: Confirm via Widget
|
|
1256
|
-
|
|
1257
|
-
Push a confirmation widget that seeds the agent's first context:
|
|
1258
|
-
|
|
1259
|
-
\`\`\`
|
|
1260
|
-
surface agent-created size=m
|
|
1261
|
-
card
|
|
1262
|
-
column
|
|
1263
|
-
text "{Agent Name} is ready" h3
|
|
1264
|
-
text "Model: {model} · ID: {agentId}" caption
|
|
1265
|
-
goal: User sends first request to this agent
|
|
1266
|
-
detail: Created agent "{agentId}" for {domain}. The orchestrator will route matching requests here.
|
|
1267
|
-
followup: +1h "Check if user has sent a request. If not, push a proactive suggestion based on the domain."
|
|
1268
|
-
context: {"agentId":"{agentId}","domain":"{domain}","tables":[list of created tables],"environment":{discovered facts},"status":"ready"}
|
|
1269
|
-
\`\`\`
|
|
1270
|
-
|
|
1271
|
-
## Writing AGENTS.md
|
|
1272
|
-
|
|
1273
|
-
Required sections:
|
|
1274
|
-
- **Role** — what the agent does and doesn't do
|
|
1275
|
-
- **Boundaries** — explicit scope limits
|
|
1276
|
-
- **Environment** — real system facts discovered in Step 2 (IPs, paths, endpoints, available tools)
|
|
1277
|
-
- **Knowledge Strategy** — what goes in context: vs agentlife.db for this domain (see below)
|
|
1278
|
-
- **Data Schema** — CREATE TABLE IF NOT EXISTS statements (agent self-initializes on first use)
|
|
1279
|
-
- **Followup Strategy** — domain-specific check patterns, intervals, deletion triggers
|
|
1280
|
-
- **Goal Patterns** — typical goal: values (concrete, verifiable outcomes)
|
|
1281
|
-
- **Proactive Triggers** — what to discover autonomously, when to delegate
|
|
1282
|
-
- **Approval Patterns** — which actions need user approval
|
|
1283
|
-
- **Widget Structure** — what the face shows (metrics, badges, gauges for this domain)
|
|
1284
|
-
|
|
1285
|
-
### Knowledge Strategy Section
|
|
1286
|
-
|
|
1287
|
-
The platform already injects the general knowledge rules (context: vs agentlife.db vs what to avoid) via TOOLS.md. Do NOT repeat those rules in AGENTS.md.
|
|
1288
|
-
|
|
1289
|
-
AGENTS.md should define the **domain-specific** storage mapping — what goes where for THIS agent:
|
|
1290
|
-
|
|
1291
|
-
**context: fields** — the lookup keys for this domain:
|
|
1292
|
-
\`{"tables":[...], "filters":{...}, "phase":"...", "config":{...}}\`
|
|
1293
|
-
|
|
1294
|
-
**agentlife.db tables** — the schemas with CREATE TABLE IF NOT EXISTS and how they're queried on followup:
|
|
1295
|
-
\`{tableName}({columns}) → query on followup to compute {what}, store result pointer in context:\`
|
|
1296
|
-
|
|
1297
|
-
Don't repeat the general context: vs agentlife.db rules — the platform injects those. Just define the domain-specific mapping.
|
|
1298
|
-
|
|
1299
|
-
### Guidelines
|
|
1300
|
-
|
|
1301
|
-
1. **Specific, not generic.** "Search flights on Google Flights" > "Help with travel."
|
|
1302
|
-
2. **Tool-first.** Write instructions around available tools, not abstract behaviors.
|
|
1303
|
-
3. **Real data.** Include IPs, paths, endpoints from Step 2 — never placeholders or guesses.
|
|
1304
|
-
4. **Error paths.** What happens when tools fail?
|
|
1305
|
-
5. **Concise but complete.** Cover the full domain — don't sacrifice completeness for brevity.
|
|
1306
|
-
|
|
1307
|
-
## Registry Enrichment
|
|
1308
|
-
|
|
1309
|
-
When you receive "Enrich agent registry descriptions" — system task, no widgets.
|
|
1310
|
-
|
|
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".
|
|
1312
|
-
|
|
1313
|
-
## Deleting an Agent
|
|
1314
|
-
|
|
1315
|
-
1. agentlife.deleteAgent: {"id": "{agentId}"}
|
|
1316
|
-
2. Optionally: exec rm -rf ~/.openclaw/workspace-{agentId}
|
|
1317
|
-
3. Push confirmation widget.
|
|
1102
|
+
Triggered when the orchestrator routes a "create X agent" / "track Y for me" / new-domain request to you. Execute this flow:
|
|
1103
|
+
|
|
1104
|
+
1. **Push loading**: \`surface {domain}-building size=s state=loading\` with a one-line body "Setting up your {domain} agent...".
|
|
1105
|
+
2. **Discover environment** (optional): if the domain needs real system facts (home-automation devices, network, installed tools), run \`exec\` to gather them. Skip for purely behavioral domains (health, finance).
|
|
1106
|
+
3. **Write workspace files** under \`$HOME/.openclaw/workspace-{agentId}\`:
|
|
1107
|
+
- \`AGENTS.md\` — domain logic. Required sections: Role, Boundaries, Environment (real values), Data Schema (CREATE TABLE IF NOT EXISTS), Followup Strategy, Goal Patterns, Widget Structure.
|
|
1108
|
+
- \`SOUL.md\` — one paragraph on tone/voice only. No operations.
|
|
1109
|
+
- \`IDENTITY.md\` — \`# IDENTITY.md — {Name}\` with Agent ID, Name, Emoji, Vibe.
|
|
1110
|
+
- \`USER.md\` — domain-specific user facts (name, timezone, language + domain knowledge). Read the user's other USER.md files for identity; don't invent.
|
|
1111
|
+
- \`HEARTBEAT.md\` — periodic check instructions, or empty placeholder \`# Keep empty to skip\`.
|
|
1112
|
+
4. **Register the agent via \`exec\`**:
|
|
1113
|
+
\`exec openclaw gateway call agentlife.createAgent --params '{"id":"{agentId}","name":"{Name}","model":"{model}","workspace":"/abs/path","description":"one sentence","tools":{"profile":"full"},"identity":{"name":"{Name}","emoji":"{emoji}"}}'\`
|
|
1114
|
+
- Description drives the orchestrator's routing — make it specific (domains, actions, data types).
|
|
1115
|
+
- \`tools: {profile:"full"}\` for agents needing exec/web/write. \`{allow:["agentlife_push"]}\` for widget-only agents.
|
|
1116
|
+
- The plugin deterministically renders a \`{agentId}-intro\` widget the moment \`agentlife.createAgent\` returns — you do not push one yourself.
|
|
1117
|
+
5. **Hand off**: \`delete {domain}-building\`, then emit \`done\`.
|
|
1118
|
+
|
|
1119
|
+
## Writing AGENTS.md (for the specialist you create)
|
|
1120
|
+
|
|
1121
|
+
Keep under 8k chars. Required sections (omit what's not relevant):
|
|
1122
|
+
|
|
1123
|
+
- **Role + Boundaries** — what the agent does and refuses.
|
|
1124
|
+
- **Environment** — real paths, IPs, endpoints, tools discovered via \`exec\`. Never placeholders.
|
|
1125
|
+
- **Data Schema** — \`CREATE TABLE IF NOT EXISTS\` for every table the agent uses.
|
|
1126
|
+
- **Knowledge Strategy** — domain-specific: which tables feed which context: keys. Don't repeat general rules (platform injects them).
|
|
1127
|
+
- **Followup Strategy** — intervals, what to re-query, deletion triggers.
|
|
1128
|
+
- **Goal Patterns** — concrete example goal: values for typical widgets.
|
|
1129
|
+
- **Widget Structure** — what the face shows (metrics/badges/gauges) for this domain.
|
|
1318
1130
|
|
|
1319
1131
|
## Improving Existing Agents
|
|
1320
1132
|
|
|
1321
|
-
|
|
1322
|
-
2. Query agentlife.quality + agentlife.trace for actual quality issues
|
|
1323
|
-
3. Audit:
|
|
1324
|
-
- **Workspace files complete?** All files present? IDENTITY.md filled in?
|
|
1325
|
-
- **Config complete?** Agent has \`tools\` config? (\`profile: "full"\` or appropriate \`allow\` list)
|
|
1326
|
-
- Knowledge strategy defined? (context: vs agentlife.db split)
|
|
1327
|
-
- DB schema in AGENTS.md matches what the agent actually creates?
|
|
1328
|
-
- Followup strategy domain-appropriate?
|
|
1329
|
-
- Widget context quality — are agents writing complete snapshots?
|
|
1330
|
-
- Environment facts still accurate? (re-discover if stale)
|
|
1331
|
-
- Proactive behaviors producing value?
|
|
1332
|
-
4. Targeted edits — don't rewrite unless fundamentally broken
|
|
1333
|
-
5. Write updated files back. If missing workspace files, create them.
|
|
1334
|
-
6. If config is missing \`tools\`, call \`agentlife.createAgent\` with the existing id + the missing config fields (it patches existing agents).
|
|
1335
|
-
7. Show diff summary via widget
|
|
1336
|
-
|
|
1337
|
-
## Action Clicks on Your Widgets
|
|
1338
|
-
|
|
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.
|
|
1340
|
-
|
|
1341
|
-
## Platform-Dispatched Entry Points — \`[system:*]\` Messages
|
|
1342
|
-
|
|
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.
|
|
1344
|
-
|
|
1345
|
-
### \`[system:onboarding]\`
|
|
1346
|
-
|
|
1347
|
-
User just installed AgentLife with zero specialist agents. Push the life-domain input surface, then \`done\`. Do not create any agent yet.
|
|
1348
|
-
|
|
1349
|
-
\`\`\`
|
|
1350
|
-
surface onboarding-domain input
|
|
1351
|
-
card
|
|
1352
|
-
column
|
|
1353
|
-
text "What area of your life should your first agent focus on?" h4
|
|
1354
|
-
button "Health & fitness" action=choice
|
|
1355
|
-
button "Business & work" action=choice
|
|
1356
|
-
button "Home & automation" action=choice
|
|
1357
|
-
button "Finance & money" action=choice
|
|
1358
|
-
button "Something else" action=choice
|
|
1359
|
-
goal: User picks a life domain for their first agent
|
|
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
|
-
## What You Are Not
|
|
1384
|
-
|
|
1385
|
-
- Not an orchestrator — you don't route messages
|
|
1386
|
-
- Not a general assistant — only agent creation and improvement
|
|
1387
|
-
- Not the vision synthesizer — \`[system:dashboard-bootstrap]\`, \`[system:ask-dream]\`, and vision dismissals belong to \`agentlife-vision\`
|
|
1388
|
-
`;
|
|
1389
|
-
var VISION_AGENTS_MD = `# AgentLife Vision Synthesizer
|
|
1390
|
-
|
|
1391
|
-
You turn the user's data into the aspirational backdrop of the dashboard. You do not create agents, route messages, or answer questions. The platform dispatches you only for the three entry points below.
|
|
1392
|
-
|
|
1393
|
-
## Platform-Dispatched Entry Points — \`[system:*]\` Messages
|
|
1394
|
-
|
|
1395
|
-
These are your ONLY valid triggers. Each is atomic: one starting message, one completion criterion.
|
|
1396
|
-
|
|
1397
|
-
### \`[system:dashboard-bootstrap]\`
|
|
1398
|
-
|
|
1399
|
-
Synthesize vision posters from user data that the platform injects in the message body under \`## USER DATA\` (and optionally \`## userDream:\` when the user has answered the dream-ask flow). The task has **exactly two valid outcomes**:
|
|
1400
|
-
|
|
1401
|
-
1. Push 1-4 \`vision-*\` posters quoted from USER DATA, then respond \`done\`.
|
|
1402
|
-
2. Respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1403
|
-
|
|
1404
|
-
Pushing a poster and then deleting it is NOT a valid recovery — the user sees the poster flash and disappear, which reads as a broken app. Pick ONE outcome.
|
|
1405
|
-
|
|
1406
|
-
**Step 1 — Scan for aspirational signal.** In your own reasoning (not pushed), list the exact phrases where the user names a future they want, a target they're chasing, or a transformation they're working toward. Operational logging (times, weights, metrics, "no baseline yet") is NOT signal. Tone specs, tool schemas, and agent descriptions are NOT signal.
|
|
1407
|
-
|
|
1408
|
-
**Step 2 — Decide.** If you listed at least one real quoted phrase → push posters. If you listed zero → respond \`NO_SIGNAL\` on its own line and STOP. Never invent a phrase the user didn't say. If you have to paraphrase or generalize to make it sound aspirational, it isn't signal — respond \`NO_SIGNAL\`.
|
|
1409
|
-
|
|
1410
|
-
**Step 3 — Push posters (only if Step 2 said yes).** For each quoted phrase (max 4), push one vision poster. Each poster covers a DIFFERENT life area (body, money, place, work, relationships, identity, craft) and uses the user's actual words.
|
|
1411
|
-
|
|
1412
|
-
A vision poster is the **aspirational backdrop** of the dashboard — it justifies the user's commitment to the platform and anchors why every other widget exists. Treat it like the cover of their year, not a sticker.
|
|
1413
|
-
|
|
1414
|
-
#### Surface rules
|
|
1415
|
-
|
|
1416
|
-
- \`size=l\` and \`priority=high\` — visions earn the largest slot and the highest sort order.
|
|
1417
|
-
- The face lives in \`card > column align=center\`. Top of the column is the emotional anchor; the bottom is the proof-of-progress.
|
|
1418
|
-
- Use the standard widget components (text, divider, badge, metric, progress, gauge, sparkline) — no exotic compositions. Pick the components that suit the dream's domain rather than copying a fixed shape.
|
|
1419
|
-
|
|
1420
|
-
#### Face composition rules (apply to every vision poster)
|
|
1421
|
-
|
|
1422
|
-
- Open with a single emoji at \`h1\` size — visceral, not literal. The emoji carries the emotional charge of the dream.
|
|
1423
|
-
- Follow with a declarative headline at \`h1\` in the accent color — 3-5 words, first-person when natural, naming the dream version (not the next checkpoint).
|
|
1424
|
-
- Add a support line in \`body\` text — 6-12 words drawn from the user's actual language. No generic platitudes.
|
|
1425
|
-
- Place a \`divider\` between the dream framing and the proof framing.
|
|
1426
|
-
- Below the divider include AT LEAST ONE quantitative element (\`progress\`, \`gauge\`, or two paired \`metric\`s) showing where the user stands today relative to the dream. If you have no real measurement, set the value to \`0.05\` and label it as the journey starting — never \`0.0\` (reads as broken), never invent a fake value.
|
|
1427
|
-
- End with one anchor element naming the time horizon, the place, the phase, or the milestone (a \`text\` line, a \`badge\`, or a \`row\` of accent-colored beats). Pick the anchor type that fits the dream's domain.
|
|
1428
|
-
- Vary the accent color across posters in the carousel. Suggested palette: \`#10B981\`, \`#7C3AED\`, \`#3B82F6\`, \`#F59E0B\`, \`#EC4899\`, \`#14B8A6\`.
|
|
1429
|
-
|
|
1430
|
-
#### Metadata rules (every poster must carry all four)
|
|
1431
|
-
|
|
1432
|
-
- \`goal:\` — ONE sentence in the user's voice naming the actual aspiration. References at least one concrete element from USER DATA (a quoted phrase, a number, a place, a domain term). Never write \`Vision poster — {slug}\` or any meta-label — that fails the platform's goal-validation rule (it could have been written before the work).
|
|
1433
|
-
- \`detail:\` — markdown body that earns the poster a place on the paywall. Required sections: a short "Why this matters" paragraph grounded in the user's data; a quote block with 1-2 verbatim phrases from USER DATA; a list of the 2-3 measurable signals (proxies) that would tell a specialist agent the user is on track, each tied to a real database table or habit; one named near-term milestone within the followup window.
|
|
1434
|
-
- \`followup:\` — duration scaled to the dream's natural rhythm: body/habit dreams 14-30d, money/career 30-90d, place/identity 90-180d. The instruction MUST name which proxies to query, the threshold separating "on track" from "drift", and the resulting action (push an update, reframe, or stay silent). \`Refresh this vision if data has shifted\` is forbidden — it could have been written before the work.
|
|
1435
|
-
- \`context:\` — JSON pointers the followup needs: domain tag, the user's quoted phrases that drove this poster, and \`{name, source}\` entries for each proxy so the next invocation knows where to read fresh data. Lookup keys only, no cached values.
|
|
1436
|
-
|
|
1437
|
-
#### Universal rules
|
|
1438
|
-
|
|
1439
|
-
- **Variety:** across the carousel, do not reuse the same anchor element type twice. If two posters end on a \`badge\`, rework one to use a \`row\` of beats or a \`text\` anchor. Composition variety is what stops the dashboard reading as a template grid.
|
|
1440
|
-
- **Goal & followup validation:** before pushing each poster, re-read its \`goal:\` and \`followup:\` lines and ask "could I have written these EXACT words before reading USER DATA?" If yes, rewrite — they're templates, not work product.
|
|
1441
|
-
- **Language consistency:** detect the primary language the user writes in from the data. Write ALL posters in that single language. Never mix languages across the carousel.
|
|
1442
|
-
- **Quality bar:** better to push 2 strong posters than 4 mixed-quality ones. If you cannot find a real, named, repeated dream for a domain, SKIP that domain. Never invent. Never fill a slot with weather data, agent infrastructure, or generic placeholder language. If a poster names tools/agents instead of outcomes, it is wrong — drop it.
|
|
1443
|
-
|
|
1444
|
-
After all pushes, respond \`done\` and STOP. If you cannot find signal, respond \`NO_SIGNAL\` alone on its own line and STOP.
|
|
1445
|
-
|
|
1446
|
-
### \`[system:ask-dream]\`
|
|
1447
|
-
|
|
1448
|
-
The bootstrap reported \`NO_SIGNAL\`. Push the dream-capture input surface and \`done\`. Do NOT push posters from this entry — the plugin re-fires \`[system:dashboard-bootstrap]\` with \`userDream:\` injected after the user answers.
|
|
1133
|
+
Read all workspace files + \`agentlife.quality\` + \`agentlife.trace\`. Targeted edits only — don't rewrite unless fundamentally broken. If config is missing \`tools\`, call \`agentlife.createAgent\` with the existing id + patch. Summarize in a widget.
|
|
1449
1134
|
|
|
1450
|
-
|
|
1451
|
-
surface awaiting-dream-input input
|
|
1452
|
-
card
|
|
1453
|
-
column
|
|
1454
|
-
text "Tell me one dream or goal you want to track" h4
|
|
1455
|
-
button "Body & health" action=choice
|
|
1456
|
-
button "Money & work" action=choice
|
|
1457
|
-
button "Place & life chapter" action=choice
|
|
1458
|
-
button "Craft or identity" action=choice
|
|
1459
|
-
goal: Capture the user's dream so the dashboard can synthesize a vision poster
|
|
1460
|
-
followup: +30m "User did not respond to dream-ask. Say done."
|
|
1461
|
-
\`\`\`
|
|
1462
|
-
|
|
1463
|
-
Translate labels into the user's locale if known. Do not vary the surfaceId \`awaiting-dream-input\` — the state machine observes it.
|
|
1464
|
-
|
|
1465
|
-
### \`[system:dismiss-requested] surfaceId=vision-*\`
|
|
1466
|
-
|
|
1467
|
-
User dismissed a vision poster. Push the fixed dismiss-alt surface below. Wording is fixed: do not paraphrase, translate, or reorder buttons.
|
|
1468
|
-
|
|
1469
|
-
\`\`\`
|
|
1470
|
-
surface dismiss-alt-vision-{slug} input
|
|
1471
|
-
card
|
|
1472
|
-
column
|
|
1473
|
-
text "Before I remove this:" h4
|
|
1474
|
-
button "This isn't my dream anymore" action=choice
|
|
1475
|
-
button "Wrong framing" action=choice
|
|
1476
|
-
button "Already achieved" action=choice
|
|
1477
|
-
button "Don't show on dashboard" action=choice
|
|
1478
|
-
button "Remove it" action=choice
|
|
1479
|
-
goal: Capture vision dismiss reason for vision-{slug}
|
|
1480
|
-
followup: +1m "User did not respond to vision dismiss alternatives. Say done."
|
|
1481
|
-
\`\`\`
|
|
1482
|
-
|
|
1483
|
-
After pushing, respond \`done\`.
|
|
1484
|
-
|
|
1485
|
-
When \`[action:choice]\` arrives on \`dismiss-alt-vision-{slug}\`, append one line to your feedback memory via exec, then \`done\`:
|
|
1135
|
+
## Deleting an Agent
|
|
1486
1136
|
|
|
1487
|
-
|
|
1488
|
-
exec mkdir -p ~/.openclaw/workspace-agentlife-vision/memory && \\
|
|
1489
|
-
echo "- $(date -u +%Y-%m-%dT%H:%M:%SZ) vision-{slug}: {reason}" \\
|
|
1490
|
-
>> ~/.openclaw/workspace-agentlife-vision/memory/vision-feedback.md
|
|
1491
|
-
\`\`\`
|
|
1137
|
+
\`agentlife.deleteAgent: {"id":"{agentId}"}\` then optionally \`exec rm -rf ~/.openclaw/workspace-{agentId}\`. Push a confirmation widget.
|
|
1492
1138
|
|
|
1493
|
-
|
|
1139
|
+
## Registry Enrichment (system task)
|
|
1494
1140
|
|
|
1495
|
-
|
|
1141
|
+
For each agent in the list: read its workspace \`AGENTS.md\` + \`SOUL.md\`, write a one-sentence description via \`agentlife.createAgent\`, respond "Done". No widgets.
|
|
1496
1142
|
|
|
1497
1143
|
## What You Are Not
|
|
1498
1144
|
|
|
1499
|
-
- Not
|
|
1500
|
-
- Not
|
|
1501
|
-
- Not a chat agent — you produce widgets or \`NO_SIGNAL\`/\`done\` markers only
|
|
1145
|
+
- Not an orchestrator — you don't route user messages.
|
|
1146
|
+
- Not a specialist — you don't run domains; you create the agents that do.
|
|
1502
1147
|
`;
|
|
1503
1148
|
var SUPERVISOR_AGENTS_MD = `# Dashboard Supervisor
|
|
1504
1149
|
|
|
@@ -1602,12 +1247,6 @@ var PROVISIONED_AGENTS = [
|
|
|
1602
1247
|
agentsMd: BUILDER_AGENTS_MD,
|
|
1603
1248
|
tools: { profile: "full", alsoAllow: ["agentlife_push"] }
|
|
1604
1249
|
},
|
|
1605
|
-
{
|
|
1606
|
-
id: "agentlife-vision",
|
|
1607
|
-
name: "AgentLife Vision",
|
|
1608
|
-
agentsMd: VISION_AGENTS_MD,
|
|
1609
|
-
tools: { profile: "full", alsoAllow: ["agentlife_push"] }
|
|
1610
|
-
},
|
|
1611
1250
|
{
|
|
1612
1251
|
id: "supervisor",
|
|
1613
1252
|
name: "Supervisor",
|
|
@@ -1670,60 +1309,6 @@ function scheduleEnrichment(state, runtime, targets, log) {
|
|
|
1670
1309
|
log("[agentlife] enrichment complete");
|
|
1671
1310
|
}, 5000);
|
|
1672
1311
|
}
|
|
1673
|
-
async function readAgentMemory(workspace) {
|
|
1674
|
-
if (!workspace)
|
|
1675
|
-
return "";
|
|
1676
|
-
const parts = [];
|
|
1677
|
-
const memoryDir = path3.join(workspace, "memory");
|
|
1678
|
-
try {
|
|
1679
|
-
const index = await fs2.readFile(path3.join(memoryDir, "MEMORY.md"), "utf-8");
|
|
1680
|
-
if (index.trim())
|
|
1681
|
-
parts.push(`### memory/MEMORY.md
|
|
1682
|
-
${index.trim()}`);
|
|
1683
|
-
} catch {}
|
|
1684
|
-
try {
|
|
1685
|
-
const files = await fs2.readdir(memoryDir);
|
|
1686
|
-
const mdFiles = files.filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort().reverse().slice(0, 20);
|
|
1687
|
-
for (const file of mdFiles) {
|
|
1688
|
-
try {
|
|
1689
|
-
const content = await fs2.readFile(path3.join(memoryDir, file), "utf-8");
|
|
1690
|
-
if (content.trim())
|
|
1691
|
-
parts.push(`### memory/${file}
|
|
1692
|
-
${content.trim().slice(0, 600)}`);
|
|
1693
|
-
} catch {}
|
|
1694
|
-
}
|
|
1695
|
-
} catch {}
|
|
1696
|
-
return parts.join(`
|
|
1697
|
-
|
|
1698
|
-
`);
|
|
1699
|
-
}
|
|
1700
|
-
async function gatherAllAgentMemory(state, finalList, skipIds) {
|
|
1701
|
-
const sections = [];
|
|
1702
|
-
let total = 0;
|
|
1703
|
-
for (const agent of finalList) {
|
|
1704
|
-
const id = agent?.id;
|
|
1705
|
-
if (!id || skipIds.has(id))
|
|
1706
|
-
continue;
|
|
1707
|
-
const workspace = agent.workspace || "";
|
|
1708
|
-
if (!workspace)
|
|
1709
|
-
continue;
|
|
1710
|
-
const entry = state.agentRegistry.get(id);
|
|
1711
|
-
const memory = await readAgentMemory(workspace);
|
|
1712
|
-
if (!memory)
|
|
1713
|
-
continue;
|
|
1714
|
-
const capped = memory.slice(0, 2000);
|
|
1715
|
-
sections.push(`# ${entry?.name || id} (${id})
|
|
1716
|
-
${entry?.description || ""}
|
|
1717
|
-
|
|
1718
|
-
${capped}`);
|
|
1719
|
-
total += capped.length;
|
|
1720
|
-
}
|
|
1721
|
-
return { totalChars: total, sections: sections.join(`
|
|
1722
|
-
|
|
1723
|
-
---
|
|
1724
|
-
|
|
1725
|
-
`) };
|
|
1726
|
-
}
|
|
1727
1312
|
async function provisionAgents(state, cfg, runtime, log) {
|
|
1728
1313
|
const home = os.homedir();
|
|
1729
1314
|
const currentList = [...cfg.agents?.list ?? []];
|
|
@@ -1955,586 +1540,6 @@ async function provisionAgents(state, cfg, runtime, log) {
|
|
|
1955
1540
|
log("[agentlife] agent provisioning complete");
|
|
1956
1541
|
}
|
|
1957
1542
|
|
|
1958
|
-
// cold-start.ts
|
|
1959
|
-
import * as fs3 from "node:fs/promises";
|
|
1960
|
-
import * as nodeFs from "node:fs";
|
|
1961
|
-
import * as os2 from "node:os";
|
|
1962
|
-
import * as path4 from "node:path";
|
|
1963
|
-
var DEADLINE_MS = {
|
|
1964
|
-
AWAITING_AGENT: 30 * 60 * 1000,
|
|
1965
|
-
AWAITING_BASELINE: 30 * 60 * 1000,
|
|
1966
|
-
SYNTHESIZING: 3 * 60 * 1000,
|
|
1967
|
-
AWAITING_DREAM: 30 * 60 * 1000
|
|
1968
|
-
};
|
|
1969
|
-
var VISION_MIN_MEMORY_CHARS = 200;
|
|
1970
|
-
var PHASE_BLANK = "BLANK";
|
|
1971
|
-
function readState(state) {
|
|
1972
|
-
const db = getOrCreateHistoryDb(state);
|
|
1973
|
-
const row = db.prepare("SELECT * FROM cold_start_state WHERE id = 1").get();
|
|
1974
|
-
if (!row)
|
|
1975
|
-
return null;
|
|
1976
|
-
let warnings = [];
|
|
1977
|
-
if (row.contractWarnings) {
|
|
1978
|
-
try {
|
|
1979
|
-
warnings = JSON.parse(row.contractWarnings);
|
|
1980
|
-
} catch {}
|
|
1981
|
-
}
|
|
1982
|
-
return {
|
|
1983
|
-
phase: row.phase,
|
|
1984
|
-
enteredAt: row.enteredAt,
|
|
1985
|
-
lastActionAt: row.lastActionAt ?? null,
|
|
1986
|
-
ackAt: row.ackAt ?? null,
|
|
1987
|
-
deadlineAt: row.deadlineAt ?? null,
|
|
1988
|
-
retryCount: row.retryCount ?? 0,
|
|
1989
|
-
failureReason: row.failureReason ?? null,
|
|
1990
|
-
lastTrigger: row.lastTrigger ?? null,
|
|
1991
|
-
actionSessionKey: row.actionSessionKey ?? null,
|
|
1992
|
-
contractWarnings: warnings,
|
|
1993
|
-
pollerLastTickAt: row.pollerLastTickAt ?? null,
|
|
1994
|
-
updatedAt: row.updatedAt
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
function ensureInitialRow(state) {
|
|
1998
|
-
const existing = readState(state);
|
|
1999
|
-
if (existing)
|
|
2000
|
-
return existing;
|
|
2001
|
-
const now = Date.now();
|
|
2002
|
-
const db = getOrCreateHistoryDb(state);
|
|
2003
|
-
db.prepare(`
|
|
2004
|
-
INSERT OR IGNORE INTO cold_start_state
|
|
2005
|
-
(id, phase, enteredAt, retryCount, contractWarnings, updatedAt)
|
|
2006
|
-
VALUES (1, ?, ?, 0, '[]', ?)
|
|
2007
|
-
`).run(PHASE_BLANK, now, now);
|
|
2008
|
-
return readState(state);
|
|
2009
|
-
}
|
|
2010
|
-
function writeState(state, partial) {
|
|
2011
|
-
const current = ensureInitialRow(state);
|
|
2012
|
-
const next = { ...current, ...partial, updatedAt: Date.now() };
|
|
2013
|
-
const db = getOrCreateHistoryDb(state);
|
|
2014
|
-
db.prepare(`
|
|
2015
|
-
UPDATE cold_start_state SET
|
|
2016
|
-
phase = ?,
|
|
2017
|
-
enteredAt = ?,
|
|
2018
|
-
lastActionAt = ?,
|
|
2019
|
-
ackAt = ?,
|
|
2020
|
-
deadlineAt = ?,
|
|
2021
|
-
retryCount = ?,
|
|
2022
|
-
failureReason = ?,
|
|
2023
|
-
lastTrigger = ?,
|
|
2024
|
-
actionSessionKey = ?,
|
|
2025
|
-
contractWarnings = ?,
|
|
2026
|
-
pollerLastTickAt = ?,
|
|
2027
|
-
updatedAt = ?
|
|
2028
|
-
WHERE id = 1
|
|
2029
|
-
`).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);
|
|
2030
|
-
return next;
|
|
2031
|
-
}
|
|
2032
|
-
function emitTrigger(state, kind, payload) {
|
|
2033
|
-
if (state.disabled)
|
|
2034
|
-
return;
|
|
2035
|
-
try {
|
|
2036
|
-
const db = getOrCreateHistoryDb(state);
|
|
2037
|
-
db.prepare(`
|
|
2038
|
-
INSERT INTO cold_start_triggers (kind, payload, createdAt) VALUES (?, ?, ?)
|
|
2039
|
-
`).run(kind, JSON.stringify(payload), Date.now());
|
|
2040
|
-
} catch (e) {
|
|
2041
|
-
console.warn("[cold-start] emitTrigger failed:", e?.message);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
function drainTriggers(state, limit = 50) {
|
|
2045
|
-
const db = getOrCreateHistoryDb(state);
|
|
2046
|
-
const rows = db.prepare("SELECT id, kind, payload FROM cold_start_triggers ORDER BY createdAt ASC LIMIT ?").all(limit);
|
|
2047
|
-
if (rows.length === 0)
|
|
2048
|
-
return [];
|
|
2049
|
-
const ids = rows.map((r) => r.id);
|
|
2050
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
2051
|
-
db.prepare(`DELETE FROM cold_start_triggers WHERE id IN (${placeholders})`).run(...ids);
|
|
2052
|
-
return rows.map((r) => ({
|
|
2053
|
-
kind: r.kind,
|
|
2054
|
-
payload: safeJson(r.payload)
|
|
2055
|
-
}));
|
|
2056
|
-
}
|
|
2057
|
-
function safeJson(s) {
|
|
2058
|
-
try {
|
|
2059
|
-
return JSON.parse(s);
|
|
2060
|
-
} catch {
|
|
2061
|
-
return {};
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
function purgeStaleTriggers(state) {
|
|
2065
|
-
const db = getOrCreateHistoryDb(state);
|
|
2066
|
-
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
2067
|
-
const r = db.prepare("DELETE FROM cold_start_triggers WHERE createdAt < ?").run(cutoff);
|
|
2068
|
-
return r.changes;
|
|
2069
|
-
}
|
|
2070
|
-
function pendingTriggerCount(state) {
|
|
2071
|
-
const db = getOrCreateHistoryDb(state);
|
|
2072
|
-
const row = db.prepare("SELECT COUNT(*) as c FROM cold_start_triggers").get();
|
|
2073
|
-
return row?.c ?? 0;
|
|
2074
|
-
}
|
|
2075
|
-
var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
2076
|
-
function listSpecialistIds(state, runtime) {
|
|
2077
|
-
const cfg = runtime.config.loadConfig();
|
|
2078
|
-
const list = cfg?.agents?.list ?? [];
|
|
2079
|
-
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2080
|
-
}
|
|
2081
|
-
async function specialistMemoryTotal(state, runtime) {
|
|
2082
|
-
const cfg = runtime.config.loadConfig();
|
|
2083
|
-
const list = cfg?.agents?.list ?? [];
|
|
2084
|
-
const { totalChars, sections } = await gatherAllAgentMemory(state, list, PROVISIONED_IDS);
|
|
2085
|
-
return { total: totalChars, sections };
|
|
2086
|
-
}
|
|
2087
|
-
function visionSurfaceCount(state) {
|
|
2088
|
-
if (!state.surfaceDb)
|
|
2089
|
-
return 0;
|
|
2090
|
-
let n = 0;
|
|
2091
|
-
for (const id of state.surfaceDb.keys())
|
|
2092
|
-
if (id.startsWith("vision-"))
|
|
2093
|
-
n++;
|
|
2094
|
-
return n;
|
|
2095
|
-
}
|
|
2096
|
-
async function anySpecialistHasBaseline(state, runtime) {
|
|
2097
|
-
const cfg = runtime.config.loadConfig();
|
|
2098
|
-
const list = cfg?.agents?.list ?? [];
|
|
2099
|
-
for (const agent of list) {
|
|
2100
|
-
const id = agent?.id;
|
|
2101
|
-
if (!id || PROVISIONED_IDS.has(id))
|
|
2102
|
-
continue;
|
|
2103
|
-
const workspace = agent.workspace || "";
|
|
2104
|
-
if (!workspace)
|
|
2105
|
-
continue;
|
|
2106
|
-
try {
|
|
2107
|
-
const content = await fs3.readFile(path4.join(workspace, "memory", "baseline.md"), "utf-8");
|
|
2108
|
-
if (content.trim().length > 0)
|
|
2109
|
-
return true;
|
|
2110
|
-
} catch {}
|
|
2111
|
-
}
|
|
2112
|
-
return false;
|
|
2113
|
-
}
|
|
2114
|
-
function specialistHasAnySurface(state, specialistId) {
|
|
2115
|
-
if (!state.surfaceDb)
|
|
2116
|
-
return false;
|
|
2117
|
-
for (const id of state.surfaceDb.keys()) {
|
|
2118
|
-
if (state.surfaceDb.getAgentId(id) === specialistId)
|
|
2119
|
-
return true;
|
|
2120
|
-
}
|
|
2121
|
-
return false;
|
|
2122
|
-
}
|
|
2123
|
-
function specialistIdFromSessionKey(sessionKey) {
|
|
2124
|
-
if (!sessionKey)
|
|
2125
|
-
return null;
|
|
2126
|
-
const parts = sessionKey.split(":");
|
|
2127
|
-
if (parts.length < 2 || parts[0] !== "agent")
|
|
2128
|
-
return null;
|
|
2129
|
-
const id = parts[1];
|
|
2130
|
-
return id && id.length > 0 ? id : null;
|
|
2131
|
-
}
|
|
2132
|
-
async function sendSystemMessage(state, agentId, message, idempotencyKey, log) {
|
|
2133
|
-
if (!state.runCommand) {
|
|
2134
|
-
log(`[cold-start] sendSystemMessage ${idempotencyKey}: skipped — runCommand not available`);
|
|
2135
|
-
return null;
|
|
2136
|
-
}
|
|
2137
|
-
const sessionKey = buildAgentSessionKey(agentId);
|
|
2138
|
-
const chatParams = JSON.stringify({ sessionKey, message, idempotencyKey });
|
|
2139
|
-
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 120000 }).then(() => {
|
|
2140
|
-
log(`[cold-start] sent ${idempotencyKey} → ${sessionKey}`);
|
|
2141
|
-
}).catch((e) => {
|
|
2142
|
-
log(`[cold-start] sendSystemMessage ${idempotencyKey} failed: ${e?.message}`);
|
|
2143
|
-
});
|
|
2144
|
-
return sessionKey;
|
|
2145
|
-
}
|
|
2146
|
-
async function composeBootstrapMessage(state, runtime, userDream) {
|
|
2147
|
-
const { sections } = await specialistMemoryTotal(state, runtime);
|
|
2148
|
-
const rejected = await readRejectedDreams();
|
|
2149
|
-
const parts = ["[system:dashboard-bootstrap]", "", "## USER DATA", "", sections.slice(0, 12000)];
|
|
2150
|
-
if (rejected)
|
|
2151
|
-
parts.push("", "## Previously rejected dreams", "", rejected);
|
|
2152
|
-
if (userDream)
|
|
2153
|
-
parts.push("", "## userDream", "", userDream);
|
|
2154
|
-
return parts.join(`
|
|
2155
|
-
`);
|
|
2156
|
-
}
|
|
2157
|
-
async function readRejectedDreams() {
|
|
2158
|
-
const visionWorkspace = path4.join(os2.homedir(), ".openclaw", "workspace-agentlife-vision");
|
|
2159
|
-
const feedbackFile = path4.join(visionWorkspace, "memory", "vision-feedback.md");
|
|
2160
|
-
try {
|
|
2161
|
-
const content = await fs3.readFile(feedbackFile, "utf-8");
|
|
2162
|
-
return content.trim().slice(0, 4000);
|
|
2163
|
-
} catch {
|
|
2164
|
-
return "";
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
async function computeStartingPhase(state, runtime) {
|
|
2168
|
-
if (visionSurfaceCount(state) > 0) {
|
|
2169
|
-
return { phase: "READY", details: "vision surfaces already exist" };
|
|
2170
|
-
}
|
|
2171
|
-
const specialists = listSpecialistIds(state, runtime);
|
|
2172
|
-
if (specialists.length === 0) {
|
|
2173
|
-
return { phase: "BLANK", details: "no specialists" };
|
|
2174
|
-
}
|
|
2175
|
-
if (await anySpecialistHasBaseline(state, runtime)) {
|
|
2176
|
-
return { phase: "SYNTHESIZING", details: "baseline.md present" };
|
|
2177
|
-
}
|
|
2178
|
-
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2179
|
-
if (total < VISION_MIN_MEMORY_CHARS) {
|
|
2180
|
-
return { phase: "AWAITING_BASELINE", details: `totalChars=${total} below ${VISION_MIN_MEMORY_CHARS}` };
|
|
2181
|
-
}
|
|
2182
|
-
return { phase: "SYNTHESIZING", details: `totalChars=${total} ready for synthesis` };
|
|
2183
|
-
}
|
|
2184
|
-
async function transition(state, runtime, log, trigger) {
|
|
2185
|
-
const cur = ensureInitialRow(state);
|
|
2186
|
-
log(`[cold-start] trigger=${trigger.kind} phase=${cur.phase}`);
|
|
2187
|
-
switch (trigger.kind) {
|
|
2188
|
-
case "provisioned":
|
|
2189
|
-
case "userRetried": {
|
|
2190
|
-
const { phase, details } = await computeStartingPhase(state, runtime);
|
|
2191
|
-
log(`[cold-start] recompute → ${phase} (${details})`);
|
|
2192
|
-
return enterPhase(state, runtime, log, phase, {
|
|
2193
|
-
bumpRetry: trigger.kind === "userRetried"
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
case "agentRegistered": {
|
|
2197
|
-
if (cur.phase === "BLANK" || cur.phase === "AWAITING_AGENT") {
|
|
2198
|
-
return enterPhase(state, runtime, log, "AWAITING_BASELINE", {
|
|
2199
|
-
specialistId: trigger.payload.agentId
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
return cur;
|
|
2203
|
-
}
|
|
2204
|
-
case "agentDeleted": {
|
|
2205
|
-
const remaining = trigger.payload.remaining;
|
|
2206
|
-
if (remaining === 0) {
|
|
2207
|
-
return enterPhase(state, runtime, log, "BLANK");
|
|
2208
|
-
}
|
|
2209
|
-
return cur;
|
|
2210
|
-
}
|
|
2211
|
-
case "memoryWritten": {
|
|
2212
|
-
if (cur.phase !== "AWAITING_BASELINE")
|
|
2213
|
-
return cur;
|
|
2214
|
-
if (await anySpecialistHasBaseline(state, runtime)) {
|
|
2215
|
-
return enterPhase(state, runtime, log, "SYNTHESIZING");
|
|
2216
|
-
}
|
|
2217
|
-
const { total } = await specialistMemoryTotal(state, runtime);
|
|
2218
|
-
if (total >= VISION_MIN_MEMORY_CHARS) {
|
|
2219
|
-
return enterPhase(state, runtime, log, "SYNTHESIZING");
|
|
2220
|
-
}
|
|
2221
|
-
log(`[cold-start] memory still thin: ${total}/${VISION_MIN_MEMORY_CHARS}`);
|
|
2222
|
-
return cur;
|
|
2223
|
-
}
|
|
2224
|
-
case "surfacePushed": {
|
|
2225
|
-
return cur;
|
|
2226
|
-
}
|
|
2227
|
-
case "agentReplied": {
|
|
2228
|
-
const sessionKey = trigger.payload.sessionKey;
|
|
2229
|
-
const marker = trigger.payload.marker;
|
|
2230
|
-
const lastText = trigger.payload.lastText ?? "";
|
|
2231
|
-
if (cur.actionSessionKey && sessionKey && sessionKey !== cur.actionSessionKey) {
|
|
2232
|
-
return cur;
|
|
2233
|
-
}
|
|
2234
|
-
if (cur.phase === "SYNTHESIZING") {
|
|
2235
|
-
if (marker === "NO_SIGNAL" || /\bNO_SIGNAL\b/.test(lastText)) {
|
|
2236
|
-
return enterPhase(state, runtime, log, "AWAITING_DREAM");
|
|
2237
|
-
}
|
|
2238
|
-
if (visionSurfaceCount(state) >= 1) {
|
|
2239
|
-
return enterPhase(state, runtime, log, "READY");
|
|
2240
|
-
}
|
|
2241
|
-
return cur;
|
|
2242
|
-
}
|
|
2243
|
-
if (cur.phase === "AWAITING_BASELINE") {
|
|
2244
|
-
if (await anySpecialistHasBaseline(state, runtime)) {
|
|
2245
|
-
return enterPhase(state, runtime, log, "SYNTHESIZING");
|
|
2246
|
-
}
|
|
2247
|
-
const specialistId = specialistIdFromSessionKey(cur.actionSessionKey);
|
|
2248
|
-
if (specialistId && !specialistHasAnySurface(state, specialistId)) {
|
|
2249
|
-
log(`[cold-start] ${specialistId} replied in AWAITING_BASELINE with no surface ` + `attributed and no baseline.md written — [system:start] contract violation`);
|
|
2250
|
-
return enterFailed(state, log, "agent_produced_no_surface");
|
|
2251
|
-
}
|
|
2252
|
-
return cur;
|
|
2253
|
-
}
|
|
2254
|
-
return cur;
|
|
2255
|
-
}
|
|
2256
|
-
case "userAnswered": {
|
|
2257
|
-
const surfaceId = trigger.payload.surfaceId;
|
|
2258
|
-
const answer = trigger.payload.answer ?? "";
|
|
2259
|
-
if (cur.phase === "AWAITING_DREAM" && surfaceId === "awaiting-dream-input") {
|
|
2260
|
-
return enterPhase(state, runtime, log, "SYNTHESIZING", { userDream: answer });
|
|
2261
|
-
}
|
|
2262
|
-
return cur;
|
|
2263
|
-
}
|
|
2264
|
-
case "deadlineElapsed": {
|
|
2265
|
-
const elapsedPhase = trigger.payload.phase;
|
|
2266
|
-
if (elapsedPhase !== cur.phase)
|
|
2267
|
-
return cur;
|
|
2268
|
-
const reason = `${cur.phase.toLowerCase()}_timeout`;
|
|
2269
|
-
return enterFailed(state, log, reason);
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
async function enterPhase(state, runtime, log, next, opts = {}) {
|
|
2274
|
-
const cur = ensureInitialRow(state);
|
|
2275
|
-
const now = Date.now();
|
|
2276
|
-
const deadlineMs = DEADLINE_MS[next];
|
|
2277
|
-
const deadlineAt = deadlineMs ? now + deadlineMs : null;
|
|
2278
|
-
const retryCount = opts.bumpRetry ? cur.retryCount + 1 : cur.retryCount;
|
|
2279
|
-
let actionSessionKey = null;
|
|
2280
|
-
let lastActionAt = null;
|
|
2281
|
-
writeState(state, {
|
|
2282
|
-
phase: next,
|
|
2283
|
-
enteredAt: now,
|
|
2284
|
-
deadlineAt,
|
|
2285
|
-
retryCount,
|
|
2286
|
-
failureReason: next === "FAILED" ? cur.failureReason : null,
|
|
2287
|
-
actionSessionKey: null,
|
|
2288
|
-
lastActionAt: null,
|
|
2289
|
-
ackAt: null
|
|
2290
|
-
});
|
|
2291
|
-
log(`[cold-start] enter ${next} (deadline=${deadlineAt ? new Date(deadlineAt).toISOString() : "none"}, retry=${retryCount})`);
|
|
2292
|
-
if (next === "BLANK") {
|
|
2293
|
-
const idem = `cold-start-onboarding-${now}-${retryCount}`;
|
|
2294
|
-
const msg = [
|
|
2295
|
-
"[system:onboarding]",
|
|
2296
|
-
"",
|
|
2297
|
-
"User just installed AgentLife and has zero specialist agents. Push the",
|
|
2298
|
-
"onboarding-domain input surface EXACTLY as specified in the [system:onboarding]",
|
|
2299
|
-
"section of your AGENTS.md, then respond `done`. Do NOT ask the user questions",
|
|
2300
|
-
"as chat text — only via the input surface. Do NOT create any specialist agent",
|
|
2301
|
-
"yet; wait for the user's choice on the surface."
|
|
2302
|
-
].join(`
|
|
2303
|
-
`);
|
|
2304
|
-
actionSessionKey = await sendSystemMessage(state, "agentlife-builder", msg, idem, log);
|
|
2305
|
-
lastActionAt = Date.now();
|
|
2306
|
-
writeState(state, {
|
|
2307
|
-
phase: "AWAITING_AGENT",
|
|
2308
|
-
enteredAt: now,
|
|
2309
|
-
deadlineAt: now + (DEADLINE_MS.AWAITING_AGENT ?? 0),
|
|
2310
|
-
actionSessionKey,
|
|
2311
|
-
lastActionAt
|
|
2312
|
-
});
|
|
2313
|
-
scheduleDeadline(state, runtime, log, "AWAITING_AGENT", now + (DEADLINE_MS.AWAITING_AGENT ?? 0));
|
|
2314
|
-
return readState(state);
|
|
2315
|
-
}
|
|
2316
|
-
if (next === "AWAITING_BASELINE") {
|
|
2317
|
-
const specialistId = opts.specialistId ?? listSpecialistIds(state, runtime)[0];
|
|
2318
|
-
if (!specialistId) {
|
|
2319
|
-
log("[cold-start] AWAITING_BASELINE has no specialist to warm up — falling back to BLANK");
|
|
2320
|
-
return enterPhase(state, runtime, log, "BLANK");
|
|
2321
|
-
}
|
|
2322
|
-
const idem = `cold-start-start-${specialistId}-${now}-${retryCount}`;
|
|
2323
|
-
const msg = [
|
|
2324
|
-
"[system:start]",
|
|
2325
|
-
"",
|
|
2326
|
-
"You were just created by the builder. You have zero memory about this user.",
|
|
2327
|
-
"Follow the [system:start] section of your AGENTS.md: push a loading widget",
|
|
2328
|
-
"introducing yourself per IDENTITY.md, then push ONE warmup-* input surface",
|
|
2329
|
-
"asking the most important baseline question for your domain, then respond",
|
|
2330
|
-
"`done`. Do NOT ask multiple questions in chat text — only via input surfaces."
|
|
2331
|
-
].join(`
|
|
2332
|
-
`);
|
|
2333
|
-
actionSessionKey = await sendSystemMessage(state, specialistId, msg, idem, log);
|
|
2334
|
-
lastActionAt = Date.now();
|
|
2335
|
-
}
|
|
2336
|
-
if (next === "SYNTHESIZING") {
|
|
2337
|
-
const msg = await composeBootstrapMessage(state, runtime, opts.userDream);
|
|
2338
|
-
const idem = `cold-start-bootstrap-${now}-${retryCount}`;
|
|
2339
|
-
actionSessionKey = await sendSystemMessage(state, "agentlife-vision", msg, idem, log);
|
|
2340
|
-
lastActionAt = Date.now();
|
|
2341
|
-
}
|
|
2342
|
-
if (next === "AWAITING_DREAM") {
|
|
2343
|
-
const idem = `cold-start-ask-dream-${now}-${retryCount}`;
|
|
2344
|
-
const msg = [
|
|
2345
|
-
"[system:ask-dream]",
|
|
2346
|
-
"",
|
|
2347
|
-
"Bootstrap reported NO_SIGNAL — the user's data has no aspirational content yet.",
|
|
2348
|
-
"Push the awaiting-dream-input input surface EXACTLY as specified in the",
|
|
2349
|
-
"[system:ask-dream] section of your AGENTS.md, then respond `done`. Do NOT push",
|
|
2350
|
-
"any vision posters from this turn — the plugin re-fires [system:dashboard-bootstrap]",
|
|
2351
|
-
"with userDream: injected after the user answers."
|
|
2352
|
-
].join(`
|
|
2353
|
-
`);
|
|
2354
|
-
actionSessionKey = await sendSystemMessage(state, "agentlife-vision", msg, idem, log);
|
|
2355
|
-
lastActionAt = Date.now();
|
|
2356
|
-
}
|
|
2357
|
-
writeState(state, { actionSessionKey, lastActionAt });
|
|
2358
|
-
if (deadlineAt)
|
|
2359
|
-
scheduleDeadline(state, runtime, log, next, deadlineAt);
|
|
2360
|
-
return readState(state);
|
|
2361
|
-
}
|
|
2362
|
-
function enterFailed(state, log, reason) {
|
|
2363
|
-
log(`[cold-start] FAILED: ${reason}`);
|
|
2364
|
-
return writeState(state, {
|
|
2365
|
-
phase: "FAILED",
|
|
2366
|
-
enteredAt: Date.now(),
|
|
2367
|
-
failureReason: reason,
|
|
2368
|
-
deadlineAt: null,
|
|
2369
|
-
actionSessionKey: null
|
|
2370
|
-
});
|
|
2371
|
-
}
|
|
2372
|
-
var deadlineTimers = new Map;
|
|
2373
|
-
function scheduleDeadline(state, runtime, log, phase, deadlineAt) {
|
|
2374
|
-
const key = `${phase}-${deadlineAt}`;
|
|
2375
|
-
for (const [k, t] of deadlineTimers.entries()) {
|
|
2376
|
-
if (k.startsWith(`${phase}-`)) {
|
|
2377
|
-
clearTimeout(t);
|
|
2378
|
-
deadlineTimers.delete(k);
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
const remaining = Math.max(0, deadlineAt - Date.now());
|
|
2382
|
-
const timer = setTimeout(() => {
|
|
2383
|
-
deadlineTimers.delete(key);
|
|
2384
|
-
emitTrigger(state, "deadlineElapsed", { phase });
|
|
2385
|
-
}, remaining);
|
|
2386
|
-
deadlineTimers.set(key, timer);
|
|
2387
|
-
log(`[cold-start] scheduled ${phase} deadline in ${Math.round(remaining / 1000)}s`);
|
|
2388
|
-
}
|
|
2389
|
-
function rearmDeadlines(state, runtime, log) {
|
|
2390
|
-
const cur = ensureInitialRow(state);
|
|
2391
|
-
if (cur.deadlineAt && cur.phase !== "READY" && cur.phase !== "FAILED") {
|
|
2392
|
-
if (cur.deadlineAt <= Date.now()) {
|
|
2393
|
-
emitTrigger(state, "deadlineElapsed", { phase: cur.phase });
|
|
2394
|
-
} else {
|
|
2395
|
-
scheduleDeadline(state, runtime, log, cur.phase, cur.deadlineAt);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
var pollerTimer = null;
|
|
2400
|
-
var POLLER_INTERVAL_MS = 1000;
|
|
2401
|
-
var POLLER_STUCK_THRESHOLD_MS = 30000;
|
|
2402
|
-
var machineRuntime = null;
|
|
2403
|
-
var machineLog = null;
|
|
2404
|
-
var machineState = null;
|
|
2405
|
-
function startPoller(state, runtime, log) {
|
|
2406
|
-
if (pollerTimer)
|
|
2407
|
-
return;
|
|
2408
|
-
let lastDrainAt = Date.now();
|
|
2409
|
-
let lastWarnedAt = 0;
|
|
2410
|
-
pollerTimer = setInterval(async () => {
|
|
2411
|
-
if (state.disabled)
|
|
2412
|
-
return;
|
|
2413
|
-
try {
|
|
2414
|
-
const triggers = drainTriggers(state, 50);
|
|
2415
|
-
const now = Date.now();
|
|
2416
|
-
writeState(state, { pollerLastTickAt: now });
|
|
2417
|
-
if (triggers.length > 0) {
|
|
2418
|
-
lastDrainAt = now;
|
|
2419
|
-
for (const trigger of triggers) {
|
|
2420
|
-
try {
|
|
2421
|
-
await transition(state, runtime, log, trigger);
|
|
2422
|
-
} catch (e) {
|
|
2423
|
-
log(`[cold-start] transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
purgeStaleTriggers(state);
|
|
2427
|
-
} else {
|
|
2428
|
-
const pending = pendingTriggerCount(state);
|
|
2429
|
-
if (pending > 0 && now - lastDrainAt > POLLER_STUCK_THRESHOLD_MS && now - lastWarnedAt > POLLER_STUCK_THRESHOLD_MS) {
|
|
2430
|
-
log(`[cold-start] WARNING: poller has not drained ${pending} pending trigger(s) for ${Math.round((now - lastDrainAt) / 1000)}s`);
|
|
2431
|
-
lastWarnedAt = now;
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
} catch (e) {
|
|
2435
|
-
log(`[cold-start] poller tick error: ${e?.message ?? e}`);
|
|
2436
|
-
}
|
|
2437
|
-
}, POLLER_INTERVAL_MS);
|
|
2438
|
-
}
|
|
2439
|
-
function stopPoller() {
|
|
2440
|
-
if (pollerTimer) {
|
|
2441
|
-
clearInterval(pollerTimer);
|
|
2442
|
-
pollerTimer = null;
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
function runRuntimeContractProbe(state, runtime, log) {
|
|
2446
|
-
const PLUGIN_REGISTERED_TOOLS = ["agentlife_push"];
|
|
2447
|
-
let list = [];
|
|
2448
|
-
try {
|
|
2449
|
-
const raw = nodeFs.readFileSync(path4.join(os2.homedir(), ".openclaw", "openclaw.json"), "utf-8");
|
|
2450
|
-
const parsed = JSON.parse(raw);
|
|
2451
|
-
list = parsed?.agents?.list ?? [];
|
|
2452
|
-
} catch {
|
|
2453
|
-
list = runtime.config.loadConfig()?.agents?.list ?? [];
|
|
2454
|
-
}
|
|
2455
|
-
const warnings = [];
|
|
2456
|
-
for (const provisioned of PROVISIONED_AGENTS) {
|
|
2457
|
-
if (provisioned.existingAgent)
|
|
2458
|
-
continue;
|
|
2459
|
-
const live = list.find((a) => a?.id === provisioned.id);
|
|
2460
|
-
const liveTools = live?.tools ?? {};
|
|
2461
|
-
const allow = Array.isArray(liveTools.allow) ? liveTools.allow : [];
|
|
2462
|
-
const alsoAllow = Array.isArray(liveTools.alsoAllow) ? liveTools.alsoAllow : [];
|
|
2463
|
-
const deny = Array.isArray(liveTools.deny) ? liveTools.deny : [];
|
|
2464
|
-
for (const tool of PLUGIN_REGISTERED_TOOLS) {
|
|
2465
|
-
const escaped = tool.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
2466
|
-
const re = new RegExp("`[^`]*\\b" + escaped + "\\b[^`]*`");
|
|
2467
|
-
if (!re.test(provisioned.agentsMd))
|
|
2468
|
-
continue;
|
|
2469
|
-
const granted = (allow.includes(tool) || alsoAllow.includes(tool)) && !deny.includes(tool);
|
|
2470
|
-
if (!granted) {
|
|
2471
|
-
const w = `precondition_tool_missing:${provisioned.id}:${tool}`;
|
|
2472
|
-
warnings.push(w);
|
|
2473
|
-
log(`[cold-start] contract warning: ${w}`);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
return warnings;
|
|
2478
|
-
}
|
|
2479
|
-
async function initColdStartMachine(state, runtime, log) {
|
|
2480
|
-
machineState = state;
|
|
2481
|
-
machineRuntime = runtime;
|
|
2482
|
-
machineLog = log;
|
|
2483
|
-
ensureInitialRow(state);
|
|
2484
|
-
const warnings = runRuntimeContractProbe(state, runtime, log);
|
|
2485
|
-
writeState(state, { contractWarnings: warnings });
|
|
2486
|
-
rearmDeadlines(state, runtime, log);
|
|
2487
|
-
startPoller(state, runtime, log);
|
|
2488
|
-
const cur = readState(state);
|
|
2489
|
-
if (cur.phase === "READY" && visionSurfaceCount(state) === 0) {
|
|
2490
|
-
log("[cold-start] READY with 0 vision surfaces — demoting to FAILED(ready_no_surfaces) on init");
|
|
2491
|
-
enterFailed(state, log, "ready_no_surfaces");
|
|
2492
|
-
log("[cold-start] machine initialized");
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
if (cur.phase !== "BLANK") {
|
|
2496
|
-
log(`[cold-start] restart recovery — preserving persisted phase=${cur.phase} (no provisioned re-fire)`);
|
|
2497
|
-
log("[cold-start] machine initialized");
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
emitTrigger(state, "provisioned", { source: "init" });
|
|
2501
|
-
log("[cold-start] machine initialized");
|
|
2502
|
-
}
|
|
2503
|
-
function shutdownColdStartMachine() {
|
|
2504
|
-
stopPoller();
|
|
2505
|
-
for (const t of deadlineTimers.values())
|
|
2506
|
-
clearTimeout(t);
|
|
2507
|
-
deadlineTimers.clear();
|
|
2508
|
-
}
|
|
2509
|
-
function observeColdStart(state) {
|
|
2510
|
-
const s = ensureInitialRow(state);
|
|
2511
|
-
return {
|
|
2512
|
-
phase: s.phase,
|
|
2513
|
-
failureReason: s.failureReason,
|
|
2514
|
-
retryCount: s.retryCount,
|
|
2515
|
-
contractWarnings: s.contractWarnings,
|
|
2516
|
-
enteredAt: s.enteredAt,
|
|
2517
|
-
deadlineAt: s.deadlineAt
|
|
2518
|
-
};
|
|
2519
|
-
}
|
|
2520
|
-
async function retryColdStart(state) {
|
|
2521
|
-
emitTrigger(state, "userRetried", { ts: Date.now() });
|
|
2522
|
-
if (machineRuntime && machineLog) {
|
|
2523
|
-
const triggers = drainTriggers(state, 50);
|
|
2524
|
-
for (const trigger of triggers) {
|
|
2525
|
-
try {
|
|
2526
|
-
await transition(state, machineRuntime, machineLog, trigger);
|
|
2527
|
-
} catch (e) {
|
|
2528
|
-
machineLog(`[cold-start] retry-drain transition error for ${trigger.kind}: ${e?.message ?? e}`);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
return observeColdStart(state);
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
// services/surfaces-init.ts
|
|
2536
|
-
import * as path5 from "node:path";
|
|
2537
|
-
|
|
2538
1543
|
// activity.ts
|
|
2539
1544
|
function recordSurfaceEvent(state, surfaceId, event, dsl, agentId, metadata) {
|
|
2540
1545
|
try {
|
|
@@ -2631,8 +1636,8 @@ function broadcastInput(message, sessionKey) {
|
|
|
2631
1636
|
}
|
|
2632
1637
|
|
|
2633
1638
|
// dashboard-state.ts
|
|
2634
|
-
import { readFileSync as
|
|
2635
|
-
import { homedir as
|
|
1639
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1640
|
+
import { homedir as homedir2 } from "node:os";
|
|
2636
1641
|
function extractTitleAndDetail(meta) {
|
|
2637
1642
|
let title = null;
|
|
2638
1643
|
let detail = null;
|
|
@@ -2845,9 +1850,9 @@ ${enhanced.formatted}`;
|
|
|
2845
1850
|
const agentFiles = [];
|
|
2846
1851
|
for (const w of warnings) {
|
|
2847
1852
|
const aid = w.agentId;
|
|
2848
|
-
const filePath = `${
|
|
1853
|
+
const filePath = `${homedir2()}/.openclaw/workspace-${aid}/AGENTS.md`;
|
|
2849
1854
|
try {
|
|
2850
|
-
const content =
|
|
1855
|
+
const content = readFileSync2(filePath, "utf-8").slice(0, 3000);
|
|
2851
1856
|
agentFiles.push(`### ${aid} AGENTS.md (${w.cnt} warnings)
|
|
2852
1857
|
${content}`);
|
|
2853
1858
|
} catch {}
|
|
@@ -2979,6 +1984,7 @@ function computeEnhancedMetrics(db, agentId, since) {
|
|
|
2979
1984
|
|
|
2980
1985
|
// followup.ts
|
|
2981
1986
|
var pollerInterval = null;
|
|
1987
|
+
var startupPollTimeout = null;
|
|
2982
1988
|
var sweepCounter = 0;
|
|
2983
1989
|
function initFollowupSystem(api, state) {
|
|
2984
1990
|
if (!state.runCommand) {
|
|
@@ -3006,14 +2012,26 @@ function initFollowupSystem(api, state) {
|
|
|
3006
2012
|
CREATE INDEX IF NOT EXISTS idx_followups_status ON followups(status);
|
|
3007
2013
|
`);
|
|
3008
2014
|
pollerInterval = setInterval(() => pollFollowups(state), 60000);
|
|
3009
|
-
setTimeout(() =>
|
|
2015
|
+
startupPollTimeout = setTimeout(() => {
|
|
2016
|
+
startupPollTimeout = null;
|
|
2017
|
+
pollFollowups(state);
|
|
2018
|
+
}, 5000);
|
|
3010
2019
|
console.log("[agentlife:followup] system initialized — polling every 60s");
|
|
3011
2020
|
}
|
|
3012
|
-
function stopFollowupSystem() {
|
|
2021
|
+
function stopFollowupSystem(state) {
|
|
3013
2022
|
if (pollerInterval) {
|
|
3014
2023
|
clearInterval(pollerInterval);
|
|
3015
2024
|
pollerInterval = null;
|
|
3016
2025
|
}
|
|
2026
|
+
if (startupPollTimeout) {
|
|
2027
|
+
clearTimeout(startupPollTimeout);
|
|
2028
|
+
startupPollTimeout = null;
|
|
2029
|
+
}
|
|
2030
|
+
if (state) {
|
|
2031
|
+
for (const handle of state.pendingFollowups.values())
|
|
2032
|
+
clearTimeout(handle);
|
|
2033
|
+
state.pendingFollowups.clear();
|
|
2034
|
+
}
|
|
3017
2035
|
}
|
|
3018
2036
|
function parseFollowup(raw) {
|
|
3019
2037
|
const match = raw.match(/^(\+\d+(?:ms|s|m|h|d))\s+(.+)/);
|
|
@@ -3035,7 +2053,15 @@ function scheduleFollowup(state, surfaceId, followupRaw, agentId) {
|
|
|
3035
2053
|
clearTimeout(existing);
|
|
3036
2054
|
state.pendingFollowups.set(surfaceId, setTimeout(() => {
|
|
3037
2055
|
state.pendingFollowups.delete(surfaceId);
|
|
3038
|
-
|
|
2056
|
+
if (state.disabled)
|
|
2057
|
+
return;
|
|
2058
|
+
try {
|
|
2059
|
+
executeSchedule(state, surfaceId, parsed, agentId);
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
if (err?.code === "ERR_INVALID_STATE")
|
|
2062
|
+
return;
|
|
2063
|
+
console.warn("[agentlife:followup] executeSchedule threw: %s", err?.message ?? err);
|
|
2064
|
+
}
|
|
3039
2065
|
}, 2000));
|
|
3040
2066
|
}
|
|
3041
2067
|
function removeFollowup(state, surfaceId) {
|
|
@@ -3229,11 +2255,185 @@ var KNOWN_METADATA_KEYS = new Set([
|
|
|
3229
2255
|
"expireAt",
|
|
3230
2256
|
"expireHint"
|
|
3231
2257
|
]);
|
|
2258
|
+
var INPUT_SURFACE_ACTIONS = new Set([
|
|
2259
|
+
"choice",
|
|
2260
|
+
"select",
|
|
2261
|
+
"toggle",
|
|
2262
|
+
"check",
|
|
2263
|
+
"multichoice"
|
|
2264
|
+
]);
|
|
2265
|
+
var TEXT_LEVELS = new Set(["h1", "h2", "h3", "h4", "h5", "body", "caption"]);
|
|
2266
|
+
var TEXT_HINTS = new Set(["icon", "avatar", "smallfeature", "mediumfeature", "largefeature", "header"]);
|
|
2267
|
+
var IMG_FITS = new Set(["contain", "cover", "fill", "none", "scale-down"]);
|
|
2268
|
+
var TF_TYPES = new Set(["email", "shorttext", "number", "phone", "longtext", "date", "obscured"]);
|
|
2269
|
+
var ALIGN_VALUES = new Set(["start", "center", "end", "stretch"]);
|
|
2270
|
+
var DISTRIBUTE_VALUES = new Set(["start", "center", "end", "spacebetween", "spacearound", "spaceevenly"]);
|
|
2271
|
+
var TREND_VALUES = new Set(["up", "down", "flat"]);
|
|
2272
|
+
var COMPONENT_CATALOG = {
|
|
2273
|
+
card: { positionalTokens: new Set, attrs: {} },
|
|
2274
|
+
column: {
|
|
2275
|
+
positionalTokens: new Set,
|
|
2276
|
+
attrs: {
|
|
2277
|
+
align: { kind: "enum", values: ALIGN_VALUES },
|
|
2278
|
+
distribute: { kind: "enum", values: DISTRIBUTE_VALUES }
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
row: {
|
|
2282
|
+
positionalTokens: new Set,
|
|
2283
|
+
attrs: {
|
|
2284
|
+
align: { kind: "enum", values: ALIGN_VALUES },
|
|
2285
|
+
distribute: { kind: "enum", values: DISTRIBUTE_VALUES }
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
list: { positionalTokens: new Set, attrs: {} },
|
|
2289
|
+
divider: { positionalTokens: new Set(["vertical"]), attrs: {} },
|
|
2290
|
+
text: {
|
|
2291
|
+
positionalTokens: TEXT_LEVELS,
|
|
2292
|
+
positionalFree: true,
|
|
2293
|
+
attrs: {
|
|
2294
|
+
color: { kind: "free" },
|
|
2295
|
+
hint: { kind: "enum", values: TEXT_HINTS },
|
|
2296
|
+
align: { kind: "free" }
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
metric: {
|
|
2300
|
+
positionalTokens: new Set,
|
|
2301
|
+
positionalFree: true,
|
|
2302
|
+
attrs: {
|
|
2303
|
+
trend: { kind: "enum", values: TREND_VALUES },
|
|
2304
|
+
color: { kind: "free" }
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
button: {
|
|
2308
|
+
positionalTokens: new Set(["primary"]),
|
|
2309
|
+
positionalFree: true,
|
|
2310
|
+
attrs: {
|
|
2311
|
+
action: { kind: "free" }
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
2314
|
+
image: {
|
|
2315
|
+
positionalTokens: new Set,
|
|
2316
|
+
positionalFree: true,
|
|
2317
|
+
attrs: {
|
|
2318
|
+
fit: { kind: "enum", values: IMG_FITS },
|
|
2319
|
+
hint: { kind: "enum", values: TEXT_HINTS }
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
icon: { positionalTokens: new Set, positionalFree: true, attrs: {} },
|
|
2323
|
+
textfield: {
|
|
2324
|
+
positionalTokens: new Set,
|
|
2325
|
+
positionalFree: true,
|
|
2326
|
+
attrs: {
|
|
2327
|
+
type: { kind: "enum", values: TF_TYPES },
|
|
2328
|
+
placeholder: { kind: "free" }
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
checkbox: {
|
|
2332
|
+
positionalTokens: new Set(["checked"]),
|
|
2333
|
+
positionalFree: true,
|
|
2334
|
+
attrs: {}
|
|
2335
|
+
},
|
|
2336
|
+
sparkline: {
|
|
2337
|
+
positionalTokens: new Set(["filled"]),
|
|
2338
|
+
positionalFree: true,
|
|
2339
|
+
attrs: {
|
|
2340
|
+
height: { kind: "int" },
|
|
2341
|
+
color: { kind: "free" }
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
progress: {
|
|
2345
|
+
positionalTokens: new Set,
|
|
2346
|
+
positionalFree: true,
|
|
2347
|
+
attrs: {
|
|
2348
|
+
value: { kind: "number" },
|
|
2349
|
+
color: { kind: "free" }
|
|
2350
|
+
}
|
|
2351
|
+
},
|
|
2352
|
+
pie: {
|
|
2353
|
+
positionalTokens: new Set(["solid"]),
|
|
2354
|
+
positionalFree: true,
|
|
2355
|
+
attrs: {
|
|
2356
|
+
labels: { kind: "array" },
|
|
2357
|
+
colors: { kind: "array" },
|
|
2358
|
+
height: { kind: "int" }
|
|
2359
|
+
}
|
|
2360
|
+
},
|
|
2361
|
+
barchart: {
|
|
2362
|
+
positionalTokens: new Set,
|
|
2363
|
+
positionalFree: true,
|
|
2364
|
+
attrs: {
|
|
2365
|
+
labels: { kind: "array" },
|
|
2366
|
+
colors: { kind: "array" },
|
|
2367
|
+
height: { kind: "int" }
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
badge: {
|
|
2371
|
+
positionalTokens: new Set(["outlined"]),
|
|
2372
|
+
positionalFree: true,
|
|
2373
|
+
attrs: {
|
|
2374
|
+
color: { kind: "free" }
|
|
2375
|
+
}
|
|
2376
|
+
},
|
|
2377
|
+
gauge: {
|
|
2378
|
+
positionalTokens: new Set,
|
|
2379
|
+
positionalFree: true,
|
|
2380
|
+
attrs: {
|
|
2381
|
+
value: { kind: "number" },
|
|
2382
|
+
color: { kind: "free" },
|
|
2383
|
+
size: { kind: "int" },
|
|
2384
|
+
text: { kind: "free" }
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
function tokenizeComponentLine(line) {
|
|
2389
|
+
let s = line.replace(/"[^"]*"/g, "");
|
|
2390
|
+
s = s.replace(/\[[^\]]*\]/g, "");
|
|
2391
|
+
return s.trim().split(/\s+/).filter(Boolean);
|
|
2392
|
+
}
|
|
2393
|
+
function validateComponentLine(line) {
|
|
2394
|
+
const unknownAttrs = [];
|
|
2395
|
+
const invalidEnumValues = [];
|
|
2396
|
+
const tokens = tokenizeComponentLine(line);
|
|
2397
|
+
if (tokens.length === 0)
|
|
2398
|
+
return { unknownAttrs, invalidEnumValues };
|
|
2399
|
+
const component = tokens[0];
|
|
2400
|
+
const spec = COMPONENT_CATALOG[component];
|
|
2401
|
+
if (!spec)
|
|
2402
|
+
return { unknownAttrs, invalidEnumValues };
|
|
2403
|
+
for (let i = 1;i < tokens.length; i++) {
|
|
2404
|
+
const tok = tokens[i];
|
|
2405
|
+
const eqIdx = tok.indexOf("=");
|
|
2406
|
+
if (eqIdx <= 0) {
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
const attr = tok.slice(0, eqIdx);
|
|
2410
|
+
const value = tok.slice(eqIdx + 1);
|
|
2411
|
+
const attrSpec = spec.attrs[attr];
|
|
2412
|
+
if (!attrSpec) {
|
|
2413
|
+
unknownAttrs.push(`${component}.${attr}`);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (attrSpec.kind === "enum" && attrSpec.values && !attrSpec.values.has(value.toLowerCase())) {
|
|
2417
|
+
invalidEnumValues.push({
|
|
2418
|
+
attr: `${component}.${attr}`,
|
|
2419
|
+
value,
|
|
2420
|
+
allowed: [...attrSpec.values]
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
return { unknownAttrs, invalidEnumValues };
|
|
2425
|
+
}
|
|
3232
2426
|
function validateBlockDsl(block) {
|
|
3233
2427
|
const unknownKeywords = [];
|
|
3234
2428
|
const metadataWithoutColon = [];
|
|
2429
|
+
const invalidInputActions = [];
|
|
2430
|
+
const unknownAttrsSet = new Set;
|
|
2431
|
+
const invalidEnumValues = [];
|
|
3235
2432
|
let hasCardKeyword = false;
|
|
3236
2433
|
let inDetailBlock = false;
|
|
2434
|
+
const headerLine = block.split(`
|
|
2435
|
+
`).find((l) => l.trim().startsWith("surface "))?.trim() ?? "";
|
|
2436
|
+
const isInputSurface = /\binput\b/.test(headerLine);
|
|
3237
2437
|
for (const rawLine of block.split(`
|
|
3238
2438
|
`)) {
|
|
3239
2439
|
const trimmed = rawLine.trim();
|
|
@@ -3261,6 +2461,24 @@ function validateBlockDsl(block) {
|
|
|
3261
2461
|
if (KNOWN_BODY_KEYWORDS.has(keyword)) {
|
|
3262
2462
|
if (keyword === "card")
|
|
3263
2463
|
hasCardKeyword = true;
|
|
2464
|
+
if (keyword === "button" && isInputSurface) {
|
|
2465
|
+
const actionMatch = trimmed.match(/\baction=(\S+)/);
|
|
2466
|
+
if (actionMatch) {
|
|
2467
|
+
const actionValue = actionMatch[1].toLowerCase();
|
|
2468
|
+
if (!INPUT_SURFACE_ACTIONS.has(actionValue)) {
|
|
2469
|
+
const labelMatch = trimmed.match(/button\s+"([^"]*)"/);
|
|
2470
|
+
invalidInputActions.push({
|
|
2471
|
+
action: actionMatch[1],
|
|
2472
|
+
label: labelMatch?.[1] ?? ""
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const componentCheck = validateComponentLine(trimmed);
|
|
2478
|
+
for (const a of componentCheck.unknownAttrs)
|
|
2479
|
+
unknownAttrsSet.add(a);
|
|
2480
|
+
for (const v of componentCheck.invalidEnumValues)
|
|
2481
|
+
invalidEnumValues.push(v);
|
|
3264
2482
|
continue;
|
|
3265
2483
|
}
|
|
3266
2484
|
unknownKeywords.push(keyword);
|
|
@@ -3268,7 +2486,10 @@ function validateBlockDsl(block) {
|
|
|
3268
2486
|
return {
|
|
3269
2487
|
unknownKeywords,
|
|
3270
2488
|
metadataWithoutColon,
|
|
3271
|
-
missingCardStructure: !hasCardKeyword
|
|
2489
|
+
missingCardStructure: !hasCardKeyword,
|
|
2490
|
+
invalidInputActions,
|
|
2491
|
+
unknownAttrs: [...unknownAttrsSet],
|
|
2492
|
+
invalidEnumValues
|
|
3272
2493
|
};
|
|
3273
2494
|
}
|
|
3274
2495
|
function processDslBlock(state, dsl) {
|
|
@@ -3377,24 +2598,65 @@ function processDslBlock(state, dsl) {
|
|
|
3377
2598
|
goalChanged,
|
|
3378
2599
|
unknownKeywords: validation.unknownKeywords.length > 0 ? validation.unknownKeywords : undefined,
|
|
3379
2600
|
metadataWithoutColon: validation.metadataWithoutColon.length > 0 ? validation.metadataWithoutColon : undefined,
|
|
3380
|
-
missingCardStructure: validation.missingCardStructure ? true : undefined
|
|
2601
|
+
missingCardStructure: validation.missingCardStructure ? true : undefined,
|
|
2602
|
+
invalidInputActions: validation.invalidInputActions.length > 0 ? validation.invalidInputActions : undefined,
|
|
2603
|
+
unknownAttrs: validation.unknownAttrs.length > 0 ? validation.unknownAttrs : undefined,
|
|
2604
|
+
invalidEnumValues: validation.invalidEnumValues.length > 0 ? validation.invalidEnumValues : undefined
|
|
3381
2605
|
});
|
|
3382
2606
|
recordSurfaceEvent(state, sid, existing ? "updated" : "created", block, state.surfaceDb.getAgentId(sid));
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
2607
|
+
}
|
|
2608
|
+
return results;
|
|
2609
|
+
}
|
|
2610
|
+
function pluginPushSurface(state, params) {
|
|
2611
|
+
if (!state.surfaceDb)
|
|
2612
|
+
return [];
|
|
2613
|
+
const blocksPre = params.dsl.split(/\n---(?:\n|$)/).filter((b) => b.trim().length > 0);
|
|
2614
|
+
const incomingInputIds = new Set;
|
|
2615
|
+
for (const block of blocksPre) {
|
|
2616
|
+
const headerLine = block.match(/^surface\s+.+$/m)?.[0] ?? "";
|
|
2617
|
+
if (/\binput\b/.test(headerLine)) {
|
|
2618
|
+
const sid = headerLine.match(/^surface\s+(\S+)/)?.[1];
|
|
2619
|
+
if (sid)
|
|
2620
|
+
incomingInputIds.add(sid);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
let effectiveDsl = params.dsl;
|
|
2624
|
+
if (incomingInputIds.size > 0) {
|
|
2625
|
+
const stale = [];
|
|
2626
|
+
for (const [existingId, meta] of state.surfaceDb.entries()) {
|
|
2627
|
+
if (incomingInputIds.has(existingId))
|
|
2628
|
+
continue;
|
|
2629
|
+
const existingHeader = meta.lines[0] ?? "";
|
|
2630
|
+
if (/\binput\b/.test(existingHeader))
|
|
2631
|
+
stale.push(existingId);
|
|
2632
|
+
}
|
|
2633
|
+
if (stale.length > 0) {
|
|
2634
|
+
console.log("[agentlife] plugin-push auto-deleting stale input(s) %s for %s", stale.join(","), [...incomingInputIds].join(","));
|
|
2635
|
+
effectiveDsl = [...stale.map((s) => `delete ${s}`), params.dsl].join(`
|
|
2636
|
+
---
|
|
2637
|
+
`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
const results = processDslBlock(state, effectiveDsl);
|
|
2641
|
+
const blocks = effectiveDsl.split(/\n---(?:\n|$)/).filter((b) => b.trim().length > 0);
|
|
2642
|
+
const surfaceIds = [];
|
|
2643
|
+
for (const block of blocks) {
|
|
2644
|
+
const match = block.match(/^surface\s+(\S+)/m);
|
|
2645
|
+
if (match)
|
|
2646
|
+
surfaceIds.push(match[1]);
|
|
2647
|
+
}
|
|
2648
|
+
for (const sid of surfaceIds) {
|
|
2649
|
+
state.surfaceDb.setAgentId(sid, params.agentId);
|
|
2650
|
+
}
|
|
2651
|
+
broadcastSurface(params.dsl);
|
|
2652
|
+
for (const result of results) {
|
|
2653
|
+
const meta = state.surfaceDb.get(result.surfaceId);
|
|
2654
|
+
if (!meta)
|
|
2655
|
+
continue;
|
|
2656
|
+
if (result.followupRemoved) {
|
|
2657
|
+
removeFollowup(state, result.surfaceId);
|
|
2658
|
+
} else if (meta.followup && (result.followupChanged || result.isNew)) {
|
|
2659
|
+
scheduleFollowup(state, result.surfaceId, meta.followup, params.agentId);
|
|
3398
2660
|
}
|
|
3399
2661
|
}
|
|
3400
2662
|
return results;
|
|
@@ -3448,6 +2710,103 @@ function runStartupPurge(state) {
|
|
|
3448
2710
|
console.log("[agentlife] surfaces DB: %d persisted%s%s", state.surfaceDb.size, purged > 0 ? ` (purged ${purged} past grace period)` : "", backfilled > 0 ? ` (backfilled ${backfilled} agent mappings from history)` : "");
|
|
3449
2711
|
}
|
|
3450
2712
|
|
|
2713
|
+
// render-widgets.ts
|
|
2714
|
+
var PROVISIONED_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
2715
|
+
function listSpecialistIds(runtime) {
|
|
2716
|
+
const cfg = runtime.config.loadConfig();
|
|
2717
|
+
const list = cfg?.agents?.list ?? [];
|
|
2718
|
+
return list.map((a) => a?.id).filter((id) => !!id && !PROVISIONED_IDS.has(id));
|
|
2719
|
+
}
|
|
2720
|
+
function recentActivityCount(state, agentId, sinceMs) {
|
|
2721
|
+
if (!state.historyDb)
|
|
2722
|
+
return 0;
|
|
2723
|
+
try {
|
|
2724
|
+
const row = state.historyDb.prepare("SELECT COUNT(*) as c FROM activity_log WHERE agentId = ? AND ts > ?").get(agentId, sinceMs);
|
|
2725
|
+
return row?.c ?? 0;
|
|
2726
|
+
} catch {
|
|
2727
|
+
return 0;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function pendingFollowupCount(state, agentId) {
|
|
2731
|
+
if (!state.historyDb)
|
|
2732
|
+
return 0;
|
|
2733
|
+
try {
|
|
2734
|
+
const row = state.historyDb.prepare("SELECT COUNT(*) as c FROM followups WHERE agentId = ? AND status = 'pending'").get(agentId);
|
|
2735
|
+
return row?.c ?? 0;
|
|
2736
|
+
} catch {
|
|
2737
|
+
return 0;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
function sanitizeDslString(s) {
|
|
2741
|
+
return s.replace(/"/g, "'").replace(/[\r\n]+/g, " ").slice(0, 120);
|
|
2742
|
+
}
|
|
2743
|
+
function composeAgentIntroDsl(opts) {
|
|
2744
|
+
const surfaceId = `${opts.agentId}-intro`;
|
|
2745
|
+
const title = `${opts.emoji} ${opts.name}`;
|
|
2746
|
+
const desc = sanitizeDslString(opts.description || `Your ${opts.name} agent, ready to track and act.`);
|
|
2747
|
+
const activeBadge = opts.activityCount > 0 ? "Active" : "Ready";
|
|
2748
|
+
const activeColor = opts.activityCount > 0 ? "#10B981" : "#6366F1";
|
|
2749
|
+
return [
|
|
2750
|
+
`surface ${surfaceId} size=m`,
|
|
2751
|
+
` card`,
|
|
2752
|
+
` column`,
|
|
2753
|
+
` text "${sanitizeDslString(title)}" h3`,
|
|
2754
|
+
` text "${desc}" body`,
|
|
2755
|
+
` divider`,
|
|
2756
|
+
` row distribute=spaceBetween`,
|
|
2757
|
+
` metric "Actions (7d)" "${opts.activityCount}"`,
|
|
2758
|
+
` metric "Upcoming" "${opts.followupCount}"`,
|
|
2759
|
+
` badge "${activeBadge}" color=${activeColor} outlined`,
|
|
2760
|
+
`goal: ${opts.name} — track progress toward user's first real outcome`,
|
|
2761
|
+
`followup: +24h "Check if the user interacted with the ${opts.name} agent. If yes, summarize progress. If not, push a gentle prompt relevant to the domain."`,
|
|
2762
|
+
`context: {"agentId":"${opts.agentId}","domain":"${opts.agentId}","phase":"intro","autoRendered":true}`
|
|
2763
|
+
].join(`
|
|
2764
|
+
`);
|
|
2765
|
+
}
|
|
2766
|
+
function agentEmoji(runtime, agentId) {
|
|
2767
|
+
const cfg = runtime.config.loadConfig();
|
|
2768
|
+
const list = cfg?.agents?.list ?? [];
|
|
2769
|
+
const found = list.find((a) => a?.id === agentId);
|
|
2770
|
+
return found?.identity?.emoji ?? "\uD83C\uDFAF";
|
|
2771
|
+
}
|
|
2772
|
+
function renderAgentWidget(state, runtime, agentId, log) {
|
|
2773
|
+
if (PROVISIONED_IDS.has(agentId))
|
|
2774
|
+
return;
|
|
2775
|
+
const entry = state.agentRegistry.get(agentId);
|
|
2776
|
+
if (!entry) {
|
|
2777
|
+
log(`[render-widgets] ${agentId}: no registry entry, skipping`);
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
2781
|
+
const dsl = composeAgentIntroDsl({
|
|
2782
|
+
agentId,
|
|
2783
|
+
name: entry.name,
|
|
2784
|
+
description: entry.description ?? "",
|
|
2785
|
+
emoji: agentEmoji(runtime, agentId),
|
|
2786
|
+
activityCount: recentActivityCount(state, agentId, sevenDaysAgo),
|
|
2787
|
+
followupCount: pendingFollowupCount(state, agentId)
|
|
2788
|
+
});
|
|
2789
|
+
pluginPushSurface(state, { agentId, dsl });
|
|
2790
|
+
log(`[render-widgets] rendered ${agentId}-intro`);
|
|
2791
|
+
}
|
|
2792
|
+
function renderAllAgentWidgets(state, runtime, log) {
|
|
2793
|
+
const ids = listSpecialistIds(runtime);
|
|
2794
|
+
if (ids.length === 0) {
|
|
2795
|
+
log("[render-widgets] no user agents — nothing to render");
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
for (const id of ids) {
|
|
2799
|
+
try {
|
|
2800
|
+
renderAgentWidget(state, runtime, id, log);
|
|
2801
|
+
} catch (e) {
|
|
2802
|
+
log(`[render-widgets] ${id} render failed: ${e?.message ?? e}`);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// services/surfaces-init.ts
|
|
2808
|
+
import * as path4 from "node:path";
|
|
2809
|
+
|
|
3451
2810
|
// cleanup.ts
|
|
3452
2811
|
function enqueueCleanupTasks(state, surfaceId, agentId, cronId, automations) {
|
|
3453
2812
|
const db = getOrCreateHistoryDb(state);
|
|
@@ -3539,25 +2898,21 @@ function registerSurfacesService(api, state) {
|
|
|
3539
2898
|
api.registerService({
|
|
3540
2899
|
id: "agentlife-surfaces",
|
|
3541
2900
|
start: async (ctx) => {
|
|
3542
|
-
const agentlifeDir =
|
|
2901
|
+
const agentlifeDir = path4.join(ctx.stateDir, "agentlife");
|
|
3543
2902
|
state.agentlifeStateDir = agentlifeDir;
|
|
3544
|
-
state.registryFilePath =
|
|
3545
|
-
state.dbBaseDir =
|
|
3546
|
-
state.historyDbPath =
|
|
2903
|
+
state.registryFilePath = path4.join(agentlifeDir, "agent-registry.json");
|
|
2904
|
+
state.dbBaseDir = path4.join(agentlifeDir, "db");
|
|
2905
|
+
state.historyDbPath = path4.join(agentlifeDir, "agentlife.db");
|
|
3547
2906
|
if (!state.surfaceDb) {
|
|
3548
2907
|
const db = getOrCreateHistoryDb(state);
|
|
3549
2908
|
state.surfaceDb = new SurfaceDb(db);
|
|
3550
2909
|
}
|
|
3551
2910
|
runStartupPurge(state);
|
|
3552
|
-
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
3553
|
-
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
3554
2911
|
let inputPurged = 0;
|
|
3555
2912
|
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
3556
2913
|
const headerLine = meta.lines[0] ?? "";
|
|
3557
2914
|
if (!/\binput\b/.test(headerLine))
|
|
3558
2915
|
continue;
|
|
3559
|
-
if (isFlowInput(surfaceId))
|
|
3560
|
-
continue;
|
|
3561
2916
|
state.surfaceDb.delete(surfaceId);
|
|
3562
2917
|
inputPurged++;
|
|
3563
2918
|
}
|
|
@@ -3612,22 +2967,22 @@ function registerSurfacesService(api, state) {
|
|
|
3612
2967
|
}
|
|
3613
2968
|
|
|
3614
2969
|
// services/config-optimizer.ts
|
|
3615
|
-
import * as
|
|
3616
|
-
import * as
|
|
3617
|
-
import * as
|
|
2970
|
+
import * as fs3 from "node:fs/promises";
|
|
2971
|
+
import * as path5 from "node:path";
|
|
2972
|
+
import * as os2 from "node:os";
|
|
3618
2973
|
var TARGET_MAX_PING_PONG = 1;
|
|
3619
2974
|
var TARGET_MAX_CONCURRENT = 10;
|
|
3620
|
-
var CONFIG_PATH =
|
|
2975
|
+
var CONFIG_PATH = path5.join(os2.homedir(), ".openclaw", "openclaw.json");
|
|
3621
2976
|
async function readRawConfigFromDisk() {
|
|
3622
|
-
const raw = await
|
|
2977
|
+
const raw = await fs3.readFile(CONFIG_PATH, "utf-8");
|
|
3623
2978
|
return JSON.parse(raw);
|
|
3624
2979
|
}
|
|
3625
2980
|
async function writeRawConfigToDisk(cfg) {
|
|
3626
|
-
await
|
|
2981
|
+
await fs3.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2) + `
|
|
3627
2982
|
`, "utf-8");
|
|
3628
2983
|
}
|
|
3629
2984
|
async function restoreConfigFromBackup(api, backupPath) {
|
|
3630
|
-
const raw = await
|
|
2985
|
+
const raw = await fs3.readFile(backupPath, "utf-8");
|
|
3631
2986
|
const backup = JSON.parse(raw);
|
|
3632
2987
|
const fileCfg = await readRawConfigFromDisk();
|
|
3633
2988
|
if (backup.maxPingPongTurns != null) {
|
|
@@ -3645,7 +3000,7 @@ async function restoreConfigFromBackup(api, backupPath) {
|
|
|
3645
3000
|
delete fileCfg.agents?.defaults?.maxConcurrent;
|
|
3646
3001
|
}
|
|
3647
3002
|
await writeRawConfigToDisk(fileCfg);
|
|
3648
|
-
await
|
|
3003
|
+
await fs3.unlink(backupPath);
|
|
3649
3004
|
console.log("[agentlife] restored config: maxPingPongTurns=%s, maxConcurrent=%s", backup.maxPingPongTurns ?? "default", backup.maxConcurrent ?? "default");
|
|
3650
3005
|
return `config restored`;
|
|
3651
3006
|
}
|
|
@@ -3653,8 +3008,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3653
3008
|
api.registerService({
|
|
3654
3009
|
id: "agentlife-config-optimizer",
|
|
3655
3010
|
start: async (ctx) => {
|
|
3656
|
-
const configBackupDir =
|
|
3657
|
-
const configBackupPath =
|
|
3011
|
+
const configBackupDir = path5.join(ctx.stateDir, "agentlife");
|
|
3012
|
+
const configBackupPath = path5.join(configBackupDir, "config-backup.json");
|
|
3658
3013
|
const cfg = api.runtime.config.loadConfig();
|
|
3659
3014
|
const currentPPT = cfg.session?.agentToAgent?.maxPingPongTurns;
|
|
3660
3015
|
const currentMC = cfg.agents?.defaults?.maxConcurrent;
|
|
@@ -3664,8 +3019,8 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3664
3019
|
console.log("[agentlife] config already optimized, skipping");
|
|
3665
3020
|
return;
|
|
3666
3021
|
}
|
|
3667
|
-
await
|
|
3668
|
-
await
|
|
3022
|
+
await fs3.mkdir(configBackupDir, { recursive: true });
|
|
3023
|
+
await fs3.writeFile(configBackupPath, JSON.stringify({
|
|
3669
3024
|
maxPingPongTurns: currentPPT ?? null,
|
|
3670
3025
|
maxConcurrent: currentMC ?? null
|
|
3671
3026
|
}));
|
|
@@ -3690,7 +3045,7 @@ function registerConfigOptimizer(api, _state) {
|
|
|
3690
3045
|
},
|
|
3691
3046
|
stop: async (ctx) => {
|
|
3692
3047
|
try {
|
|
3693
|
-
const backupPath =
|
|
3048
|
+
const backupPath = path5.join(ctx.stateDir, "agentlife", "config-backup.json");
|
|
3694
3049
|
await restoreConfigFromBackup(api, backupPath);
|
|
3695
3050
|
} catch {}
|
|
3696
3051
|
}
|
|
@@ -4141,8 +3496,8 @@ function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
|
|
|
4141
3496
|
}
|
|
4142
3497
|
|
|
4143
3498
|
// notifications.ts
|
|
4144
|
-
import * as
|
|
4145
|
-
import * as
|
|
3499
|
+
import * as fs4 from "node:fs";
|
|
3500
|
+
import * as path6 from "node:path";
|
|
4146
3501
|
var config;
|
|
4147
3502
|
var recentNotifications = new Map;
|
|
4148
3503
|
var DEBOUNCE_MS = 60000;
|
|
@@ -4150,8 +3505,8 @@ function loadConfig() {
|
|
|
4150
3505
|
if (config !== undefined)
|
|
4151
3506
|
return config;
|
|
4152
3507
|
try {
|
|
4153
|
-
const configPath =
|
|
4154
|
-
const raw =
|
|
3508
|
+
const configPath = path6.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
3509
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
4155
3510
|
const parsed = JSON.parse(raw);
|
|
4156
3511
|
if (parsed.serverUrl && parsed.apiKey) {
|
|
4157
3512
|
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
@@ -4316,7 +3671,7 @@ function sendToInternalSession(state, agentId, message, idempotencyKey) {
|
|
|
4316
3671
|
});
|
|
4317
3672
|
}
|
|
4318
3673
|
}
|
|
4319
|
-
var SKIP_WIDGET_CHECK = new Set(["agentlife", "agentlife-builder", "
|
|
3674
|
+
var SKIP_WIDGET_CHECK = new Set(["agentlife", "agentlife-builder", "supervisor"]);
|
|
4320
3675
|
function registerActivityHooks(api, state) {
|
|
4321
3676
|
api.on("llm_output", (event, ctx) => {
|
|
4322
3677
|
if (state.disabled)
|
|
@@ -4382,17 +3737,6 @@ function registerActivityHooks(api, state) {
|
|
|
4382
3737
|
console.log("[agentlife] action redirected to %s for %s (origin=%s)", actionSessionKey, actionSurfaceId, origin ? "set" : "legacy");
|
|
4383
3738
|
}
|
|
4384
3739
|
}
|
|
4385
|
-
const dreamMatch = message.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
4386
|
-
if (dreamMatch) {
|
|
4387
|
-
const labelMatch = message.match(/\blabel=([^\n]+)/);
|
|
4388
|
-
const valueMatch = message.match(/\bvalue=([^\n]+)/);
|
|
4389
|
-
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
4390
|
-
try {
|
|
4391
|
-
emitTrigger(state, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
4392
|
-
} catch (e) {
|
|
4393
|
-
console.warn("[agentlife] cold-start emit userAnswered failed: %s", e?.message);
|
|
4394
|
-
}
|
|
4395
|
-
}
|
|
4396
3740
|
});
|
|
4397
3741
|
api.on("llm_output", (event, ctx) => {
|
|
4398
3742
|
if (state.disabled)
|
|
@@ -4425,39 +3769,6 @@ function registerActivityHooks(api, state) {
|
|
|
4425
3769
|
})
|
|
4426
3770
|
});
|
|
4427
3771
|
}
|
|
4428
|
-
let isCurrentColdStartSession = false;
|
|
4429
|
-
if (agentId) {
|
|
4430
|
-
try {
|
|
4431
|
-
const cs = readState(state);
|
|
4432
|
-
if (cs && cs.actionSessionKey === sessionKey && cs.phase !== "READY" && cs.phase !== "FAILED") {
|
|
4433
|
-
isCurrentColdStartSession = true;
|
|
4434
|
-
}
|
|
4435
|
-
} catch {}
|
|
4436
|
-
}
|
|
4437
|
-
if (isCurrentColdStartSession && agentId) {
|
|
4438
|
-
const lastText = texts.length > 0 ? texts[texts.length - 1].trim() : "";
|
|
4439
|
-
const lastLine = lastText.split(`
|
|
4440
|
-
`).pop()?.trim() ?? "";
|
|
4441
|
-
let marker = null;
|
|
4442
|
-
if (/\bNO_SIGNAL\b/.test(lastText)) {
|
|
4443
|
-
marker = "NO_SIGNAL";
|
|
4444
|
-
} else if (TERMINAL_MARKERS.has(lastLine)) {
|
|
4445
|
-
marker = lastLine;
|
|
4446
|
-
} else if (TERMINAL_MARKERS.has(lastText)) {
|
|
4447
|
-
marker = lastText;
|
|
4448
|
-
}
|
|
4449
|
-
try {
|
|
4450
|
-
emitTrigger(state, "agentReplied", {
|
|
4451
|
-
agentId,
|
|
4452
|
-
sessionKey,
|
|
4453
|
-
marker,
|
|
4454
|
-
lastText: lastText.slice(0, 4000),
|
|
4455
|
-
terminal: marker !== null
|
|
4456
|
-
});
|
|
4457
|
-
} catch (e) {
|
|
4458
|
-
console.warn("[agentlife] cold-start emit agentReplied failed: %s", e?.message);
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
3772
|
}, { priority: 100 });
|
|
4462
3773
|
api.on("before_tool_call", (event, ctx) => {
|
|
4463
3774
|
if (state.disabled)
|
|
@@ -4497,30 +3808,10 @@ function registerActivityHooks(api, state) {
|
|
|
4497
3808
|
const isError = !!event.error;
|
|
4498
3809
|
const toolName = event.toolName ?? ctx?.toolName;
|
|
4499
3810
|
const toolCallId = event.toolCallId ?? ctx?.toolCallId ?? null;
|
|
4500
|
-
const runId = event.runId ?? ctx?.runId ?? null;
|
|
4501
|
-
let canvasDsl = null;
|
|
4502
|
-
if (event.toolName === "agentlife_push" && typeof event.params?.dsl === "string") {
|
|
4503
|
-
canvasDsl = event.params.dsl;
|
|
4504
|
-
}
|
|
4505
|
-
if (!isError && agentId) {
|
|
4506
|
-
let touchedMemory = false;
|
|
4507
|
-
if (toolName === "exec") {
|
|
4508
|
-
const command = event.params?.command ?? event.params?.script ?? "";
|
|
4509
|
-
if (typeof command === "string" && /memory\/[\w.\-]+\.md/.test(command) && /(>>?|tee\b|cp\b|mv\b)/.test(command)) {
|
|
4510
|
-
touchedMemory = true;
|
|
4511
|
-
}
|
|
4512
|
-
} else if (toolName === "write" || toolName === "edit" || toolName === "str_replace" || toolName === "apply_patch") {
|
|
4513
|
-
const filePath = event.params?.path ?? event.params?.file_path ?? "";
|
|
4514
|
-
if (/memory\/[\w.\-]+\.md$/.test(filePath))
|
|
4515
|
-
touchedMemory = true;
|
|
4516
|
-
}
|
|
4517
|
-
if (touchedMemory) {
|
|
4518
|
-
try {
|
|
4519
|
-
emitTrigger(state, "memoryWritten", { agentId, sessionKey });
|
|
4520
|
-
} catch (e) {
|
|
4521
|
-
console.warn("[agentlife] cold-start emit memoryWritten failed: %s", e?.message);
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
3811
|
+
const runId = event.runId ?? ctx?.runId ?? null;
|
|
3812
|
+
let canvasDsl = null;
|
|
3813
|
+
if (event.toolName === "agentlife_push" && typeof event.params?.dsl === "string") {
|
|
3814
|
+
canvasDsl = event.params.dsl;
|
|
4524
3815
|
}
|
|
4525
3816
|
const resultStr = typeof event.result === "string" ? event.result : event.result != null ? JSON.stringify(event.result) : null;
|
|
4526
3817
|
recordActivity(state, "tool_end", sessionKey, agentId, {
|
|
@@ -5307,31 +4598,31 @@ function startDailySweepService(state) {
|
|
|
5307
4598
|
import * as crypto4 from "node:crypto";
|
|
5308
4599
|
import * as fsSync3 from "node:fs";
|
|
5309
4600
|
import { createRequire as createRequire3 } from "node:module";
|
|
5310
|
-
import * as
|
|
5311
|
-
import * as
|
|
4601
|
+
import * as os6 from "node:os";
|
|
4602
|
+
import * as path10 from "node:path";
|
|
5312
4603
|
|
|
5313
4604
|
// gateway/web-app.ts
|
|
5314
4605
|
import * as crypto3 from "node:crypto";
|
|
5315
|
-
import * as
|
|
5316
|
-
import * as
|
|
5317
|
-
import * as
|
|
4606
|
+
import * as os5 from "node:os";
|
|
4607
|
+
import * as path9 from "node:path";
|
|
4608
|
+
import * as fs7 from "node:fs";
|
|
5318
4609
|
|
|
5319
4610
|
// services/pairing-access-token.ts
|
|
5320
4611
|
import * as crypto from "node:crypto";
|
|
5321
|
-
import * as
|
|
5322
|
-
import * as
|
|
5323
|
-
import * as
|
|
4612
|
+
import * as fs5 from "node:fs";
|
|
4613
|
+
import * as os3 from "node:os";
|
|
4614
|
+
import * as path7 from "node:path";
|
|
5324
4615
|
var cachedToken = null;
|
|
5325
4616
|
function pairingAccessPath() {
|
|
5326
|
-
return
|
|
4617
|
+
return path7.join(os3.homedir(), ".openclaw", "agentlife", "pairing-access.json");
|
|
5327
4618
|
}
|
|
5328
4619
|
function loadOrCreatePairingAccessToken() {
|
|
5329
4620
|
if (cachedToken)
|
|
5330
4621
|
return cachedToken;
|
|
5331
4622
|
const filePath = pairingAccessPath();
|
|
5332
4623
|
try {
|
|
5333
|
-
if (
|
|
5334
|
-
const raw =
|
|
4624
|
+
if (fs5.existsSync(filePath)) {
|
|
4625
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
5335
4626
|
const obj = JSON.parse(raw);
|
|
5336
4627
|
const token2 = typeof obj?.token === "string" ? obj.token : null;
|
|
5337
4628
|
if (token2 && token2.length >= 32) {
|
|
@@ -5340,12 +4631,12 @@ function loadOrCreatePairingAccessToken() {
|
|
|
5340
4631
|
}
|
|
5341
4632
|
}
|
|
5342
4633
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
5343
|
-
const dir =
|
|
5344
|
-
if (!
|
|
5345
|
-
|
|
5346
|
-
|
|
4634
|
+
const dir = path7.dirname(filePath);
|
|
4635
|
+
if (!fs5.existsSync(dir))
|
|
4636
|
+
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4637
|
+
fs5.writeFileSync(filePath, JSON.stringify({ token, createdAtMs: Date.now() }, null, 2), { mode: 384 });
|
|
5347
4638
|
try {
|
|
5348
|
-
|
|
4639
|
+
fs5.chmodSync(filePath, 384);
|
|
5349
4640
|
} catch {}
|
|
5350
4641
|
cachedToken = token;
|
|
5351
4642
|
return token;
|
|
@@ -5358,18 +4649,18 @@ function loadOrCreatePairingAccessToken() {
|
|
|
5358
4649
|
// services/cloudflared-supervisor.ts
|
|
5359
4650
|
import { spawn, spawnSync } from "node:child_process";
|
|
5360
4651
|
import * as crypto2 from "node:crypto";
|
|
5361
|
-
import * as
|
|
5362
|
-
import * as
|
|
5363
|
-
import * as
|
|
5364
|
-
var AGENTLIFE_DIR =
|
|
5365
|
-
var TUNNEL_FILE =
|
|
5366
|
-
var BIN_DIR =
|
|
5367
|
-
var PAIR_REQUEST_MARKER =
|
|
5368
|
-
var IDENTITY_DIR =
|
|
5369
|
-
var DEVICE_FILE =
|
|
5370
|
-
var LEGACY_DEVICE_FILE =
|
|
5371
|
-
var CLOUDFLARED_BIN =
|
|
5372
|
-
var CLOUDFLARED_PATH =
|
|
4652
|
+
import * as fs6 from "node:fs";
|
|
4653
|
+
import * as os4 from "node:os";
|
|
4654
|
+
import * as path8 from "node:path";
|
|
4655
|
+
var AGENTLIFE_DIR = path8.join(os4.homedir(), ".openclaw", "agentlife");
|
|
4656
|
+
var TUNNEL_FILE = path8.join(AGENTLIFE_DIR, "tunnel.json");
|
|
4657
|
+
var BIN_DIR = path8.join(AGENTLIFE_DIR, "bin");
|
|
4658
|
+
var PAIR_REQUEST_MARKER = path8.join(AGENTLIFE_DIR, "pair-requested");
|
|
4659
|
+
var IDENTITY_DIR = path8.join(os4.homedir(), ".agentlife");
|
|
4660
|
+
var DEVICE_FILE = path8.join(IDENTITY_DIR, "device.json");
|
|
4661
|
+
var LEGACY_DEVICE_FILE = path8.join(AGENTLIFE_DIR, "device.json");
|
|
4662
|
+
var CLOUDFLARED_BIN = os4.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
4663
|
+
var CLOUDFLARED_PATH = path8.join(BIN_DIR, CLOUDFLARED_BIN);
|
|
5373
4664
|
var API_BASE = process.env.AGENTLIFE_API_BASE || "https://api.agentlife.app";
|
|
5374
4665
|
var RESTART_DELAY_MS = 5000;
|
|
5375
4666
|
var STABLE_RUNTIME_MS = 60000;
|
|
@@ -5489,20 +4780,20 @@ async function doBootstrap() {
|
|
|
5489
4780
|
return tunnelInfo;
|
|
5490
4781
|
}
|
|
5491
4782
|
function isPairRequested() {
|
|
5492
|
-
return
|
|
4783
|
+
return fs6.existsSync(PAIR_REQUEST_MARKER);
|
|
5493
4784
|
}
|
|
5494
4785
|
function requestPair() {
|
|
5495
4786
|
try {
|
|
5496
|
-
if (!
|
|
5497
|
-
|
|
5498
|
-
|
|
4787
|
+
if (!fs6.existsSync(AGENTLIFE_DIR))
|
|
4788
|
+
fs6.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
4789
|
+
fs6.writeFileSync(PAIR_REQUEST_MARKER, String(Date.now()), { mode: 384 });
|
|
5499
4790
|
} catch (err) {
|
|
5500
4791
|
console.warn(`[cloudflared-supervisor] failed to write pair-request marker: ${err?.message ?? err}`);
|
|
5501
4792
|
}
|
|
5502
4793
|
}
|
|
5503
4794
|
function clearPairRequest() {
|
|
5504
4795
|
try {
|
|
5505
|
-
|
|
4796
|
+
fs6.unlinkSync(PAIR_REQUEST_MARKER);
|
|
5506
4797
|
} catch {}
|
|
5507
4798
|
}
|
|
5508
4799
|
function parseRetryAfter(value) {
|
|
@@ -5526,18 +4817,18 @@ function scheduleProvisionRetry(delayMs) {
|
|
|
5526
4817
|
}, delayMs);
|
|
5527
4818
|
}
|
|
5528
4819
|
function ensureDirs() {
|
|
5529
|
-
if (!
|
|
5530
|
-
|
|
5531
|
-
if (!
|
|
5532
|
-
|
|
5533
|
-
if (!
|
|
5534
|
-
|
|
4820
|
+
if (!fs6.existsSync(AGENTLIFE_DIR))
|
|
4821
|
+
fs6.mkdirSync(AGENTLIFE_DIR, { recursive: true, mode: 448 });
|
|
4822
|
+
if (!fs6.existsSync(BIN_DIR))
|
|
4823
|
+
fs6.mkdirSync(BIN_DIR, { recursive: true, mode: 448 });
|
|
4824
|
+
if (!fs6.existsSync(IDENTITY_DIR))
|
|
4825
|
+
fs6.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
5535
4826
|
}
|
|
5536
4827
|
function readIdentityFile(filePath) {
|
|
5537
|
-
if (!
|
|
4828
|
+
if (!fs6.existsSync(filePath))
|
|
5538
4829
|
return null;
|
|
5539
4830
|
try {
|
|
5540
|
-
const raw =
|
|
4831
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
5541
4832
|
const parsed = JSON.parse(raw);
|
|
5542
4833
|
if (typeof parsed.deviceId === "string" && typeof parsed.deviceSecret === "string") {
|
|
5543
4834
|
return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
|
|
@@ -5555,14 +4846,14 @@ function loadDeviceIdentity() {
|
|
|
5555
4846
|
if (!legacy)
|
|
5556
4847
|
return null;
|
|
5557
4848
|
try {
|
|
5558
|
-
if (!
|
|
5559
|
-
|
|
5560
|
-
|
|
4849
|
+
if (!fs6.existsSync(IDENTITY_DIR))
|
|
4850
|
+
fs6.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
4851
|
+
fs6.writeFileSync(DEVICE_FILE, JSON.stringify(legacy, null, 2), { mode: 384 });
|
|
5561
4852
|
try {
|
|
5562
|
-
|
|
4853
|
+
fs6.chmodSync(DEVICE_FILE, 384);
|
|
5563
4854
|
} catch {}
|
|
5564
4855
|
try {
|
|
5565
|
-
|
|
4856
|
+
fs6.unlinkSync(LEGACY_DEVICE_FILE);
|
|
5566
4857
|
} catch {}
|
|
5567
4858
|
console.log(`[cloudflared-supervisor] migrated device.json to ${DEVICE_FILE}`);
|
|
5568
4859
|
} catch (err) {
|
|
@@ -5574,25 +4865,25 @@ function loadOrCreateDeviceIdentity() {
|
|
|
5574
4865
|
const existing = loadDeviceIdentity();
|
|
5575
4866
|
if (existing)
|
|
5576
4867
|
return existing;
|
|
5577
|
-
if (!
|
|
5578
|
-
|
|
4868
|
+
if (!fs6.existsSync(IDENTITY_DIR))
|
|
4869
|
+
fs6.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 448 });
|
|
5579
4870
|
const identity = {
|
|
5580
4871
|
deviceId: crypto2.randomUUID(),
|
|
5581
4872
|
deviceSecret: crypto2.randomBytes(32).toString("base64url")
|
|
5582
4873
|
};
|
|
5583
|
-
|
|
4874
|
+
fs6.writeFileSync(DEVICE_FILE, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5584
4875
|
try {
|
|
5585
|
-
|
|
4876
|
+
fs6.chmodSync(DEVICE_FILE, 384);
|
|
5586
4877
|
} catch {}
|
|
5587
4878
|
console.log(`[cloudflared-supervisor] generated new device identity (deviceId=${identity.deviceId})`);
|
|
5588
4879
|
return identity;
|
|
5589
4880
|
}
|
|
5590
4881
|
var AGENTLIFE_API_BASE = API_BASE;
|
|
5591
4882
|
function loadCachedTunnel() {
|
|
5592
|
-
if (!
|
|
4883
|
+
if (!fs6.existsSync(TUNNEL_FILE))
|
|
5593
4884
|
return null;
|
|
5594
4885
|
try {
|
|
5595
|
-
const raw =
|
|
4886
|
+
const raw = fs6.readFileSync(TUNNEL_FILE, "utf-8");
|
|
5596
4887
|
const parsed = JSON.parse(raw);
|
|
5597
4888
|
if (typeof parsed.subdomain === "string" && typeof parsed.hostname === "string" && typeof parsed.tunnelUrl === "string" && typeof parsed.tunnelToken === "string" && typeof parsed.provisionedAt === "number") {
|
|
5598
4889
|
return parsed;
|
|
@@ -5601,9 +4892,9 @@ function loadCachedTunnel() {
|
|
|
5601
4892
|
return null;
|
|
5602
4893
|
}
|
|
5603
4894
|
function persistTunnel(info) {
|
|
5604
|
-
|
|
4895
|
+
fs6.writeFileSync(TUNNEL_FILE, JSON.stringify(info, null, 2), { mode: 384 });
|
|
5605
4896
|
try {
|
|
5606
|
-
|
|
4897
|
+
fs6.chmodSync(TUNNEL_FILE, 384);
|
|
5607
4898
|
} catch {}
|
|
5608
4899
|
}
|
|
5609
4900
|
async function provisionTunnel(identity) {
|
|
@@ -5646,14 +4937,14 @@ async function provisionTunnel(identity) {
|
|
|
5646
4937
|
}
|
|
5647
4938
|
}
|
|
5648
4939
|
function ensureCloudflaredBinary() {
|
|
5649
|
-
if (
|
|
4940
|
+
if (fs6.existsSync(CLOUDFLARED_PATH)) {
|
|
5650
4941
|
try {
|
|
5651
|
-
|
|
4942
|
+
fs6.accessSync(CLOUDFLARED_PATH, fs6.constants.X_OK);
|
|
5652
4943
|
return CLOUDFLARED_PATH;
|
|
5653
4944
|
} catch {}
|
|
5654
4945
|
}
|
|
5655
|
-
const platform2 =
|
|
5656
|
-
const arch2 =
|
|
4946
|
+
const platform2 = os4.platform();
|
|
4947
|
+
const arch2 = os4.arch();
|
|
5657
4948
|
const release = detectCloudflaredRelease(platform2, arch2);
|
|
5658
4949
|
if (!release) {
|
|
5659
4950
|
console.warn(`[cloudflared-supervisor] unsupported platform: ${platform2}/${arch2}`);
|
|
@@ -5666,28 +4957,28 @@ function ensureCloudflaredBinary() {
|
|
|
5666
4957
|
if (release.kind === "tgz") {
|
|
5667
4958
|
const extracted = extractTgzCloudflared(release.tempPath, BIN_DIR);
|
|
5668
4959
|
try {
|
|
5669
|
-
|
|
4960
|
+
fs6.unlinkSync(release.tempPath);
|
|
5670
4961
|
} catch {}
|
|
5671
4962
|
if (!extracted)
|
|
5672
4963
|
return null;
|
|
5673
4964
|
} else {
|
|
5674
4965
|
try {
|
|
5675
|
-
|
|
4966
|
+
fs6.renameSync(release.tempPath, CLOUDFLARED_PATH);
|
|
5676
4967
|
} catch (err) {
|
|
5677
4968
|
console.warn(`[cloudflared-supervisor] rename failed: ${err?.message}`);
|
|
5678
4969
|
return null;
|
|
5679
4970
|
}
|
|
5680
4971
|
}
|
|
5681
4972
|
try {
|
|
5682
|
-
|
|
4973
|
+
fs6.chmodSync(CLOUDFLARED_PATH, 493);
|
|
5683
4974
|
} catch {}
|
|
5684
|
-
return
|
|
4975
|
+
return fs6.existsSync(CLOUDFLARED_PATH) ? CLOUDFLARED_PATH : null;
|
|
5685
4976
|
}
|
|
5686
4977
|
function detectCloudflaredRelease(platform2, arch2) {
|
|
5687
4978
|
const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
5688
4979
|
if (platform2 === "darwin") {
|
|
5689
4980
|
const asset = arch2 === "arm64" ? "cloudflared-darwin-arm64.tgz" : "cloudflared-darwin-amd64.tgz";
|
|
5690
|
-
return { url: `${base}/${asset}`, kind: "tgz", tempPath:
|
|
4981
|
+
return { url: `${base}/${asset}`, kind: "tgz", tempPath: path8.join(BIN_DIR, asset) };
|
|
5691
4982
|
}
|
|
5692
4983
|
if (platform2 === "linux") {
|
|
5693
4984
|
const asset = arch2 === "arm64" ? "cloudflared-linux-arm64" : arch2 === "arm" ? "cloudflared-linux-arm" : arch2 === "x64" ? "cloudflared-linux-amd64" : null;
|
|
@@ -5711,11 +5002,11 @@ function downloadWithCurl(url, dest) {
|
|
|
5711
5002
|
if (result.status !== 0) {
|
|
5712
5003
|
console.warn(`[cloudflared-supervisor] curl failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
5713
5004
|
try {
|
|
5714
|
-
|
|
5005
|
+
fs6.unlinkSync(dest);
|
|
5715
5006
|
} catch {}
|
|
5716
5007
|
return false;
|
|
5717
5008
|
}
|
|
5718
|
-
return
|
|
5009
|
+
return fs6.existsSync(dest) && fs6.statSync(dest).size > 0;
|
|
5719
5010
|
}
|
|
5720
5011
|
function extractTgzCloudflared(tgzPath, destDir) {
|
|
5721
5012
|
const result = spawnSync("tar", ["-xzf", tgzPath, "-C", destDir], { stdio: "pipe" });
|
|
@@ -5723,7 +5014,7 @@ function extractTgzCloudflared(tgzPath, destDir) {
|
|
|
5723
5014
|
console.warn(`[cloudflared-supervisor] tar extract failed (exit ${result.status}): ${result.stderr?.toString().slice(0, 200)}`);
|
|
5724
5015
|
return false;
|
|
5725
5016
|
}
|
|
5726
|
-
return
|
|
5017
|
+
return fs6.existsSync(CLOUDFLARED_PATH);
|
|
5727
5018
|
}
|
|
5728
5019
|
function startCloudflaredProcess(binPath, tunnelToken) {
|
|
5729
5020
|
if (state.stopped)
|
|
@@ -5780,11 +5071,11 @@ var MIME_TYPES = {
|
|
|
5780
5071
|
};
|
|
5781
5072
|
function mintBootstrapToken() {
|
|
5782
5073
|
const bootstrapToken = crypto3.randomBytes(32).toString("base64url");
|
|
5783
|
-
const devicesDir =
|
|
5784
|
-
const bootstrapPath =
|
|
5785
|
-
if (!
|
|
5786
|
-
|
|
5787
|
-
const registry =
|
|
5074
|
+
const devicesDir = path9.join(os5.homedir(), ".openclaw", "devices");
|
|
5075
|
+
const bootstrapPath = path9.join(devicesDir, "bootstrap.json");
|
|
5076
|
+
if (!fs7.existsSync(devicesDir))
|
|
5077
|
+
fs7.mkdirSync(devicesDir, { recursive: true });
|
|
5078
|
+
const registry = fs7.existsSync(bootstrapPath) ? JSON.parse(fs7.readFileSync(bootstrapPath, "utf-8")) : {};
|
|
5788
5079
|
registry[bootstrapToken] = {
|
|
5789
5080
|
token: bootstrapToken,
|
|
5790
5081
|
profile: {
|
|
@@ -5793,15 +5084,15 @@ function mintBootstrapToken() {
|
|
|
5793
5084
|
},
|
|
5794
5085
|
issuedAtMs: Date.now()
|
|
5795
5086
|
};
|
|
5796
|
-
|
|
5087
|
+
fs7.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
|
|
5797
5088
|
return bootstrapToken;
|
|
5798
5089
|
}
|
|
5799
5090
|
function registerWebApp(api) {
|
|
5800
|
-
const pluginRoot =
|
|
5801
|
-
const appRoot =
|
|
5802
|
-
const hasWebBuild =
|
|
5803
|
-
const indexPath = hasWebBuild ?
|
|
5804
|
-
const hasIndex = !!(indexPath &&
|
|
5091
|
+
const pluginRoot = path9.resolve(path9.dirname(api.source), "..");
|
|
5092
|
+
const appRoot = path9.join(pluginRoot, "web-build");
|
|
5093
|
+
const hasWebBuild = fs7.existsSync(appRoot);
|
|
5094
|
+
const indexPath = hasWebBuild ? path9.join(appRoot, "index.html") : null;
|
|
5095
|
+
const hasIndex = !!(indexPath && fs7.existsSync(indexPath));
|
|
5805
5096
|
if (!hasWebBuild) {
|
|
5806
5097
|
api.logger.info("[agentlife] web-build/ not found — /agentlife/pair will still register; static dashboard disabled");
|
|
5807
5098
|
} else if (!hasIndex) {
|
|
@@ -5809,8 +5100,8 @@ function registerWebApp(api) {
|
|
|
5809
5100
|
}
|
|
5810
5101
|
let gatewayToken = "";
|
|
5811
5102
|
try {
|
|
5812
|
-
const configPath =
|
|
5813
|
-
const raw = JSON.parse(
|
|
5103
|
+
const configPath = path9.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
|
|
5104
|
+
const raw = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5814
5105
|
gatewayToken = raw?.gateway?.auth?.token || "";
|
|
5815
5106
|
} catch {}
|
|
5816
5107
|
loadOrCreatePairingAccessToken();
|
|
@@ -5928,16 +5219,16 @@ function registerWebApp(api) {
|
|
|
5928
5219
|
return true;
|
|
5929
5220
|
}
|
|
5930
5221
|
const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
|
|
5931
|
-
const filePath =
|
|
5222
|
+
const filePath = path9.resolve(appRoot, relative);
|
|
5932
5223
|
if (!filePath.startsWith(appRoot)) {
|
|
5933
5224
|
res.writeHead(403);
|
|
5934
5225
|
res.end();
|
|
5935
5226
|
return true;
|
|
5936
5227
|
}
|
|
5937
|
-
const target =
|
|
5938
|
-
const ext =
|
|
5228
|
+
const target = fs7.existsSync(filePath) && fs7.statSync(filePath).isFile() ? filePath : indexPath;
|
|
5229
|
+
const ext = path9.extname(target).toLowerCase();
|
|
5939
5230
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
5940
|
-
let content =
|
|
5231
|
+
let content = fs7.readFileSync(target);
|
|
5941
5232
|
if (target === indexPath) {
|
|
5942
5233
|
setTimeout(approveLatest, 2000);
|
|
5943
5234
|
setTimeout(approveLatest, 5000);
|
|
@@ -6026,9 +5317,9 @@ Setup code: ${setupCode}
|
|
|
6026
5317
|
api.registerService({
|
|
6027
5318
|
id: "agentlife-auto-pair",
|
|
6028
5319
|
start: async (ctx) => {
|
|
6029
|
-
const devicesDir =
|
|
6030
|
-
const pendingPath =
|
|
6031
|
-
const pairedPath =
|
|
5320
|
+
const devicesDir = path10.join(os6.homedir(), ".openclaw", "devices");
|
|
5321
|
+
const pendingPath = path10.join(devicesDir, "pending.json");
|
|
5322
|
+
const pairedPath = path10.join(devicesDir, "paired.json");
|
|
6032
5323
|
const pollInterval = 3000;
|
|
6033
5324
|
const withAdmin = (arr) => {
|
|
6034
5325
|
const base = Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
@@ -6126,7 +5417,7 @@ Setup code: ${setupCode}
|
|
|
6126
5417
|
|
|
6127
5418
|
// hooks/bootstrap.ts
|
|
6128
5419
|
var SKIP_GUIDANCE_AGENTS = new Set(["agentlife"]);
|
|
6129
|
-
var SKIP_AGENTS_MD_INJECTION = new Set(["agentlife", "agentlife-builder", "
|
|
5420
|
+
var SKIP_AGENTS_MD_INJECTION = new Set(["agentlife", "agentlife-builder", "quick", "supervisor"]);
|
|
6130
5421
|
function registerBootstrapHook(api, state2) {
|
|
6131
5422
|
api.registerService({
|
|
6132
5423
|
id: "agentlife-bootstrap-hook",
|
|
@@ -6347,7 +5638,120 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6347
5638
|
newSurfaceIds.push(match[1]);
|
|
6348
5639
|
}
|
|
6349
5640
|
}
|
|
6350
|
-
const
|
|
5641
|
+
const ownershipViolations = [];
|
|
5642
|
+
const filteredBlocks = [];
|
|
5643
|
+
if (agentId && state2.surfaceDb) {
|
|
5644
|
+
for (const block of blocks) {
|
|
5645
|
+
const trimmed = block.trim();
|
|
5646
|
+
const deleteMatch = trimmed.match(/^delete\s+(\S+)/);
|
|
5647
|
+
const surfaceMatch = block.match(/^surface\s+(\S+)/m);
|
|
5648
|
+
const sid = deleteMatch?.[1] ?? surfaceMatch?.[1] ?? null;
|
|
5649
|
+
const action = deleteMatch ? "delete" : "push";
|
|
5650
|
+
if (!sid) {
|
|
5651
|
+
filteredBlocks.push(block);
|
|
5652
|
+
continue;
|
|
5653
|
+
}
|
|
5654
|
+
const existingOwner = state2.surfaceDb.getAgentId(sid);
|
|
5655
|
+
if (existingOwner && existingOwner !== agentId) {
|
|
5656
|
+
ownershipViolations.push({ surfaceId: sid, owner: existingOwner, action });
|
|
5657
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
5658
|
+
data: JSON.stringify({
|
|
5659
|
+
issue: "surface_ownership_violation",
|
|
5660
|
+
surfaceId: sid,
|
|
5661
|
+
owner: existingOwner,
|
|
5662
|
+
attempt: agentId,
|
|
5663
|
+
action
|
|
5664
|
+
})
|
|
5665
|
+
});
|
|
5666
|
+
continue;
|
|
5667
|
+
}
|
|
5668
|
+
filteredBlocks.push(block);
|
|
5669
|
+
}
|
|
5670
|
+
} else {
|
|
5671
|
+
filteredBlocks.push(...blocks);
|
|
5672
|
+
}
|
|
5673
|
+
if (filteredBlocks.length === 0 && ownershipViolations.length > 0) {
|
|
5674
|
+
const detail = ownershipViolations.map((v) => `${v.surfaceId} (owner=${v.owner}, attempt=${v.action})`).join("; ");
|
|
5675
|
+
return {
|
|
5676
|
+
content: [{
|
|
5677
|
+
type: "text",
|
|
5678
|
+
text: `[QUALITY ERROR] surface ownership violation: ${detail}. ` + `Each surfaceId is bound to the first agent that pushed it. ` + `You cannot push or delete surfaces owned by other agents. ` + `If you need to coordinate, delegate via sessions_send (internal) to the owning agent. ` + `If you are renaming/retiring a widget, push with a new surfaceId under your own namespace.`
|
|
5679
|
+
}]
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
if (agentId === "agentlife-builder") {
|
|
5683
|
+
const CONFIRMATION_SUFFIXES = /-(?:created|ready|built|setup|done|welcome)$/i;
|
|
5684
|
+
const confirmationViolations = [];
|
|
5685
|
+
const stripped = [];
|
|
5686
|
+
for (const block of filteredBlocks) {
|
|
5687
|
+
const deleteMatch = block.trim().match(/^delete\s+(\S+)/);
|
|
5688
|
+
const surfaceMatch = block.match(/^surface\s+(\S+)/m);
|
|
5689
|
+
const sid = deleteMatch?.[1] ?? surfaceMatch?.[1] ?? null;
|
|
5690
|
+
if (!sid || deleteMatch) {
|
|
5691
|
+
stripped.push(block);
|
|
5692
|
+
continue;
|
|
5693
|
+
}
|
|
5694
|
+
if (sid === "agent-created" || CONFIRMATION_SUFFIXES.test(sid)) {
|
|
5695
|
+
confirmationViolations.push(sid);
|
|
5696
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
5697
|
+
data: JSON.stringify({ issue: "builder_confirmation_widget", surfaceId: sid })
|
|
5698
|
+
});
|
|
5699
|
+
continue;
|
|
5700
|
+
}
|
|
5701
|
+
stripped.push(block);
|
|
5702
|
+
}
|
|
5703
|
+
if (confirmationViolations.length > 0 && stripped.length === 0) {
|
|
5704
|
+
return {
|
|
5705
|
+
content: [{
|
|
5706
|
+
type: "text",
|
|
5707
|
+
text: `[QUALITY ERROR] builder pushed confirmation widget(s): ${confirmationViolations.join(", ")}. ` + `Per BUILDER_AGENTS_MD, the builder does NOT push an "agent ready / created" widget. ` + `The plugin pushes a deterministic {agentId}-intro as the confirmation the moment ` + `agentlife.createAgent returns. Delete your loading widget and emit 'done'.`
|
|
5708
|
+
}]
|
|
5709
|
+
};
|
|
5710
|
+
}
|
|
5711
|
+
if (confirmationViolations.length > 0) {
|
|
5712
|
+
filteredBlocks.length = 0;
|
|
5713
|
+
filteredBlocks.push(...stripped);
|
|
5714
|
+
console.log("[agentlife] stripped %d builder confirmation widget(s): %s", confirmationViolations.length, confirmationViolations.join(", "));
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
const incomingInputIds = new Set;
|
|
5718
|
+
for (const block of filteredBlocks) {
|
|
5719
|
+
const headerLine = block.match(/^surface\s+.+$/m)?.[0] ?? "";
|
|
5720
|
+
if (/\binput\b/.test(headerLine)) {
|
|
5721
|
+
const sid = headerLine.match(/^surface\s+(\S+)/)?.[1];
|
|
5722
|
+
if (sid)
|
|
5723
|
+
incomingInputIds.add(sid);
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
const autoDeletedInputIds = [];
|
|
5727
|
+
if (incomingInputIds.size > 0 && state2.surfaceDb) {
|
|
5728
|
+
for (const [existingId, meta] of state2.surfaceDb.entries()) {
|
|
5729
|
+
if (incomingInputIds.has(existingId))
|
|
5730
|
+
continue;
|
|
5731
|
+
const existingHeader = meta.lines[0] ?? "";
|
|
5732
|
+
if (/\binput\b/.test(existingHeader)) {
|
|
5733
|
+
autoDeletedInputIds.push(existingId);
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
if (autoDeletedInputIds.length > 0) {
|
|
5737
|
+
console.log("[agentlife] auto-deleting %d stale input surface(s) to make room for %s: %s", autoDeletedInputIds.length, [...incomingInputIds].join(","), autoDeletedInputIds.join(","));
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
const prelude = autoDeletedInputIds.map((sid) => `delete ${sid}`);
|
|
5741
|
+
const filteredDsl = [...prelude, ...filteredBlocks].join(`
|
|
5742
|
+
---
|
|
5743
|
+
`);
|
|
5744
|
+
pushSurfaceIds.length = 0;
|
|
5745
|
+
newSurfaceIds.length = 0;
|
|
5746
|
+
for (const block of filteredBlocks) {
|
|
5747
|
+
const match = block.match(/^surface\s+(\S+)/m);
|
|
5748
|
+
if (match) {
|
|
5749
|
+
pushSurfaceIds.push(match[1]);
|
|
5750
|
+
if (!state2.surfaceDb?.has(match[1]))
|
|
5751
|
+
newSurfaceIds.push(match[1]);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
const dslResults = processDslBlock(state2, filteredDsl);
|
|
6351
5755
|
if (state2.surfaceDb) {
|
|
6352
5756
|
for (const sid of pushSurfaceIds) {
|
|
6353
5757
|
if (agentId)
|
|
@@ -6364,6 +5768,9 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6364
5768
|
broadcastDelete(deleteMatch[1]);
|
|
6365
5769
|
}
|
|
6366
5770
|
}
|
|
5771
|
+
for (const sid of autoDeletedInputIds) {
|
|
5772
|
+
broadcastDelete(sid);
|
|
5773
|
+
}
|
|
6367
5774
|
for (const sid of newSurfaceIds) {
|
|
6368
5775
|
const meta = state2.surfaceDb?.get(sid);
|
|
6369
5776
|
if (!meta)
|
|
@@ -6423,6 +5830,9 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6423
5830
|
const unknownKeywordHits = [];
|
|
6424
5831
|
const metadataNoColonHits = [];
|
|
6425
5832
|
const missingStructureHits = [];
|
|
5833
|
+
const invalidInputActionHits = [];
|
|
5834
|
+
const unknownAttrHits = [];
|
|
5835
|
+
const invalidEnumHits = [];
|
|
6426
5836
|
for (const result of dslResults) {
|
|
6427
5837
|
if (result.unknownKeywords?.length) {
|
|
6428
5838
|
unknownKeywordHits.push({ sid: result.surfaceId, keywords: result.unknownKeywords });
|
|
@@ -6453,6 +5863,36 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6453
5863
|
})
|
|
6454
5864
|
});
|
|
6455
5865
|
}
|
|
5866
|
+
if (result.invalidInputActions?.length) {
|
|
5867
|
+
invalidInputActionHits.push({ sid: result.surfaceId, actions: result.invalidInputActions });
|
|
5868
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
5869
|
+
data: JSON.stringify({
|
|
5870
|
+
issue: "invalid_input_action",
|
|
5871
|
+
surfaceId: result.surfaceId,
|
|
5872
|
+
actions: result.invalidInputActions
|
|
5873
|
+
})
|
|
5874
|
+
});
|
|
5875
|
+
}
|
|
5876
|
+
if (result.unknownAttrs?.length) {
|
|
5877
|
+
unknownAttrHits.push({ sid: result.surfaceId, attrs: result.unknownAttrs });
|
|
5878
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
5879
|
+
data: JSON.stringify({
|
|
5880
|
+
issue: "unknown_component_attr",
|
|
5881
|
+
surfaceId: result.surfaceId,
|
|
5882
|
+
attrs: result.unknownAttrs
|
|
5883
|
+
})
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
if (result.invalidEnumValues?.length) {
|
|
5887
|
+
invalidEnumHits.push({ sid: result.surfaceId, entries: result.invalidEnumValues });
|
|
5888
|
+
recordActivity(state2, "quality_warning", sessionKey, agentId, {
|
|
5889
|
+
data: JSON.stringify({
|
|
5890
|
+
issue: "invalid_enum_value",
|
|
5891
|
+
surfaceId: result.surfaceId,
|
|
5892
|
+
entries: result.invalidEnumValues
|
|
5893
|
+
})
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
6456
5896
|
}
|
|
6457
5897
|
const summary = pushSurfaceIds.length === 1 ? `Widget ${pushSurfaceIds[0]} updated` : `${pushSurfaceIds.length} widgets updated`;
|
|
6458
5898
|
const errors = [];
|
|
@@ -6473,6 +5913,22 @@ function registerWidgetPushTool(api, state2) {
|
|
|
6473
5913
|
if (missingStructureHits.length > 0) {
|
|
6474
5914
|
errors.push(`[QUALITY ERROR] ${missingStructureHits.join(", ")}: missing \`card\` wrapper. ` + `Every widget must be nested: \`surface <id> size=…\\n card\\n column\\n <children>\`. ` + `Content placed directly under surface (no card) cannot render.`);
|
|
6475
5915
|
}
|
|
5916
|
+
if (invalidInputActionHits.length > 0) {
|
|
5917
|
+
const detail = invalidInputActionHits.map((h) => `${h.sid}=[${h.actions.map((a) => `"${a.label}" action=${a.action}`).join(", ")}]`).join("; ");
|
|
5918
|
+
errors.push(`[QUALITY ERROR] input surface buttons must use a supported action value: ${detail}. ` + `Inside an \`input\` surface the only valid action values are \`choice\` / \`select\` (numbered single-select list) ` + `and \`toggle\` / \`check\` / \`multichoice\` (checkbox multi-select list). ` + `Any other value (e.g. \`action=domain\`) renders as a plain outlined button and breaks the input-bar UX. ` + `Rewrite the buttons to use \`action=choice\` (pick one) or \`action=toggle\` (pick multiple). ` + `Use custom action names only on regular dashboard widgets, not on input surfaces.`);
|
|
5919
|
+
}
|
|
5920
|
+
if (ownershipViolations.length > 0) {
|
|
5921
|
+
const detail = ownershipViolations.map((v) => `${v.surfaceId} (owner=${v.owner}, attempt=${v.action})`).join("; ");
|
|
5922
|
+
errors.push(`[QUALITY ERROR] surface ownership violation: ${detail}. ` + `Each surfaceId is bound to the first agent that pushed it. ` + `You cannot push or delete surfaces owned by other agents. ` + `The offending block(s) were skipped — the original owner's content is preserved. ` + `If you need to coordinate, delegate via sessions_send (internal) to the owning agent. ` + `If you are emitting a new widget, pick a surfaceId under your own namespace.`);
|
|
5923
|
+
}
|
|
5924
|
+
if (unknownAttrHits.length > 0) {
|
|
5925
|
+
const detail = unknownAttrHits.map((h) => `${h.sid}=[${h.attrs.join(",")}]`).join("; ");
|
|
5926
|
+
errors.push(`[QUALITY ERROR] unknown component attribute(s): ${detail}. ` + `The client parser recognises a closed set of attributes per component — anything else is either silently dropped or renders identically to a plain component (no visual effect). ` + `The notation \`component.attr\` means an unknown \`key=value\` pair; \`component:token\` means an unknown bare flag (e.g. button.primary has no visual effect, drop it). ` + `Remove the attribute, or confirm it against the current DSL grammar before re-emitting.`);
|
|
5927
|
+
}
|
|
5928
|
+
if (invalidEnumHits.length > 0) {
|
|
5929
|
+
const detail = invalidEnumHits.map((h) => h.entries.map((e) => `${h.sid}:${e.attr}=${e.value} (allowed: ${e.allowed.join("|")})`).join(", ")).join("; ");
|
|
5930
|
+
errors.push(`[QUALITY ERROR] enum value outside the allowed set: ${detail}. ` + `Pick one of the allowed values listed above for each attribute. Any other value is silently coerced to the default by the parser.`);
|
|
5931
|
+
}
|
|
6476
5932
|
if (errors.length > 0) {
|
|
6477
5933
|
return { content: [{ type: "text", text: `${summary}
|
|
6478
5934
|
|
|
@@ -6567,53 +6023,12 @@ function extractWidgetText(meta) {
|
|
|
6567
6023
|
}
|
|
6568
6024
|
|
|
6569
6025
|
// hooks/prompt-state.ts
|
|
6570
|
-
var lastEmittedAnswerByKey = new Map;
|
|
6571
|
-
function lastUserMessage(messages) {
|
|
6572
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
6573
|
-
const m = messages[i];
|
|
6574
|
-
if (m?.role !== "user")
|
|
6575
|
-
continue;
|
|
6576
|
-
if (typeof m.content === "string")
|
|
6577
|
-
return m.content;
|
|
6578
|
-
if (Array.isArray(m.content)) {
|
|
6579
|
-
const parts = [];
|
|
6580
|
-
for (const block of m.content) {
|
|
6581
|
-
if (typeof block === "string")
|
|
6582
|
-
parts.push(block);
|
|
6583
|
-
else if (block?.type === "text" && typeof block.text === "string")
|
|
6584
|
-
parts.push(block.text);
|
|
6585
|
-
}
|
|
6586
|
-
return parts.join(`
|
|
6587
|
-
`);
|
|
6588
|
-
}
|
|
6589
|
-
}
|
|
6590
|
-
return "";
|
|
6591
|
-
}
|
|
6592
6026
|
function registerPromptStateHook(api, state2) {
|
|
6593
6027
|
api.on("before_prompt_build", (event, ctx) => {
|
|
6594
6028
|
if (state2.disabled)
|
|
6595
6029
|
return;
|
|
6596
6030
|
if (!isAgentlifeSession(ctx?.sessionKey))
|
|
6597
6031
|
return;
|
|
6598
|
-
const sessionKey = ctx?.sessionKey;
|
|
6599
|
-
if (sessionKey) {
|
|
6600
|
-
const messages = event?.messages ?? [];
|
|
6601
|
-
const latest = lastUserMessage(messages);
|
|
6602
|
-
if (latest && lastEmittedAnswerByKey.get(sessionKey) !== latest) {
|
|
6603
|
-
const actionMatch = latest.match(/\[action:\S+\]\s*surfaceId=awaiting-dream-input\b/);
|
|
6604
|
-
if (actionMatch) {
|
|
6605
|
-
const labelMatch = latest.match(/\blabel=([^\n]+)/);
|
|
6606
|
-
const valueMatch = latest.match(/\bvalue=([^\n]+)/);
|
|
6607
|
-
const answer = (labelMatch?.[1] ?? valueMatch?.[1] ?? "").trim();
|
|
6608
|
-
lastEmittedAnswerByKey.set(sessionKey, latest);
|
|
6609
|
-
try {
|
|
6610
|
-
emitTrigger(state2, "userAnswered", { surfaceId: "awaiting-dream-input", answer });
|
|
6611
|
-
} catch (e) {
|
|
6612
|
-
console.warn("[agentlife] cold-start emit userAnswered (prompt-build) failed: %s", e?.message);
|
|
6613
|
-
}
|
|
6614
|
-
}
|
|
6615
|
-
}
|
|
6616
|
-
}
|
|
6617
6032
|
const agentId = ctx?.agentId;
|
|
6618
6033
|
const isOrchestrator = agentId === "agentlife";
|
|
6619
6034
|
if (isOrchestrator)
|
|
@@ -6633,8 +6048,8 @@ function registerPromptStateHook(api, state2) {
|
|
|
6633
6048
|
}
|
|
6634
6049
|
|
|
6635
6050
|
// gateway/agents.ts
|
|
6636
|
-
import * as
|
|
6637
|
-
import * as
|
|
6051
|
+
import * as fs8 from "node:fs";
|
|
6052
|
+
import * as path11 from "node:path";
|
|
6638
6053
|
function registerAgentGateway(api, state2) {
|
|
6639
6054
|
api.registerGatewayMethod("agentlife.createAgent", async ({ params, respond }) => {
|
|
6640
6055
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
@@ -6726,9 +6141,9 @@ function registerAgentGateway(api, state2) {
|
|
|
6726
6141
|
const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6727
6142
|
if (!provisionedIds.has(id)) {
|
|
6728
6143
|
try {
|
|
6729
|
-
|
|
6144
|
+
renderAgentWidget(state2, api.runtime, id, console.log);
|
|
6730
6145
|
} catch (e) {
|
|
6731
|
-
console.warn("[agentlife]
|
|
6146
|
+
console.warn("[agentlife] renderAgentWidget failed for %s: %s", id, e?.message);
|
|
6732
6147
|
}
|
|
6733
6148
|
}
|
|
6734
6149
|
}
|
|
@@ -6794,10 +6209,10 @@ function registerAgentGateway(api, state2) {
|
|
|
6794
6209
|
state2.agentDbs.delete(id);
|
|
6795
6210
|
}
|
|
6796
6211
|
if (state2.dbBaseDir) {
|
|
6797
|
-
const dbPath =
|
|
6212
|
+
const dbPath = path11.join(state2.dbBaseDir, `${id}.db`);
|
|
6798
6213
|
try {
|
|
6799
|
-
if (
|
|
6800
|
-
|
|
6214
|
+
if (fs8.existsSync(dbPath)) {
|
|
6215
|
+
fs8.unlinkSync(dbPath);
|
|
6801
6216
|
cleanup.agentDbDeleted = true;
|
|
6802
6217
|
}
|
|
6803
6218
|
} catch (e) {
|
|
@@ -6823,17 +6238,6 @@ function registerAgentGateway(api, state2) {
|
|
|
6823
6238
|
await saveRegistryToDisk(state2);
|
|
6824
6239
|
}
|
|
6825
6240
|
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);
|
|
6826
|
-
try {
|
|
6827
|
-
const provisionedIds2 = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
6828
|
-
let remaining = 0;
|
|
6829
|
-
for (const aid of state2.agentRegistry.keys()) {
|
|
6830
|
-
if (!provisionedIds2.has(aid))
|
|
6831
|
-
remaining++;
|
|
6832
|
-
}
|
|
6833
|
-
emitTrigger(state2, "agentDeleted", { agentId: id, remaining });
|
|
6834
|
-
} catch (e) {
|
|
6835
|
-
console.warn("[agentlife] cold-start emit agentDeleted failed: %s", e?.message);
|
|
6836
|
-
}
|
|
6837
6241
|
respond(true, { status: "deleted", id, removedFromConfig, removedFromRegistry, cleanup });
|
|
6838
6242
|
});
|
|
6839
6243
|
api.registerGatewayMethod("agentlife.agents", ({ respond }) => {
|
|
@@ -7143,21 +6547,13 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7143
6547
|
respond(true, { surfaces: [] });
|
|
7144
6548
|
return;
|
|
7145
6549
|
}
|
|
7146
|
-
const visibility = params?.visibility === "paywall" ? "paywall" : "dashboard";
|
|
7147
|
-
const FLOW_INPUT_IDS = new Set(["onboarding-domain", "awaiting-dream-input"]);
|
|
7148
|
-
const isFlowInput = (id) => FLOW_INPUT_IDS.has(id) || id.startsWith("warmup-") || id.startsWith("dismiss-alt-");
|
|
7149
6550
|
for (const [surfaceId, meta] of state2.surfaceDb.entries()) {
|
|
7150
6551
|
if (isExpired(meta, now))
|
|
7151
6552
|
continue;
|
|
7152
6553
|
const headerLine = meta.lines[0] ?? "";
|
|
7153
6554
|
const isInput = /\binput\b/.test(headerLine);
|
|
7154
|
-
if (isInput
|
|
6555
|
+
if (isInput)
|
|
7155
6556
|
continue;
|
|
7156
|
-
if (visibility === "dashboard" && surfaceId.startsWith("vision-")) {
|
|
7157
|
-
const visible = state2.surfaceDb.getDashboardVisible(surfaceId);
|
|
7158
|
-
if (!visible)
|
|
7159
|
-
continue;
|
|
7160
|
-
}
|
|
7161
6557
|
if (meta.lines.length > 0) {
|
|
7162
6558
|
surfaceEntries.push({ surfaceId, dsl: meta.lines.join(`
|
|
7163
6559
|
`) });
|
|
@@ -7295,12 +6691,7 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7295
6691
|
automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
|
|
7296
6692
|
} catch {}
|
|
7297
6693
|
guidedDismissSent.delete(surfaceId);
|
|
7298
|
-
|
|
7299
|
-
if (isVision) {
|
|
7300
|
-
state2.surfaceDb.setDashboardVisible(surfaceId, false);
|
|
7301
|
-
} else {
|
|
7302
|
-
state2.surfaceDb.delete(surfaceId);
|
|
7303
|
-
}
|
|
6694
|
+
state2.surfaceDb.delete(surfaceId);
|
|
7304
6695
|
broadcastDelete(surfaceId);
|
|
7305
6696
|
try {
|
|
7306
6697
|
enqueueCleanupTasks(state2, surfaceId, agentId, cronId, automations);
|
|
@@ -7308,7 +6699,7 @@ function registerSurfacesGateway(api, state2) {
|
|
|
7308
6699
|
console.error("[agentlife] dismiss: failed to enqueue cleanup tasks for %s: %s", surfaceId, err?.message);
|
|
7309
6700
|
}
|
|
7310
6701
|
const taskCount = (cronId ? 1 : 0) + (agentId ? 1 : 0);
|
|
7311
|
-
console.log("[agentlife] dismiss: %s
|
|
6702
|
+
console.log("[agentlife] dismiss: %s deleted, %d cleanup tasks enqueued (reason=%s)", surfaceId, taskCount, reason ?? "none");
|
|
7312
6703
|
respond(true, { surfaceId, dismissed: true });
|
|
7313
6704
|
processCleanupTasks(state2, surfaceId).catch((e) => console.warn("[agentlife] dismiss cleanup processor error:", e?.message));
|
|
7314
6705
|
}, { scope: "operator.write" });
|
|
@@ -7460,12 +6851,12 @@ function registerAutomationsGateway(api, state2) {
|
|
|
7460
6851
|
}
|
|
7461
6852
|
|
|
7462
6853
|
// gateway/admin.ts
|
|
7463
|
-
import * as
|
|
6854
|
+
import * as fs9 from "node:fs/promises";
|
|
7464
6855
|
import * as fsSync4 from "node:fs";
|
|
7465
|
-
import * as
|
|
7466
|
-
import * as
|
|
6856
|
+
import * as os7 from "node:os";
|
|
6857
|
+
import * as path12 from "node:path";
|
|
7467
6858
|
function pluginConfigPath() {
|
|
7468
|
-
return
|
|
6859
|
+
return path12.join(os7.homedir(), ".openclaw", "agentlife", "plugin-config.json");
|
|
7469
6860
|
}
|
|
7470
6861
|
function readPluginConfig() {
|
|
7471
6862
|
try {
|
|
@@ -7475,7 +6866,7 @@ function readPluginConfig() {
|
|
|
7475
6866
|
}
|
|
7476
6867
|
}
|
|
7477
6868
|
function writePluginConfig(config2) {
|
|
7478
|
-
const dir =
|
|
6869
|
+
const dir = path12.dirname(pluginConfigPath());
|
|
7479
6870
|
fsSync4.mkdirSync(dir, { recursive: true });
|
|
7480
6871
|
fsSync4.writeFileSync(pluginConfigPath(), JSON.stringify(config2, null, 2));
|
|
7481
6872
|
}
|
|
@@ -7574,7 +6965,7 @@ function registerAdminGateway(api, state2) {
|
|
|
7574
6965
|
api.registerGatewayMethod("agentlife.uninstall", async ({ respond }) => {
|
|
7575
6966
|
try {
|
|
7576
6967
|
const cleaned = [];
|
|
7577
|
-
const baseDir = state2.agentlifeStateDir ??
|
|
6968
|
+
const baseDir = state2.agentlifeStateDir ?? path12.join(os7.homedir(), ".openclaw", "agentlife");
|
|
7578
6969
|
const identity = loadDeviceIdentity();
|
|
7579
6970
|
if (!identity) {
|
|
7580
6971
|
cleaned.push("server deprovision skipped (no device identity)");
|
|
@@ -7639,20 +7030,19 @@ function registerAdminGateway(api, state2) {
|
|
|
7639
7030
|
} catch {
|
|
7640
7031
|
cleaned.push("agent config cleanup skipped");
|
|
7641
7032
|
}
|
|
7642
|
-
const backupPath =
|
|
7033
|
+
const backupPath = path12.join(baseDir, "config-backup.json");
|
|
7643
7034
|
try {
|
|
7644
7035
|
cleaned.push(await restoreConfigFromBackup(api, backupPath));
|
|
7645
7036
|
} catch {
|
|
7646
7037
|
cleaned.push("maxPingPongTurns already at default (no backup found)");
|
|
7647
7038
|
}
|
|
7648
|
-
stopFollowupSystem();
|
|
7039
|
+
stopFollowupSystem(state2);
|
|
7649
7040
|
if (state2.surfaceDb)
|
|
7650
7041
|
state2.surfaceDb.clear();
|
|
7651
7042
|
closeAllDbs(state2);
|
|
7652
7043
|
state2.agentRegistry.clear();
|
|
7653
7044
|
state2.usageAccumulator.clear();
|
|
7654
7045
|
state2.sessionBootstrapSnapshots.clear();
|
|
7655
|
-
state2.pendingFollowups.clear();
|
|
7656
7046
|
state2.surfaceDb = null;
|
|
7657
7047
|
state2.disabled = true;
|
|
7658
7048
|
cleaned.push("stopped runtime (poller, DBs, in-memory state)");
|
|
@@ -7662,35 +7052,35 @@ function registerAdminGateway(api, state2) {
|
|
|
7662
7052
|
dbPath,
|
|
7663
7053
|
dbPath ? `${dbPath}-wal` : null,
|
|
7664
7054
|
dbPath ? `${dbPath}-shm` : null,
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7055
|
+
path12.join(baseDir, "config-backup.json"),
|
|
7056
|
+
path12.join(baseDir, "notification-config.json"),
|
|
7057
|
+
path12.join(baseDir, "canvas-node-identity.json")
|
|
7668
7058
|
].filter(Boolean);
|
|
7669
7059
|
for (const fp of stateFiles) {
|
|
7670
7060
|
try {
|
|
7671
|
-
await
|
|
7672
|
-
cleaned.push(`deleted ${
|
|
7061
|
+
await fs9.unlink(fp);
|
|
7062
|
+
cleaned.push(`deleted ${path12.basename(fp)}`);
|
|
7673
7063
|
} catch {}
|
|
7674
7064
|
}
|
|
7675
7065
|
if (state2.dbBaseDir) {
|
|
7676
7066
|
try {
|
|
7677
|
-
await
|
|
7067
|
+
await fs9.rm(state2.dbBaseDir, { recursive: true, force: true });
|
|
7678
7068
|
cleaned.push("deleted agent databases");
|
|
7679
7069
|
} catch {}
|
|
7680
7070
|
}
|
|
7681
7071
|
for (const agent of PROVISIONED_AGENTS) {
|
|
7682
7072
|
if (agent.existingAgent)
|
|
7683
7073
|
continue;
|
|
7684
|
-
const wsDir = agent.workspaceDir ??
|
|
7074
|
+
const wsDir = agent.workspaceDir ?? path12.join(os7.homedir(), ".openclaw", `workspace-${agent.id}`);
|
|
7685
7075
|
try {
|
|
7686
|
-
await
|
|
7076
|
+
await fs9.rm(wsDir, { recursive: true, force: true });
|
|
7687
7077
|
cleaned.push(`deleted workspace ${agent.id}`);
|
|
7688
7078
|
} catch {}
|
|
7689
7079
|
}
|
|
7690
7080
|
try {
|
|
7691
|
-
const remaining = await
|
|
7081
|
+
const remaining = await fs9.readdir(baseDir);
|
|
7692
7082
|
if (remaining.length === 0) {
|
|
7693
|
-
await
|
|
7083
|
+
await fs9.rmdir(baseDir);
|
|
7694
7084
|
cleaned.push("deleted agentlife state directory");
|
|
7695
7085
|
}
|
|
7696
7086
|
} catch {}
|
|
@@ -7933,31 +7323,60 @@ function parseOffset2(offset) {
|
|
|
7933
7323
|
}
|
|
7934
7324
|
|
|
7935
7325
|
// gateway/providers.ts
|
|
7936
|
-
import * as fs11 from "node:fs";
|
|
7937
|
-
import * as os9 from "node:os";
|
|
7938
|
-
import * as path14 from "node:path";
|
|
7939
7326
|
import {
|
|
7940
7327
|
upsertApiKeyProfile,
|
|
7328
|
+
upsertAuthProfile,
|
|
7329
|
+
buildApiKeyCredential,
|
|
7941
7330
|
applyAuthProfileConfig
|
|
7942
7331
|
} from "openclaw/plugin-sdk/provider-auth-api-key";
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7332
|
+
|
|
7333
|
+
// gateway/config-utils.ts
|
|
7334
|
+
import * as fs10 from "node:fs";
|
|
7335
|
+
import * as os8 from "node:os";
|
|
7336
|
+
import * as path13 from "node:path";
|
|
7947
7337
|
function configPath() {
|
|
7948
|
-
return
|
|
7338
|
+
return path13.join(os8.homedir(), ".openclaw", "openclaw.json");
|
|
7949
7339
|
}
|
|
7950
7340
|
function readConfig() {
|
|
7951
7341
|
try {
|
|
7952
|
-
return JSON.parse(
|
|
7342
|
+
return JSON.parse(fs10.readFileSync(configPath(), "utf-8"));
|
|
7953
7343
|
} catch {
|
|
7954
7344
|
return {};
|
|
7955
7345
|
}
|
|
7956
7346
|
}
|
|
7957
7347
|
function writeConfig(cfg) {
|
|
7958
|
-
|
|
7348
|
+
fs10.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + `
|
|
7959
7349
|
`, "utf-8");
|
|
7960
7350
|
}
|
|
7351
|
+
|
|
7352
|
+
// gateway/providers.ts
|
|
7353
|
+
import * as fs11 from "node:fs";
|
|
7354
|
+
import * as os9 from "node:os";
|
|
7355
|
+
import * as path14 from "node:path";
|
|
7356
|
+
function siblingAgentDirs() {
|
|
7357
|
+
const root = path14.join(os9.homedir(), ".openclaw", "agents");
|
|
7358
|
+
let entries;
|
|
7359
|
+
try {
|
|
7360
|
+
entries = fs11.readdirSync(root, { withFileTypes: true });
|
|
7361
|
+
} catch {
|
|
7362
|
+
return [];
|
|
7363
|
+
}
|
|
7364
|
+
const out = [];
|
|
7365
|
+
for (const entry of entries) {
|
|
7366
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
7367
|
+
continue;
|
|
7368
|
+
const dir = path14.join(root, entry.name, "agent");
|
|
7369
|
+
try {
|
|
7370
|
+
if (fs11.statSync(dir).isDirectory())
|
|
7371
|
+
out.push(dir);
|
|
7372
|
+
} catch {}
|
|
7373
|
+
}
|
|
7374
|
+
return out;
|
|
7375
|
+
}
|
|
7376
|
+
var catalogCache = null;
|
|
7377
|
+
var catalogPromise = null;
|
|
7378
|
+
var modelsCache = null;
|
|
7379
|
+
var modelsPromise = null;
|
|
7961
7380
|
async function fetchCatalog(run) {
|
|
7962
7381
|
const result = await run(["openclaw", "infer", "model", "providers", "--json"], { timeoutMs: 60000 });
|
|
7963
7382
|
if ((result?.code ?? 0) !== 0)
|
|
@@ -8016,6 +7435,37 @@ async function ensureModels(run) {
|
|
|
8016
7435
|
}
|
|
8017
7436
|
return modelsPromise;
|
|
8018
7437
|
}
|
|
7438
|
+
function readOAuthExpiries() {
|
|
7439
|
+
const result = new Map;
|
|
7440
|
+
const fs12 = __require("node:fs");
|
|
7441
|
+
const path15 = __require("node:path");
|
|
7442
|
+
const os10 = __require("node:os");
|
|
7443
|
+
const agentsDir = path15.join(os10.homedir(), ".openclaw", "agents");
|
|
7444
|
+
let entries;
|
|
7445
|
+
try {
|
|
7446
|
+
entries = fs12.readdirSync(agentsDir);
|
|
7447
|
+
} catch {
|
|
7448
|
+
return result;
|
|
7449
|
+
}
|
|
7450
|
+
for (const name of entries) {
|
|
7451
|
+
const filePath = path15.join(agentsDir, name, "agent", "auth-profiles.json");
|
|
7452
|
+
try {
|
|
7453
|
+
const raw = fs12.readFileSync(filePath, "utf-8");
|
|
7454
|
+
const data = JSON.parse(raw);
|
|
7455
|
+
const profiles = data?.profiles ?? {};
|
|
7456
|
+
for (const entry of Object.values(profiles)) {
|
|
7457
|
+
const p = entry;
|
|
7458
|
+
if (p?.type === "oauth" && typeof p.provider === "string" && typeof p.expires === "number") {
|
|
7459
|
+
const prev = result.get(p.provider);
|
|
7460
|
+
if (!prev || p.expires > prev.expiresAt) {
|
|
7461
|
+
result.set(p.provider, { expiresAt: p.expires });
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
} catch {}
|
|
7466
|
+
}
|
|
7467
|
+
return result;
|
|
7468
|
+
}
|
|
8019
7469
|
function authenticatedProviders() {
|
|
8020
7470
|
const cfg = readConfig();
|
|
8021
7471
|
const profiles = cfg?.auth?.profiles ?? {};
|
|
@@ -8135,6 +7585,12 @@ function registerProvidersGateway(api, state2) {
|
|
|
8135
7585
|
} catch {}
|
|
8136
7586
|
try {
|
|
8137
7587
|
const profileId = upsertApiKeyProfile({ provider, input: apiKey });
|
|
7588
|
+
const credential = buildApiKeyCredential(provider, apiKey);
|
|
7589
|
+
for (const agentDir of siblingAgentDirs()) {
|
|
7590
|
+
try {
|
|
7591
|
+
upsertAuthProfile({ profileId, credential, agentDir });
|
|
7592
|
+
} catch {}
|
|
7593
|
+
}
|
|
8138
7594
|
const cfg = readConfig();
|
|
8139
7595
|
const nextCfg = applyAuthProfileConfig(cfg, {
|
|
8140
7596
|
profileId,
|
|
@@ -8165,6 +7621,29 @@ function registerProvidersGateway(api, state2) {
|
|
|
8165
7621
|
delete cfg.auth.order[provider];
|
|
8166
7622
|
}
|
|
8167
7623
|
writeConfig(cfg);
|
|
7624
|
+
for (const agentDir of siblingAgentDirs()) {
|
|
7625
|
+
const file = path14.join(agentDir, "auth-profiles.json");
|
|
7626
|
+
try {
|
|
7627
|
+
const raw = fs11.readFileSync(file, "utf-8");
|
|
7628
|
+
const data = JSON.parse(raw);
|
|
7629
|
+
const agentProfiles = data?.profiles;
|
|
7630
|
+
if (!agentProfiles || typeof agentProfiles !== "object")
|
|
7631
|
+
continue;
|
|
7632
|
+
let changed = false;
|
|
7633
|
+
for (const [id, entry] of Object.entries(agentProfiles)) {
|
|
7634
|
+
if (entry?.provider === provider) {
|
|
7635
|
+
delete agentProfiles[id];
|
|
7636
|
+
changed = true;
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
if (data?.order?.[provider]) {
|
|
7640
|
+
delete data.order[provider];
|
|
7641
|
+
changed = true;
|
|
7642
|
+
}
|
|
7643
|
+
if (changed)
|
|
7644
|
+
fs11.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
|
7645
|
+
} catch {}
|
|
7646
|
+
}
|
|
8168
7647
|
respond(true, { provider, configured: false });
|
|
8169
7648
|
} catch (err) {
|
|
8170
7649
|
respond(false, { error: err?.message ?? String(err) });
|
|
@@ -8203,6 +7682,36 @@ function registerProvidersGateway(api, state2) {
|
|
|
8203
7682
|
respond(false, { error: err?.message ?? String(err) });
|
|
8204
7683
|
}
|
|
8205
7684
|
}, { scope: "operator.read" });
|
|
7685
|
+
api.registerGatewayMethod("agentlife.providers.health", ({ respond }) => {
|
|
7686
|
+
try {
|
|
7687
|
+
const cfg = readConfig();
|
|
7688
|
+
const profiles = cfg?.auth?.profiles ?? {};
|
|
7689
|
+
const oauthInfo = readOAuthExpiries();
|
|
7690
|
+
const probes = [];
|
|
7691
|
+
for (const entry of Object.values(profiles)) {
|
|
7692
|
+
const p = entry;
|
|
7693
|
+
if (typeof p?.provider !== "string")
|
|
7694
|
+
continue;
|
|
7695
|
+
const provider = p.provider;
|
|
7696
|
+
const probe2 = {
|
|
7697
|
+
provider,
|
|
7698
|
+
configured: true,
|
|
7699
|
+
supported: true
|
|
7700
|
+
};
|
|
7701
|
+
if (p.mode === "oauth") {
|
|
7702
|
+
const info = oauthInfo.get(provider);
|
|
7703
|
+
if (info?.expiresAt) {
|
|
7704
|
+
probe2.expiresAt = info.expiresAt;
|
|
7705
|
+
probe2.oauthExpired = info.expiresAt < Date.now();
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
probes.push(probe2);
|
|
7709
|
+
}
|
|
7710
|
+
respond(true, { probes });
|
|
7711
|
+
} catch (err) {
|
|
7712
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7713
|
+
}
|
|
7714
|
+
}, { scope: "operator.read" });
|
|
8206
7715
|
api.registerGatewayMethod("agentlife.providers.loginCodex", async ({ respond }) => {
|
|
8207
7716
|
if (!run)
|
|
8208
7717
|
return respond(false, { error: "runCommand unavailable" });
|
|
@@ -8221,6 +7730,159 @@ ${result?.stderr ?? ""}`;
|
|
|
8221
7730
|
}, { scope: "operator.write" });
|
|
8222
7731
|
}
|
|
8223
7732
|
|
|
7733
|
+
// gateway/models-config.ts
|
|
7734
|
+
import * as fs12 from "node:fs";
|
|
7735
|
+
import * as os10 from "node:os";
|
|
7736
|
+
import * as path15 from "node:path";
|
|
7737
|
+
function pluginConfigPath2() {
|
|
7738
|
+
return path15.join(os10.homedir(), ".openclaw", "agentlife", "plugin-config.json");
|
|
7739
|
+
}
|
|
7740
|
+
function readPluginConfig2() {
|
|
7741
|
+
try {
|
|
7742
|
+
return JSON.parse(fs12.readFileSync(pluginConfigPath2(), "utf-8"));
|
|
7743
|
+
} catch {
|
|
7744
|
+
return {};
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
function writePluginConfig2(cfg) {
|
|
7748
|
+
fs12.mkdirSync(path15.dirname(pluginConfigPath2()), { recursive: true });
|
|
7749
|
+
fs12.writeFileSync(pluginConfigPath2(), JSON.stringify(cfg, null, 2) + `
|
|
7750
|
+
`, "utf-8");
|
|
7751
|
+
}
|
|
7752
|
+
function registerModelsConfigGateway(api) {
|
|
7753
|
+
api.registerGatewayMethod("agentlife.models.defaults.get", ({ respond }) => {
|
|
7754
|
+
try {
|
|
7755
|
+
const cfg = readConfig();
|
|
7756
|
+
const m = cfg?.agents?.defaults?.model ?? {};
|
|
7757
|
+
const pluginCfg = readPluginConfig2();
|
|
7758
|
+
respond(true, {
|
|
7759
|
+
primary: typeof m.primary === "string" ? m.primary : "",
|
|
7760
|
+
internal: typeof pluginCfg.internalModel === "string" ? pluginCfg.internalModel : "",
|
|
7761
|
+
fallbacks: Array.isArray(m.fallbacks) ? m.fallbacks.filter((x) => typeof x === "string") : []
|
|
7762
|
+
});
|
|
7763
|
+
} catch (err) {
|
|
7764
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7765
|
+
}
|
|
7766
|
+
}, { scope: "operator.read" });
|
|
7767
|
+
api.registerGatewayMethod("agentlife.models.defaults.setPrimary", ({ params, respond }) => {
|
|
7768
|
+
const model = typeof params?.model === "string" ? params.model.trim() : "";
|
|
7769
|
+
if (!model)
|
|
7770
|
+
return respond(false, { error: "model is required" });
|
|
7771
|
+
try {
|
|
7772
|
+
const cfg = readConfig();
|
|
7773
|
+
cfg.agents = cfg.agents ?? {};
|
|
7774
|
+
cfg.agents.defaults = cfg.agents.defaults ?? {};
|
|
7775
|
+
cfg.agents.defaults.model = cfg.agents.defaults.model ?? {};
|
|
7776
|
+
cfg.agents.defaults.model.primary = model;
|
|
7777
|
+
writeConfig(cfg);
|
|
7778
|
+
respond(true, { ok: true });
|
|
7779
|
+
} catch (err) {
|
|
7780
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7781
|
+
}
|
|
7782
|
+
}, { scope: "operator.write" });
|
|
7783
|
+
api.registerGatewayMethod("agentlife.models.defaults.setInternal", ({ params, respond }) => {
|
|
7784
|
+
const model = typeof params?.model === "string" ? params.model.trim() : "";
|
|
7785
|
+
if (!model)
|
|
7786
|
+
return respond(false, { error: "model is required" });
|
|
7787
|
+
try {
|
|
7788
|
+
const cfg = readPluginConfig2();
|
|
7789
|
+
cfg.internalModel = model;
|
|
7790
|
+
writePluginConfig2(cfg);
|
|
7791
|
+
respond(true, { ok: true });
|
|
7792
|
+
} catch (err) {
|
|
7793
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7794
|
+
}
|
|
7795
|
+
}, { scope: "operator.write" });
|
|
7796
|
+
api.registerGatewayMethod("agentlife.models.fallbacks.set", ({ params, respond }) => {
|
|
7797
|
+
const models = Array.isArray(params?.models) ? params.models.filter((x) => typeof x === "string") : null;
|
|
7798
|
+
if (!models)
|
|
7799
|
+
return respond(false, { error: "models must be an array of strings" });
|
|
7800
|
+
try {
|
|
7801
|
+
const cfg = readConfig();
|
|
7802
|
+
cfg.agents = cfg.agents ?? {};
|
|
7803
|
+
cfg.agents.defaults = cfg.agents.defaults ?? {};
|
|
7804
|
+
cfg.agents.defaults.model = cfg.agents.defaults.model ?? {};
|
|
7805
|
+
cfg.agents.defaults.model.fallbacks = models;
|
|
7806
|
+
writeConfig(cfg);
|
|
7807
|
+
respond(true, { ok: true });
|
|
7808
|
+
} catch (err) {
|
|
7809
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7810
|
+
}
|
|
7811
|
+
}, { scope: "operator.write" });
|
|
7812
|
+
api.registerGatewayMethod("agentlife.agents.list", ({ respond }) => {
|
|
7813
|
+
try {
|
|
7814
|
+
const cfg = readConfig();
|
|
7815
|
+
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
7816
|
+
const agents = list.map((a) => ({
|
|
7817
|
+
id: typeof a?.id === "string" ? a.id : null,
|
|
7818
|
+
name: typeof a?.name === "string" ? a.name : null,
|
|
7819
|
+
modelOverride: typeof a?.model === "string" ? a.model : null
|
|
7820
|
+
})).filter((a) => a.id !== null && a.name !== null);
|
|
7821
|
+
respond(true, { agents });
|
|
7822
|
+
} catch (err) {
|
|
7823
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7824
|
+
}
|
|
7825
|
+
}, { scope: "operator.read" });
|
|
7826
|
+
api.registerGatewayMethod("agentlife.agents.setModel", ({ params, respond }) => {
|
|
7827
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
|
7828
|
+
if (!agentId)
|
|
7829
|
+
return respond(false, { error: "agentId is required" });
|
|
7830
|
+
const model = params?.model;
|
|
7831
|
+
if (model !== null && typeof model !== "string") {
|
|
7832
|
+
return respond(false, { error: "model must be string or null" });
|
|
7833
|
+
}
|
|
7834
|
+
try {
|
|
7835
|
+
const cfg = readConfig();
|
|
7836
|
+
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
7837
|
+
const idx = list.findIndex((a) => a?.id === agentId);
|
|
7838
|
+
if (idx < 0)
|
|
7839
|
+
return respond(false, { error: `agent not found: ${agentId}` });
|
|
7840
|
+
if (model === null) {
|
|
7841
|
+
delete list[idx].model;
|
|
7842
|
+
} else {
|
|
7843
|
+
list[idx].model = model.trim();
|
|
7844
|
+
}
|
|
7845
|
+
writeConfig(cfg);
|
|
7846
|
+
respond(true, { ok: true });
|
|
7847
|
+
} catch (err) {
|
|
7848
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7849
|
+
}
|
|
7850
|
+
}, { scope: "operator.write" });
|
|
7851
|
+
api.registerGatewayMethod("agentlife.agents.setModel.bulk", ({ params, respond }) => {
|
|
7852
|
+
const updates = Array.isArray(params?.updates) ? params.updates : null;
|
|
7853
|
+
if (!updates)
|
|
7854
|
+
return respond(false, { error: "updates must be an array" });
|
|
7855
|
+
for (const u of updates) {
|
|
7856
|
+
if (!u || typeof u.agentId !== "string" || !u.agentId.trim()) {
|
|
7857
|
+
return respond(false, { error: "each update needs agentId (string)" });
|
|
7858
|
+
}
|
|
7859
|
+
if (u.model !== null && typeof u.model !== "string") {
|
|
7860
|
+
return respond(false, { error: "each update model must be string or null" });
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
try {
|
|
7864
|
+
const cfg = readConfig();
|
|
7865
|
+
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
7866
|
+
let applied = 0;
|
|
7867
|
+
for (const u of updates) {
|
|
7868
|
+
const idx = list.findIndex((a) => a?.id === u.agentId);
|
|
7869
|
+
if (idx < 0)
|
|
7870
|
+
continue;
|
|
7871
|
+
if (u.model === null) {
|
|
7872
|
+
delete list[idx].model;
|
|
7873
|
+
} else {
|
|
7874
|
+
list[idx].model = u.model.trim();
|
|
7875
|
+
}
|
|
7876
|
+
applied++;
|
|
7877
|
+
}
|
|
7878
|
+
writeConfig(cfg);
|
|
7879
|
+
respond(true, { ok: true, applied });
|
|
7880
|
+
} catch (err) {
|
|
7881
|
+
respond(false, { error: err?.message ?? String(err) });
|
|
7882
|
+
}
|
|
7883
|
+
}, { scope: "operator.write" });
|
|
7884
|
+
}
|
|
7885
|
+
|
|
8224
7886
|
// index.ts
|
|
8225
7887
|
var currentState = null;
|
|
8226
7888
|
var registered = false;
|
|
@@ -8230,7 +7892,7 @@ var stopQualityCheckPoller = null;
|
|
|
8230
7892
|
var stopCloudflared = null;
|
|
8231
7893
|
function resolveInternalModel(api) {
|
|
8232
7894
|
try {
|
|
8233
|
-
const pluginCfgPath =
|
|
7895
|
+
const pluginCfgPath = path16.join(homedir12(), ".openclaw", "agentlife", "plugin-config.json");
|
|
8234
7896
|
try {
|
|
8235
7897
|
const raw = __require("node:fs").readFileSync(pluginCfgPath, "utf-8");
|
|
8236
7898
|
const pluginCfg = JSON.parse(raw);
|
|
@@ -8266,7 +7928,7 @@ function register(api) {
|
|
|
8266
7928
|
return;
|
|
8267
7929
|
}
|
|
8268
7930
|
registered = true;
|
|
8269
|
-
const fallbackDir =
|
|
7931
|
+
const fallbackDir = path16.join(homedir12(), ".openclaw", "agentlife");
|
|
8270
7932
|
const state2 = {
|
|
8271
7933
|
surfaceDb: null,
|
|
8272
7934
|
agentRegistry: new Map,
|
|
@@ -8276,9 +7938,9 @@ function register(api) {
|
|
|
8276
7938
|
agentDbs: new Map,
|
|
8277
7939
|
historyDb: null,
|
|
8278
7940
|
agentlifeStateDir: fallbackDir,
|
|
8279
|
-
registryFilePath:
|
|
8280
|
-
dbBaseDir:
|
|
8281
|
-
historyDbPath:
|
|
7941
|
+
registryFilePath: path16.join(fallbackDir, "agent-registry.json"),
|
|
7942
|
+
dbBaseDir: path16.join(fallbackDir, "db"),
|
|
7943
|
+
historyDbPath: path16.join(fallbackDir, "agentlife.db"),
|
|
8282
7944
|
runCommand: api.runtime.system?.runCommandWithTimeout ?? null,
|
|
8283
7945
|
enqueueSystemEvent: null,
|
|
8284
7946
|
requestHeartbeatNow: null,
|
|
@@ -8309,7 +7971,7 @@ function register(api) {
|
|
|
8309
7971
|
stopObservability = startObservabilityService(state2);
|
|
8310
7972
|
stopDailySweep = startDailySweepService(state2);
|
|
8311
7973
|
stopQualityCheckPoller = startQualityCheckPoller(state2);
|
|
8312
|
-
|
|
7974
|
+
renderAllAgentWidgets(state2, api.runtime, console.log);
|
|
8313
7975
|
}
|
|
8314
7976
|
});
|
|
8315
7977
|
registerConfigOptimizer(api, state2);
|
|
@@ -8319,7 +7981,7 @@ function register(api) {
|
|
|
8319
7981
|
id: "agentlife-shutdown",
|
|
8320
7982
|
start: () => {},
|
|
8321
7983
|
stop: () => {
|
|
8322
|
-
stopFollowupSystem();
|
|
7984
|
+
stopFollowupSystem(state2);
|
|
8323
7985
|
if (stopObservability) {
|
|
8324
7986
|
stopObservability();
|
|
8325
7987
|
stopObservability = null;
|
|
@@ -8336,7 +7998,6 @@ function register(api) {
|
|
|
8336
7998
|
stopCloudflared();
|
|
8337
7999
|
stopCloudflared = null;
|
|
8338
8000
|
}
|
|
8339
|
-
shutdownColdStartMachine();
|
|
8340
8001
|
closeAllDbs(state2);
|
|
8341
8002
|
}
|
|
8342
8003
|
});
|
|
@@ -8354,8 +8015,9 @@ function register(api) {
|
|
|
8354
8015
|
registerFollowupsGateway(api, state2);
|
|
8355
8016
|
registerAdminGateway(api, state2);
|
|
8356
8017
|
registerProvidersGateway(api, state2);
|
|
8018
|
+
registerModelsConfigGateway(api);
|
|
8357
8019
|
registerWebApp(api);
|
|
8358
|
-
const notifyConfigPath =
|
|
8020
|
+
const notifyConfigPath = path16.join(fallbackDir, "notification-config.json");
|
|
8359
8021
|
api.registerGatewayMethod("agentlife.notifications.register", ({ params, respond }) => {
|
|
8360
8022
|
const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
|
|
8361
8023
|
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
@@ -8363,45 +8025,12 @@ function register(api) {
|
|
|
8363
8025
|
return respond(false, { error: "missing serverUrl or apiKey" });
|
|
8364
8026
|
}
|
|
8365
8027
|
try {
|
|
8366
|
-
const { writeFileSync:
|
|
8367
|
-
|
|
8368
|
-
|
|
8028
|
+
const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync7 } = __require("node:fs");
|
|
8029
|
+
mkdirSync7(path16.dirname(notifyConfigPath), { recursive: true });
|
|
8030
|
+
writeFileSync10(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
|
|
8369
8031
|
} catch {}
|
|
8370
8032
|
respond(true, { registered: true });
|
|
8371
8033
|
}, { scope: "operator.write" });
|
|
8372
|
-
api.registerGatewayMethod("agentlife.coldStart.observe", ({ respond }) => {
|
|
8373
|
-
try {
|
|
8374
|
-
if (!currentState) {
|
|
8375
|
-
return respond(false, { error: "plugin state not initialized" });
|
|
8376
|
-
}
|
|
8377
|
-
respond(true, observeColdStart(currentState));
|
|
8378
|
-
} catch (e) {
|
|
8379
|
-
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8380
|
-
}
|
|
8381
|
-
}, { scope: "operator.read" });
|
|
8382
|
-
api.registerGatewayMethod("agentlife.coldStart.retry", async ({ respond }) => {
|
|
8383
|
-
try {
|
|
8384
|
-
if (!currentState) {
|
|
8385
|
-
return respond(false, { error: "plugin state not initialized" });
|
|
8386
|
-
}
|
|
8387
|
-
respond(true, await retryColdStart(currentState));
|
|
8388
|
-
} catch (e) {
|
|
8389
|
-
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8390
|
-
}
|
|
8391
|
-
}, { scope: "operator.read" });
|
|
8392
|
-
api.registerGatewayMethod("agentlife.coldStart.advance", ({ params, respond }) => {
|
|
8393
|
-
try {
|
|
8394
|
-
if (!currentState)
|
|
8395
|
-
return respond(false, { error: "plugin state not initialized" });
|
|
8396
|
-
const kind = typeof params?.kind === "string" ? params.kind : null;
|
|
8397
|
-
if (!kind)
|
|
8398
|
-
return respond(false, { error: "missing kind" });
|
|
8399
|
-
emitTrigger(currentState, kind, params?.payload ?? {});
|
|
8400
|
-
respond(true, { enqueued: true });
|
|
8401
|
-
} catch (e) {
|
|
8402
|
-
respond(false, { code: "internal_error", message: e?.message ?? String(e) });
|
|
8403
|
-
}
|
|
8404
|
-
}, { scope: "operator.admin" });
|
|
8405
8034
|
}
|
|
8406
8035
|
export {
|
|
8407
8036
|
register as default,
|