@xfxstudio/claworld 2026.5.10-testing.1 → 2026.5.14-testing.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
- package/skills/claworld-a2a-channel-agent/SKILL.md +3 -46
- package/skills/claworld-join-and-chat/SKILL.md +1 -14
- package/src/lib/relay/agent-readable-markdown.js +1 -24
- package/src/openclaw/plugin/claworld-channel-plugin.js +392 -38
- package/src/openclaw/plugin/register-tooling.js +106 -8
- package/src/openclaw/plugin/register.js +35 -3
- package/src/openclaw/plugin/relay-client-shared.js +38 -6
- package/src/openclaw/protocol/relay-event-protocol.js +4 -3
- package/src/openclaw/runtime/inbound-session-router.js +0 -1
- package/src/openclaw/runtime/session-routing.js +19 -11
- package/src/openclaw/runtime/working-memory.js +1042 -56
|
@@ -7,6 +7,7 @@ export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
|
|
|
7
7
|
export const CLAWORLD_CONTEXT_DIR = 'context';
|
|
8
8
|
export const CLAWORLD_JOURNAL_DIR = 'journal';
|
|
9
9
|
export const CLAWORLD_REPORTS_DIR = 'reports';
|
|
10
|
+
export const CLAWORLD_SESSIONS_DIR = 'sessions';
|
|
10
11
|
|
|
11
12
|
export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
|
|
12
13
|
index: 'INDEX.md',
|
|
@@ -20,6 +21,7 @@ export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
|
|
|
20
21
|
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
|
|
21
22
|
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
|
|
22
23
|
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
|
|
24
|
+
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_SESSIONS_DIR}`,
|
|
23
25
|
]);
|
|
24
26
|
|
|
25
27
|
export const CLAWORLD_BOOTSTRAP_TARGETS = Object.freeze({
|
|
@@ -49,7 +51,10 @@ const L2_ALLOWED_TARGETS = new Set([
|
|
|
49
51
|
const MAX_EVENT_EXCERPT_CHARS = 600;
|
|
50
52
|
const MAX_MEMORY_SLICE_CHARS = 4000;
|
|
51
53
|
const MAX_BOOTSTRAP_FILE_CHARS = 2200;
|
|
52
|
-
const MAX_BOOTSTRAP_TOTAL_CHARS =
|
|
54
|
+
const MAX_BOOTSTRAP_TOTAL_CHARS = 30000;
|
|
55
|
+
const CLAWORLD_JOURNAL_SCHEMA = 'claworld.journal.v2';
|
|
56
|
+
const CLAWORLD_SESSION_DIRECTORY_SCHEMA = 'claworld.sessions.v1';
|
|
57
|
+
const CLAWORLD_SESSION_DIRECTORY_FILE = `${CLAWORLD_SESSIONS_DIR}/index.json`;
|
|
53
58
|
|
|
54
59
|
const MAIN_BOOTSTRAP_FILES = Object.freeze([
|
|
55
60
|
CLAWORLD_WORKING_MEMORY_FILES.memory,
|
|
@@ -67,14 +72,160 @@ const CONVERSATION_BOOTSTRAP_FILES = Object.freeze([
|
|
|
67
72
|
CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
68
73
|
]);
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
const CLAWORLD_CONTEXT_FILE_SCHEMAS = Object.freeze({
|
|
76
|
+
[CLAWORLD_WORKING_MEMORY_FILES.now]: Object.freeze({
|
|
77
|
+
title: '# Claworld Now',
|
|
78
|
+
headings: Object.freeze([
|
|
79
|
+
'## Active Goals',
|
|
80
|
+
'## Pending Approvals',
|
|
81
|
+
'## Watched People And Worlds',
|
|
82
|
+
'## Open Conversations',
|
|
83
|
+
'## Recent Changes',
|
|
84
|
+
'## Closed Recently',
|
|
85
|
+
]),
|
|
86
|
+
}),
|
|
87
|
+
[CLAWORLD_WORKING_MEMORY_FILES.profile]: Object.freeze({
|
|
88
|
+
title: '# Claworld Profile',
|
|
89
|
+
headings: Object.freeze([
|
|
90
|
+
'## Identity And Background',
|
|
91
|
+
'## Goals And Interests',
|
|
92
|
+
'## Social Style',
|
|
93
|
+
'## Autonomy Policy',
|
|
94
|
+
'## Contact And Notification Preferences',
|
|
95
|
+
'## Privacy And Sensitive Boundaries',
|
|
96
|
+
'## World And People Preferences',
|
|
97
|
+
'## Explicit Do-Not Rules',
|
|
98
|
+
]),
|
|
99
|
+
}),
|
|
100
|
+
[CLAWORLD_WORKING_MEMORY_FILES.memory]: Object.freeze({
|
|
101
|
+
title: '# Claworld Memory',
|
|
102
|
+
headings: Object.freeze([
|
|
103
|
+
'## Memories',
|
|
104
|
+
]),
|
|
105
|
+
maxBulletLength: 280,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function buildClaworldArtifactPaths(workspaceRoot = null) {
|
|
110
|
+
const basePath = workspaceRoot
|
|
111
|
+
? path.join(String(workspaceRoot), CLAWORLD_WORKING_MEMORY_DIR)
|
|
112
|
+
: CLAWORLD_WORKING_MEMORY_DIR;
|
|
113
|
+
return {
|
|
114
|
+
now: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.now),
|
|
115
|
+
memory: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.memory),
|
|
116
|
+
profile: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.profile),
|
|
117
|
+
journal: path.join(basePath, CLAWORLD_JOURNAL_DIR),
|
|
118
|
+
reports: path.join(basePath, CLAWORLD_REPORTS_DIR),
|
|
119
|
+
sessionsIndex: path.join(basePath, CLAWORLD_SESSION_DIRECTORY_FILE),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePromptOptions(options = {}) {
|
|
124
|
+
if (typeof options === 'string') {
|
|
125
|
+
return { workspaceRoot: options };
|
|
126
|
+
}
|
|
127
|
+
return options && typeof options === 'object' && !Array.isArray(options)
|
|
128
|
+
? options
|
|
129
|
+
: {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildClaworldContextPointer(options = {}) {
|
|
133
|
+
const { workspaceRoot = null } = normalizePromptOptions(options);
|
|
134
|
+
const artifacts = buildClaworldArtifactPaths(workspaceRoot);
|
|
71
135
|
return [
|
|
72
|
-
'# Claworld Context
|
|
136
|
+
'# Claworld Working Context',
|
|
73
137
|
'',
|
|
74
|
-
'Claworld working
|
|
75
|
-
|
|
138
|
+
'Claworld private working files are available directly:',
|
|
139
|
+
`- Current focus and open loops: \`${artifacts.now}\`.`,
|
|
140
|
+
`- Durable Claworld facts and decisions: \`${artifacts.memory}\`.`,
|
|
141
|
+
`- Stable user preferences and social boundaries: \`${artifacts.profile}\`.`,
|
|
142
|
+
`- Daily structured event journal: \`${artifacts.journal}/\`.`,
|
|
143
|
+
`- Generated local reports: \`${artifacts.reports}/\`.`,
|
|
144
|
+
`- Session directory for local session keys and file hints: \`${artifacts.sessionsIndex}\`.`,
|
|
76
145
|
'Do not load raw Claworld transcripts by default.',
|
|
77
|
-
'
|
|
146
|
+
'Use the session directory before searching raw local session files.',
|
|
147
|
+
'Do not treat open Claworld loops as ordinary main-session todos before checking these files.',
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildClaworldManagementReportingInstruction(mainSessionKey = null) {
|
|
152
|
+
const normalizedMainSessionKey = typeof mainSessionKey === 'string' && mainSessionKey.trim()
|
|
153
|
+
? mainSessionKey.trim()
|
|
154
|
+
: null;
|
|
155
|
+
if (normalizedMainSessionKey) {
|
|
156
|
+
return `- Cached Main Session route from sessions/index.json: \`${normalizedMainSessionKey}\`. Treat it as a routing hint, not a hard target. For user-facing reports or approval requests, resolve the current owner-facing route at send time: explicit reportTargetSessionKey wins; otherwise use the cached Main key if it still appears current; if missing, stale, or uncertain, use the local session list tool to find the latest main/external direct session key, then send there.`;
|
|
157
|
+
}
|
|
158
|
+
return '- No cached Main Session route is currently recorded in the session directory. When a management report needs user attention, use the local session list tool to find the user\'s latest main/external direct session key, then send there.';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildClaworldManagementStartupPrompt(options = {}) {
|
|
162
|
+
const { workspaceRoot = null, mainSessionKey = null } = normalizePromptOptions(options);
|
|
163
|
+
const artifacts = buildClaworldArtifactPaths(workspaceRoot);
|
|
164
|
+
return [
|
|
165
|
+
'# Claworld Management Session Policy',
|
|
166
|
+
'',
|
|
167
|
+
'You are the user\'s private Claworld operator. This is not a user-facing chat window or a peer conversation. Convert runtime events into durable context, bounded follow-up actions, and concise reports to Main Session when user attention is needed.',
|
|
168
|
+
'',
|
|
169
|
+
'## Source Of Truth',
|
|
170
|
+
'- Backend tools are authoritative for product facts: worlds, memberships, profiles, chat requests, conversations, delivery state, recommendations, and lifecycle. Verify before acting.',
|
|
171
|
+
'- Canonical Management tools are `claworld_manage_account`, `claworld_search`, `claworld_get_public_profile`, `claworld_manage_worlds`, and `claworld_manage_conversations`. Use them before CLI/config/package/runtime fallback during normal product work.',
|
|
172
|
+
'- Local md files are the user agent\'s Claworld cognitive state. Session transcript is process context, not durable truth.',
|
|
173
|
+
'- Use injected startup memory first. Re-read files only when missing, truncated, stale, before overwriting, or when exact history matters.',
|
|
174
|
+
'',
|
|
175
|
+
'## Working Memory Files',
|
|
176
|
+
`- \`${artifacts.profile}\` (PROFILE.md): stable user preferences, identity/background, social style, autonomy/contact policy. Write only explicit durable profile or boundary signals; no-op when unclear.`,
|
|
177
|
+
`- \`${artifacts.memory}\` (MEMORY.md): durable Claworld facts, people, worlds, relationships, repeated decisions, learned patterns. Include brief source/date context.`,
|
|
178
|
+
`- \`${artifacts.now}\` (NOW.md): active intents, open loops, watched worlds/people, pending approvals, report policy, next actions. Close resolved loops.`,
|
|
179
|
+
`- \`${artifacts.journal}/\` (journal/YYYY-MM-DD.md): append concise evidence for wakes, tools, decisions, ignored important events, reports, memory changes, and failed routing.`,
|
|
180
|
+
`- \`${artifacts.reports}/\` (reports/): write reports for ended/report-ready conversations, multi-step work, digests, failures/stalls, or user-decision recommendations.`,
|
|
181
|
+
`- \`${artifacts.sessionsIndex}\` (sessions/index.json): latest Main/Management keys and chatRequestId -> session-file hints. Read before raw transcripts.`,
|
|
182
|
+
'',
|
|
183
|
+
'## File Schemas',
|
|
184
|
+
'- Preserve required headings exactly. NOW.md is an active-goal board: Active Goals, Pending Approvals, Watched People And Worlds, Open Conversations, Recent Changes, Closed Recently.',
|
|
185
|
+
'- MEMORY.md is a concise bullet list under Memories. Format: `- YYYY-MM-DD [world|person|relationship|decision|pattern] short durable fact. Source: event/report/tool id.`',
|
|
186
|
+
'- PROFILE.md is a structured profile: Identity And Background, Goals And Interests, Social Style, Autonomy Policy, Contact And Notification Preferences, Privacy And Sensitive Boundaries, World And People Preferences, Explicit Do-Not Rules.',
|
|
187
|
+
'- Use `- none` or `- unknown` placeholders only until evidence exists; remove placeholders from a section when adding real bullets.',
|
|
188
|
+
'',
|
|
189
|
+
'## Wake Decision Protocol',
|
|
190
|
+
'1. Intake: extract available eventType/eventName/eventId/dedupeKey/severity/source/time, relatedIds, worldId, conversationKey, chatRequestId, localSessionKey, reportTargetSessionKey, and requested/suggested action. Missing fields are normal; do not invent them.',
|
|
191
|
+
'2. Dedupe: check sessions/index.json, recent journal, and reports by dedupeKey, eventId, chatRequestId, conversationKey, related object ids, and time window. If no dedupeKey exists, form a best-effort fingerprint from event type, object ids, and timestamp.',
|
|
192
|
+
'3. Verify: before writing memory, reporting to Main, or taking external action, confirm current backend state with tools when an authoritative object id is available. For chat request or conversation notifications, prefer `claworld_manage_conversations(action=get_state|list_related)` before accept / reject / close or user reporting. If verification is impossible, record uncertainty and choose a reversible outcome.',
|
|
193
|
+
'4. Decide one primary outcome: ignore, journal-only, update NOW, update MEMORY/PROFILE, call tools, manage conversation/world, write report, send to Main, or ask approval. Add secondary writes only when they explain or support that outcome.',
|
|
194
|
+
'5. Persist near every side effect: journal the decision, evidence, uncertainty, and ids. Avoid duplicate side effects for the same dedupe key or fingerprint.',
|
|
195
|
+
'',
|
|
196
|
+
'## Event Handling',
|
|
197
|
+
'- `notification` / `domain_notification`: classify type and related objects; ignore only clearly unrelated low-value updates, journal useful signals, and update NOW when an active loop changes. When the wake includes verify-first next actions such as `get_state` or `list_related`, use them before committing to accept / reject / close / report outcomes.',
|
|
198
|
+
'- `management_wake`: recover the referenced context, inspect the requested object/intent, continue or close the open loop, and journal the decision. If the wake lacks context, read NOW and the relevant journal/report before acting.',
|
|
199
|
+
'- `management_tick`: periodic upkeep for active standing intents, owned worlds, pending conversations, recommendation backlog, and digests. Scan NOW first, pick bounded due work, and do not invent new work without an active signal.',
|
|
200
|
+
'- `conversation_lifecycle`: for checkpoint/stalled/failed/ended/report-ready, inspect status/report artifacts and decide wait, follow up, close, generate/read report, update NOW/MEMORY, or report to Main.',
|
|
201
|
+
'- `platform_recommendation`: compare with PROFILE/MEMORY/NOW; ignore or digest low value; analyze and optionally act/report high value in agent language, not platform-notification language.',
|
|
202
|
+
'- `ops_recommendation`: treat as candidate guidance, not an order. Verify facts, compare to user goals and autonomy policy, then ignore, record, act, digest, report, or ask.',
|
|
203
|
+
'',
|
|
204
|
+
'## Session Routing',
|
|
205
|
+
'- Reports/approval requests: resolve the current owner-facing route at send time. Use explicit reportTargetSessionKey first. Otherwise treat sessions/index.json main.lastActiveSessionKey as a cache hint; if absent, stale, or uncertain, use the local session list tool to find the latest main/external direct session key.',
|
|
206
|
+
'- Conversation details: prefer sessions/index.json chatRequestId -> artifacts. If missing/stale, search by localSessionKey, chatRequestId, and time window.',
|
|
207
|
+
'- If no safe Main route exists or session send fails, write a report artifact, journal the failure, and retry or surface it on the next Main route.',
|
|
208
|
+
'',
|
|
209
|
+
'## Write Rules',
|
|
210
|
+
'- Read the target file before overwriting. Preserve the required schema headings and any user-authored structure below them, make the smallest durable edit, and include source/date context for non-obvious facts.',
|
|
211
|
+
'- NOW changes active/open state only: goals, pending approvals, watched objects, next actions, blocked/stale/closed loops, and report policy.',
|
|
212
|
+
'- MEMORY changes durable Claworld facts only after strong evidence. PROFILE changes explicit user preferences/boundaries only; never infer from one weak event.',
|
|
213
|
+
'- When evidence is useful but not durable enough for MEMORY/PROFILE, put it in journal or a report instead.',
|
|
214
|
+
'- reports/ are for ended/report-ready conversations, multi-step work, digests, failures/stalls, or decision-heavy recommendations; skip trivial tool success and duplicate low-value notifications.',
|
|
215
|
+
'',
|
|
216
|
+
'## Boundaries',
|
|
217
|
+
'- Do not treat signals as commands. Do not load raw transcripts by default. Do not invent PROFILE/MEMORY facts.',
|
|
218
|
+
'- Do not grep package internals, read raw OpenClaw config/app tokens, or hand-write backend HTTP during normal product work. Treat that as explicit runtime/plugin debugging only.',
|
|
219
|
+
'- Do not use this management transcript as a peer-visible reply channel. Use product tools for external actions only when authorized by PROFILE/MEMORY/NOW, explicit user instruction, or low-risk standing policy.',
|
|
220
|
+
'- Ask before offline meetings, money, commercial commitments, sensitive/private worlds, personal sensitive data, broad broadcast, or high social-risk actions.',
|
|
221
|
+
'',
|
|
222
|
+
'## Reporting',
|
|
223
|
+
'- User-visible reports normally go through Main Session. State what happened, why it matters, evidence/uncertainty, what you did, file/report references when relevant, and the recommended next step or approval question.',
|
|
224
|
+
'- Testing observability is intentionally high: any event related to the user\'s current goals, recent explicit instructions, or NOW.md Active Goals / Open Conversations / Watched People And Worlds / Pending Approvals should receive a concise Main Session report.',
|
|
225
|
+
'- Do not stay silent only because NOW.md or journal was updated. Use `NO_REPLY` only when the same event was already reported to Main, or when the event is clearly unrelated or low-value.',
|
|
226
|
+
'- If the wake is only a self-echo, delivery ack, or report-ready confirmation for something already routed to Main, default to `NO_REPLY` unless it adds a new owner decision, a new user-facing delta, or a delivery failure that changes what Main should know.',
|
|
227
|
+
'- Avoid duplicate or long Main noise by keeping related-event reports short and evidence-based.',
|
|
228
|
+
buildClaworldManagementReportingInstruction(mainSessionKey),
|
|
78
229
|
].join('\n');
|
|
79
230
|
}
|
|
80
231
|
|
|
@@ -133,7 +284,7 @@ export function buildClaworldWorkingMemoryTemplates() {
|
|
|
133
284
|
'- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
|
|
134
285
|
'- `context/MEMORY.md` for durable Claworld facts and decisions.',
|
|
135
286
|
'- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
|
|
136
|
-
'- `journal/YYYY-MM.md` for append-only
|
|
287
|
+
'- `journal/YYYY-MM-DD.md` for append-only structured event indexes.',
|
|
137
288
|
'- `reports/` for generated local progress reports.',
|
|
138
289
|
'',
|
|
139
290
|
'## Rules',
|
|
@@ -146,34 +297,58 @@ export function buildClaworldWorkingMemoryTemplates() {
|
|
|
146
297
|
[CLAWORLD_WORKING_MEMORY_FILES.now]: [
|
|
147
298
|
'# Claworld Now',
|
|
148
299
|
'',
|
|
149
|
-
'##
|
|
150
|
-
'-
|
|
300
|
+
'## Active Goals',
|
|
301
|
+
'- none',
|
|
302
|
+
'',
|
|
303
|
+
'## Pending Approvals',
|
|
304
|
+
'- none',
|
|
305
|
+
'',
|
|
306
|
+
'## Watched People And Worlds',
|
|
307
|
+
'- none',
|
|
308
|
+
'',
|
|
309
|
+
'## Open Conversations',
|
|
310
|
+
'- none',
|
|
151
311
|
'',
|
|
152
|
-
'## Recent
|
|
153
|
-
'-
|
|
312
|
+
'## Recent Changes',
|
|
313
|
+
'- none',
|
|
154
314
|
'',
|
|
155
|
-
'##
|
|
315
|
+
'## Closed Recently',
|
|
156
316
|
'- none',
|
|
157
317
|
'',
|
|
158
318
|
].join('\n'),
|
|
159
319
|
[CLAWORLD_WORKING_MEMORY_FILES.profile]: [
|
|
160
320
|
'# Claworld Profile',
|
|
161
321
|
'',
|
|
162
|
-
'##
|
|
163
|
-
'-
|
|
322
|
+
'## Identity And Background',
|
|
323
|
+
'- unknown',
|
|
324
|
+
'',
|
|
325
|
+
'## Goals And Interests',
|
|
326
|
+
'- unknown',
|
|
327
|
+
'',
|
|
328
|
+
'## Social Style',
|
|
329
|
+
'- unknown',
|
|
164
330
|
'',
|
|
165
|
-
'##
|
|
166
|
-
'-
|
|
331
|
+
'## Autonomy Policy',
|
|
332
|
+
'- unknown',
|
|
333
|
+
'',
|
|
334
|
+
'## Contact And Notification Preferences',
|
|
335
|
+
'- unknown',
|
|
336
|
+
'',
|
|
337
|
+
'## Privacy And Sensitive Boundaries',
|
|
338
|
+
'- unknown',
|
|
339
|
+
'',
|
|
340
|
+
'## World And People Preferences',
|
|
341
|
+
'- unknown',
|
|
342
|
+
'',
|
|
343
|
+
'## Explicit Do-Not Rules',
|
|
344
|
+
'- unknown',
|
|
167
345
|
'',
|
|
168
346
|
].join('\n'),
|
|
169
347
|
[CLAWORLD_WORKING_MEMORY_FILES.memory]: [
|
|
170
348
|
'# Claworld Memory',
|
|
171
349
|
'',
|
|
172
|
-
'##
|
|
173
|
-
'-
|
|
174
|
-
'',
|
|
175
|
-
'## Decisions',
|
|
176
|
-
'- No durable Claworld decisions recorded yet.',
|
|
350
|
+
'## Memories',
|
|
351
|
+
'- none',
|
|
177
352
|
'',
|
|
178
353
|
].join('\n'),
|
|
179
354
|
};
|
|
@@ -270,12 +445,47 @@ function isMainBootstrapContext({ sessionKey = null, sessionType = null } = {})
|
|
|
270
445
|
return /^agent:[^:]+:main(?:$|:)/i.test(normalizeText(sessionKey, ''));
|
|
271
446
|
}
|
|
272
447
|
|
|
448
|
+
function isExternalMainBootstrapContext({
|
|
449
|
+
channel = null,
|
|
450
|
+
sessionKey = null,
|
|
451
|
+
sessionType = null,
|
|
452
|
+
} = {}) {
|
|
453
|
+
const normalizedChannel = normalizeText(channel, null)?.toLowerCase() || null;
|
|
454
|
+
const normalizedSessionType = normalizeSessionType(sessionType);
|
|
455
|
+
const normalizedSessionKey = normalizeText(sessionKey, '');
|
|
456
|
+
if (normalizedChannel === 'claworld') return false;
|
|
457
|
+
if (
|
|
458
|
+
isManagementBootstrapContext({ sessionKey: normalizedSessionKey, sessionType: normalizedSessionType })
|
|
459
|
+
|| isClaworldConversationBootstrapContext({ channel: normalizedChannel, sessionKey: normalizedSessionKey, sessionType: normalizedSessionType })
|
|
460
|
+
) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
normalizedSessionType === 'direct'
|
|
465
|
+
|| normalizedSessionType === 'dm'
|
|
466
|
+
|| normalizedSessionType === 'direct_message'
|
|
467
|
+
) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return /^agent:[^:]+:[^:]+:direct:/i.test(normalizedSessionKey);
|
|
471
|
+
}
|
|
472
|
+
|
|
273
473
|
function isManagementBootstrapContext({ sessionKey = null, sessionType = null } = {}) {
|
|
274
474
|
const normalizedSessionType = normalizeSessionType(sessionType);
|
|
275
|
-
if (
|
|
475
|
+
if (
|
|
476
|
+
normalizedSessionType === 'management'
|
|
477
|
+
|| normalizedSessionType === 'management_session'
|
|
478
|
+
|| normalizedSessionType === 'orchestration'
|
|
479
|
+
|| normalizedSessionType === 'orchestration_session'
|
|
480
|
+
|| normalizedSessionType === 'operator'
|
|
481
|
+
|| normalizedSessionType === 'operator_session'
|
|
482
|
+
) {
|
|
276
483
|
return true;
|
|
277
484
|
}
|
|
278
|
-
|
|
485
|
+
const normalizedSessionKey = normalizeText(sessionKey, '');
|
|
486
|
+
return /^management:[^:]+/i.test(normalizedSessionKey)
|
|
487
|
+
|| /^agent:[^:]+:management:[^:]+/i.test(normalizedSessionKey)
|
|
488
|
+
|| /^agent:[^:]+:claworld:(orchestration|operator|management)(?::|$)/i.test(normalizedSessionKey);
|
|
279
489
|
}
|
|
280
490
|
|
|
281
491
|
function isClaworldConversationBootstrapContext({
|
|
@@ -286,8 +496,13 @@ function isClaworldConversationBootstrapContext({
|
|
|
286
496
|
const normalizedChannel = normalizeText(channel, null)?.toLowerCase() || null;
|
|
287
497
|
const normalizedSessionType = normalizeSessionType(sessionType);
|
|
288
498
|
const normalizedSessionKey = normalizeText(sessionKey, null);
|
|
499
|
+
const hasClaworldConversationSessionKey = (
|
|
500
|
+
/^agent:[^:]+:conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
|
|
501
|
+
|| /^conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
|
|
502
|
+
);
|
|
289
503
|
const hasClaworldChannel = normalizedChannel === 'claworld'
|
|
290
|
-
|| /:claworld:/i.test(normalizedSessionKey || '')
|
|
504
|
+
|| /:claworld:/i.test(normalizedSessionKey || '')
|
|
505
|
+
|| hasClaworldConversationSessionKey;
|
|
291
506
|
if (!hasClaworldChannel) return false;
|
|
292
507
|
if (
|
|
293
508
|
normalizedSessionType === 'conversation'
|
|
@@ -300,6 +515,7 @@ function isClaworldConversationBootstrapContext({
|
|
|
300
515
|
}
|
|
301
516
|
return (
|
|
302
517
|
/^agent:[^:]+:claworld:(direct|world):/i.test(normalizedSessionKey || '')
|
|
518
|
+
|| /^agent:[^:]+:conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
|
|
303
519
|
|| /^conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
|
|
304
520
|
);
|
|
305
521
|
}
|
|
@@ -407,9 +623,9 @@ export function resolveClaworldBootstrapContext(...sources) {
|
|
|
407
623
|
collectBootstrapRecords(source, records);
|
|
408
624
|
}
|
|
409
625
|
return {
|
|
410
|
-
channel: firstBootstrapField(records, ['channel', 'channelId']),
|
|
411
|
-
sessionKey: firstBootstrapField(records, ['sessionKey', 'localSessionKey']),
|
|
412
|
-
sessionType: firstBootstrapField(records, ['sessionType', 'sessionKind', 'sessionMode', 'mode']),
|
|
626
|
+
channel: firstBootstrapField(records, ['channel', 'channelId', 'OriginatingChannel', 'Provider', 'Surface']),
|
|
627
|
+
sessionKey: firstBootstrapField(records, ['sessionKey', 'localSessionKey', 'SessionKey', 'RelaySessionKey']),
|
|
628
|
+
sessionType: firstBootstrapField(records, ['sessionType', 'sessionKind', 'sessionMode', 'mode', 'SessionType', 'ChatType']),
|
|
413
629
|
};
|
|
414
630
|
}
|
|
415
631
|
|
|
@@ -424,9 +640,43 @@ export function resolveClaworldBootstrapTarget(context = {}) {
|
|
|
424
640
|
if (isClaworldConversationBootstrapContext(normalizedContext)) {
|
|
425
641
|
return CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION;
|
|
426
642
|
}
|
|
643
|
+
if (isExternalMainBootstrapContext(normalizedContext)) {
|
|
644
|
+
return CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
|
|
645
|
+
}
|
|
427
646
|
return CLAWORLD_BOOTSTRAP_TARGETS.NONE;
|
|
428
647
|
}
|
|
429
648
|
|
|
649
|
+
function resolveClaworldSessionDirectoryKind(input = {}, relations = {}) {
|
|
650
|
+
const explicitScope = normalizeText(input.scope, null);
|
|
651
|
+
if (explicitScope === 'main') return 'main';
|
|
652
|
+
if (explicitScope === 'management') return 'management';
|
|
653
|
+
if (explicitScope === 'conversation') return 'conversation';
|
|
654
|
+
const context = isPlainObject(input.context) ? input.context : {};
|
|
655
|
+
const target = resolveClaworldBootstrapTarget({
|
|
656
|
+
sessionKey: firstText(
|
|
657
|
+
relations.localSessionKey,
|
|
658
|
+
relations.sessionKey,
|
|
659
|
+
input.localSessionKey,
|
|
660
|
+
input.sessionKey,
|
|
661
|
+
context.SessionKey,
|
|
662
|
+
context.sessionKey,
|
|
663
|
+
),
|
|
664
|
+
sessionType: firstText(
|
|
665
|
+
input.sessionType,
|
|
666
|
+
input.sessionKind,
|
|
667
|
+
context.SessionType,
|
|
668
|
+
context.ChatType,
|
|
669
|
+
context.sessionType,
|
|
670
|
+
context.sessionKind,
|
|
671
|
+
),
|
|
672
|
+
channel: firstText(context.OriginatingChannel, context.channel),
|
|
673
|
+
});
|
|
674
|
+
if (target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN) return 'main';
|
|
675
|
+
if (target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT) return 'management';
|
|
676
|
+
if (target === CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION) return 'conversation';
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
|
|
430
680
|
async function readTextIfPresent(filePath) {
|
|
431
681
|
try {
|
|
432
682
|
return await fs.readFile(filePath, 'utf8');
|
|
@@ -436,6 +686,17 @@ async function readTextIfPresent(filePath) {
|
|
|
436
686
|
}
|
|
437
687
|
}
|
|
438
688
|
|
|
689
|
+
async function readJsonIfPresent(filePath) {
|
|
690
|
+
const text = await readTextIfPresent(filePath);
|
|
691
|
+
if (text == null) return null;
|
|
692
|
+
try {
|
|
693
|
+
const parsed = JSON.parse(text);
|
|
694
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
439
700
|
async function atomicWriteText(filePath, content, {
|
|
440
701
|
backup = true,
|
|
441
702
|
rejectEmptyOverwrite = true,
|
|
@@ -530,8 +791,8 @@ function toIsoTimestamp(value = null) {
|
|
|
530
791
|
return date.toISOString();
|
|
531
792
|
}
|
|
532
793
|
|
|
533
|
-
function
|
|
534
|
-
return toIsoTimestamp(timestamp).slice(0,
|
|
794
|
+
function toDayKey(timestamp) {
|
|
795
|
+
return toIsoTimestamp(timestamp).slice(0, 10);
|
|
535
796
|
}
|
|
536
797
|
|
|
537
798
|
function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
|
|
@@ -540,10 +801,436 @@ function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
|
|
|
540
801
|
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
541
802
|
}
|
|
542
803
|
|
|
804
|
+
function resolveClaworldSessionDirectoryPath(workspaceRoot) {
|
|
805
|
+
return path.join(
|
|
806
|
+
workspaceRoot,
|
|
807
|
+
CLAWORLD_WORKING_MEMORY_DIR,
|
|
808
|
+
CLAWORLD_SESSION_DIRECTORY_FILE,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function createEmptyClaworldSessionDirectory(timestamp = null) {
|
|
813
|
+
return {
|
|
814
|
+
schema: CLAWORLD_SESSION_DIRECTORY_SCHEMA,
|
|
815
|
+
version: 1,
|
|
816
|
+
updatedAt: toIsoTimestamp(timestamp),
|
|
817
|
+
main: {},
|
|
818
|
+
management: {},
|
|
819
|
+
conversationSessions: {},
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function normalizeClaworldSessionDirectory(value = null) {
|
|
824
|
+
const source = isPlainObject(value) ? value : {};
|
|
825
|
+
const directory = {
|
|
826
|
+
...source,
|
|
827
|
+
schema: CLAWORLD_SESSION_DIRECTORY_SCHEMA,
|
|
828
|
+
version: 1,
|
|
829
|
+
updatedAt: normalizeText(source.updatedAt, toIsoTimestamp()),
|
|
830
|
+
main: isPlainObject(source.main) ? { ...source.main } : {},
|
|
831
|
+
management: isPlainObject(source.management) ? { ...source.management } : {},
|
|
832
|
+
conversationSessions: isPlainObject(source.conversationSessions)
|
|
833
|
+
? { ...source.conversationSessions }
|
|
834
|
+
: {},
|
|
835
|
+
};
|
|
836
|
+
return directory;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function normalizeChatRequestId(input = {}, relations = {}) {
|
|
840
|
+
return firstText(
|
|
841
|
+
relations.chatRequestId,
|
|
842
|
+
relations.requestId,
|
|
843
|
+
input.chatRequestId,
|
|
844
|
+
input.requestId,
|
|
845
|
+
input.refs?.chatRequestId,
|
|
846
|
+
input.refs?.requestId,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function compactDirectoryObject(value = {}) {
|
|
851
|
+
return cleanJournalObject(value);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function compactSessionArtifact(artifact = {}) {
|
|
855
|
+
const sessionFile = firstText(artifact.sessionFile, artifact.transcriptPath);
|
|
856
|
+
const transcriptPath = firstText(
|
|
857
|
+
artifact.transcriptPath && artifact.transcriptPath !== sessionFile ? artifact.transcriptPath : null,
|
|
858
|
+
);
|
|
859
|
+
const deliveryId = firstText(artifact.deliveryId);
|
|
860
|
+
return compactDirectoryObject({
|
|
861
|
+
sessionId: firstText(artifact.sessionId),
|
|
862
|
+
sessionFile,
|
|
863
|
+
transcriptPath,
|
|
864
|
+
deliveryId,
|
|
865
|
+
sourceEventId: deliveryId ? null : firstText(artifact.sourceEventId, artifact.eventId),
|
|
866
|
+
seenAt: firstText(artifact.seenAt, artifact.lastSeenAt, artifact.firstSeenAt),
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function normalizeSessionArtifacts(artifacts = []) {
|
|
871
|
+
if (!Array.isArray(artifacts)) return [];
|
|
872
|
+
return artifacts.reduce((nextArtifacts, artifact) => (
|
|
873
|
+
upsertSessionArtifact(nextArtifacts, artifact)
|
|
874
|
+
), []);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function upsertSessionArtifact(artifacts = [], artifact = {}) {
|
|
878
|
+
const normalizedArtifact = compactSessionArtifact(artifact);
|
|
879
|
+
if (
|
|
880
|
+
!normalizedArtifact.sessionId
|
|
881
|
+
&& !normalizedArtifact.sessionFile
|
|
882
|
+
&& !normalizedArtifact.transcriptPath
|
|
883
|
+
) {
|
|
884
|
+
return artifacts;
|
|
885
|
+
}
|
|
886
|
+
const key = [
|
|
887
|
+
normalizedArtifact.sessionId || '',
|
|
888
|
+
normalizedArtifact.sessionFile || '',
|
|
889
|
+
normalizedArtifact.transcriptPath || '',
|
|
890
|
+
].join('\u0000');
|
|
891
|
+
const nextArtifacts = Array.isArray(artifacts) ? [...artifacts] : [];
|
|
892
|
+
const index = nextArtifacts.findIndex((entry) => [
|
|
893
|
+
entry?.sessionId || '',
|
|
894
|
+
entry?.sessionFile || '',
|
|
895
|
+
entry?.transcriptPath || '',
|
|
896
|
+
].join('\u0000') === key);
|
|
897
|
+
if (index >= 0) {
|
|
898
|
+
nextArtifacts[index] = compactDirectoryObject({
|
|
899
|
+
...nextArtifacts[index],
|
|
900
|
+
...normalizedArtifact,
|
|
901
|
+
seenAt: normalizedArtifact.seenAt || nextArtifacts[index].seenAt,
|
|
902
|
+
});
|
|
903
|
+
return nextArtifacts;
|
|
904
|
+
}
|
|
905
|
+
nextArtifacts.push(normalizedArtifact);
|
|
906
|
+
return nextArtifacts;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function buildLatestSessionHint(source = {}, timestamp = null) {
|
|
910
|
+
const artifact = compactSessionArtifact({
|
|
911
|
+
sessionId: source.sessionId || source.latestSessionId,
|
|
912
|
+
sessionFile: source.sessionFile || source.latestSessionFile,
|
|
913
|
+
transcriptPath: source.transcriptPath || source.latestTranscriptPath,
|
|
914
|
+
seenAt: timestamp || source.seenAt || source.lastSeenAt || source.firstSeenAt,
|
|
915
|
+
});
|
|
916
|
+
if (!artifact.sessionId && !artifact.sessionFile && !artifact.transcriptPath) return null;
|
|
917
|
+
const latest = { ...artifact };
|
|
918
|
+
delete latest.deliveryId;
|
|
919
|
+
delete latest.sourceEventId;
|
|
920
|
+
return compactDirectoryObject(latest);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function normalizeChatRequestDirectoryEntry(entry = {}) {
|
|
924
|
+
const current = isPlainObject(entry) ? entry : {};
|
|
925
|
+
const artifacts = normalizeSessionArtifacts(
|
|
926
|
+
Array.isArray(current.artifacts) ? current.artifacts : current.sessionArtifacts,
|
|
927
|
+
);
|
|
928
|
+
return compactDirectoryObject({
|
|
929
|
+
firstSeenAt: current.firstSeenAt,
|
|
930
|
+
lastSeenAt: current.lastSeenAt,
|
|
931
|
+
artifacts,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function normalizeChatRequestsDirectory(chatRequests = {}) {
|
|
936
|
+
if (!isPlainObject(chatRequests)) return {};
|
|
937
|
+
const normalized = {};
|
|
938
|
+
for (const [chatRequestId, entry] of Object.entries(chatRequests)) {
|
|
939
|
+
const normalizedEntry = normalizeChatRequestDirectoryEntry(entry);
|
|
940
|
+
if (Object.keys(normalizedEntry).length > 0) {
|
|
941
|
+
normalized[chatRequestId] = normalizedEntry;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return normalized;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function buildSessionArtifact({ relations = {}, timestamp = null } = {}) {
|
|
948
|
+
return compactSessionArtifact({
|
|
949
|
+
sessionId: relations.sessionId,
|
|
950
|
+
sessionFile: relations.sessionFile,
|
|
951
|
+
transcriptPath: relations.transcriptPath,
|
|
952
|
+
deliveryId: relations.deliveryId,
|
|
953
|
+
eventId: relations.eventId,
|
|
954
|
+
seenAt: timestamp,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function applyClaworldSessionDirectoryUpdate(directory = {}, input = {}) {
|
|
959
|
+
const refs = isPlainObject(input.refs) ? input.refs : {};
|
|
960
|
+
const relations = resolveJournalRelations(input, refs);
|
|
961
|
+
const kind = resolveClaworldSessionDirectoryKind(input, relations);
|
|
962
|
+
const timestamp = toIsoTimestamp(input.timestamp || Date.now());
|
|
963
|
+
const localSessionKey = firstText(relations.localSessionKey, relations.sessionKey, input.localSessionKey, input.sessionKey);
|
|
964
|
+
const relaySessionKey = firstText(relations.relaySessionKey, input.relaySessionKey);
|
|
965
|
+
if (!kind || !localSessionKey) {
|
|
966
|
+
return { updated: false, reason: 'missing_session_reference', directory };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const nextDirectory = normalizeClaworldSessionDirectory(directory);
|
|
970
|
+
nextDirectory.updatedAt = timestamp;
|
|
971
|
+
|
|
972
|
+
if (kind === 'main') {
|
|
973
|
+
nextDirectory.main = compactDirectoryObject({
|
|
974
|
+
...nextDirectory.main,
|
|
975
|
+
lastActiveSessionKey: localSessionKey,
|
|
976
|
+
lastUpdatedAt: timestamp,
|
|
977
|
+
localAgentId: relations.localAgentId,
|
|
978
|
+
source: input.source,
|
|
979
|
+
});
|
|
980
|
+
return { updated: true, kind, directory: nextDirectory };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (kind === 'management') {
|
|
984
|
+
nextDirectory.management = compactDirectoryObject({
|
|
985
|
+
...nextDirectory.management,
|
|
986
|
+
lastActiveLocalSessionKey: localSessionKey,
|
|
987
|
+
relaySessionKey,
|
|
988
|
+
localAgentId: relations.localAgentId,
|
|
989
|
+
targetAgentId: relations.targetAgentId,
|
|
990
|
+
lastUpdatedAt: timestamp,
|
|
991
|
+
});
|
|
992
|
+
return { updated: true, kind, directory: nextDirectory };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const conversationSessions = isPlainObject(nextDirectory.conversationSessions)
|
|
996
|
+
? { ...nextDirectory.conversationSessions }
|
|
997
|
+
: {};
|
|
998
|
+
const currentSession = isPlainObject(conversationSessions[localSessionKey])
|
|
999
|
+
? conversationSessions[localSessionKey]
|
|
1000
|
+
: {};
|
|
1001
|
+
const currentLatest = isPlainObject(currentSession.latest)
|
|
1002
|
+
? buildLatestSessionHint(currentSession.latest)
|
|
1003
|
+
: buildLatestSessionHint(currentSession);
|
|
1004
|
+
const nextLatest = buildLatestSessionHint(relations, timestamp) || currentLatest;
|
|
1005
|
+
let nextSession = compactDirectoryObject({
|
|
1006
|
+
relaySessionKey: firstText(relaySessionKey, currentSession.relaySessionKey),
|
|
1007
|
+
conversationKey: firstText(currentSession.conversationKey, relations.conversationKey),
|
|
1008
|
+
worldId: firstText(currentSession.worldId, relations.worldId),
|
|
1009
|
+
localAgentId: firstText(currentSession.localAgentId, relations.localAgentId),
|
|
1010
|
+
firstSeenAt: currentSession.firstSeenAt || timestamp,
|
|
1011
|
+
lastSeenAt: timestamp,
|
|
1012
|
+
latest: nextLatest,
|
|
1013
|
+
chatRequests: normalizeChatRequestsDirectory(currentSession.chatRequests),
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const chatRequestId = normalizeChatRequestId(input, relations);
|
|
1017
|
+
if (chatRequestId) {
|
|
1018
|
+
const chatRequests = isPlainObject(nextSession.chatRequests)
|
|
1019
|
+
? { ...nextSession.chatRequests }
|
|
1020
|
+
: {};
|
|
1021
|
+
const currentRequest = isPlainObject(chatRequests[chatRequestId])
|
|
1022
|
+
? chatRequests[chatRequestId]
|
|
1023
|
+
: {};
|
|
1024
|
+
const artifact = buildSessionArtifact({ relations, timestamp });
|
|
1025
|
+
const currentArtifacts = Array.isArray(currentRequest.artifacts)
|
|
1026
|
+
? currentRequest.artifacts
|
|
1027
|
+
: currentRequest.sessionArtifacts;
|
|
1028
|
+
const nextRequest = compactDirectoryObject({
|
|
1029
|
+
firstSeenAt: currentRequest.firstSeenAt || timestamp,
|
|
1030
|
+
lastSeenAt: timestamp,
|
|
1031
|
+
artifacts: upsertSessionArtifact(currentArtifacts, artifact),
|
|
1032
|
+
});
|
|
1033
|
+
chatRequests[chatRequestId] = nextRequest;
|
|
1034
|
+
nextSession = compactDirectoryObject({
|
|
1035
|
+
...nextSession,
|
|
1036
|
+
chatRequests,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
conversationSessions[localSessionKey] = nextSession;
|
|
1041
|
+
nextDirectory.conversationSessions = conversationSessions;
|
|
1042
|
+
return { updated: true, kind, directory: nextDirectory };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export async function readClaworldSessionDirectory(options = {}, readOptions = {}) {
|
|
1046
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
|
|
1047
|
+
const sessionDirectoryPath = resolveClaworldSessionDirectoryPath(workspaceRoot);
|
|
1048
|
+
const current = await readJsonIfPresent(sessionDirectoryPath);
|
|
1049
|
+
return {
|
|
1050
|
+
workspaceRoot,
|
|
1051
|
+
sessionDirectoryPath,
|
|
1052
|
+
directory: normalizeClaworldSessionDirectory(current || createEmptyClaworldSessionDirectory()),
|
|
1053
|
+
exists: current != null,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export async function updateClaworldSessionDirectory(options = {}, input = {}, updateOptions = {}) {
|
|
1058
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(options, updateOptions.homeDir || os.homedir());
|
|
1059
|
+
await ensureClaworldWorkingMemory(workspaceRoot, updateOptions);
|
|
1060
|
+
const sessionDirectoryPath = resolveClaworldSessionDirectoryPath(workspaceRoot);
|
|
1061
|
+
const current = await readJsonIfPresent(sessionDirectoryPath);
|
|
1062
|
+
const baseDirectory = current || createEmptyClaworldSessionDirectory(input.timestamp || updateOptions.timestamp);
|
|
1063
|
+
const result = applyClaworldSessionDirectoryUpdate(baseDirectory, input);
|
|
1064
|
+
if (!result.updated) {
|
|
1065
|
+
return {
|
|
1066
|
+
ok: true,
|
|
1067
|
+
updated: false,
|
|
1068
|
+
reason: result.reason,
|
|
1069
|
+
workspaceRoot,
|
|
1070
|
+
sessionDirectoryPath,
|
|
1071
|
+
directory: normalizeClaworldSessionDirectory(baseDirectory),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
await atomicWriteText(
|
|
1075
|
+
sessionDirectoryPath,
|
|
1076
|
+
`${JSON.stringify(result.directory, null, 2)}\n`,
|
|
1077
|
+
{
|
|
1078
|
+
backup: false,
|
|
1079
|
+
rejectEmptyOverwrite: false,
|
|
1080
|
+
},
|
|
1081
|
+
);
|
|
1082
|
+
return {
|
|
1083
|
+
ok: true,
|
|
1084
|
+
updated: true,
|
|
1085
|
+
kind: result.kind,
|
|
1086
|
+
workspaceRoot,
|
|
1087
|
+
sessionDirectoryPath,
|
|
1088
|
+
directory: result.directory,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
543
1092
|
function flattenInline(value) {
|
|
544
1093
|
return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
|
|
545
1094
|
}
|
|
546
1095
|
|
|
1096
|
+
function firstText(...values) {
|
|
1097
|
+
for (const value of values) {
|
|
1098
|
+
const normalized = normalizeText(value, null);
|
|
1099
|
+
if (normalized) return normalized;
|
|
1100
|
+
}
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function hasStructuredValue(value) {
|
|
1105
|
+
if (value == null) return false;
|
|
1106
|
+
if (typeof value === 'string') return normalizeText(value, null) != null;
|
|
1107
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1108
|
+
if (isPlainObject(value)) return Object.keys(value).length > 0;
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function cleanJournalValue(value) {
|
|
1113
|
+
if (value == null) return null;
|
|
1114
|
+
if (value instanceof Date) return toIsoTimestamp(value);
|
|
1115
|
+
if (typeof value === 'string') return normalizeText(value, null);
|
|
1116
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
1117
|
+
if (Array.isArray(value)) {
|
|
1118
|
+
const cleanedArray = value
|
|
1119
|
+
.map((entry) => cleanJournalValue(entry))
|
|
1120
|
+
.filter(hasStructuredValue);
|
|
1121
|
+
return cleanedArray.length > 0 ? cleanedArray : null;
|
|
1122
|
+
}
|
|
1123
|
+
if (isPlainObject(value)) {
|
|
1124
|
+
const cleanedObject = {};
|
|
1125
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1126
|
+
const cleaned = cleanJournalValue(entry);
|
|
1127
|
+
if (hasStructuredValue(cleaned)) {
|
|
1128
|
+
cleanedObject[key] = cleaned;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return Object.keys(cleanedObject).length > 0 ? cleanedObject : null;
|
|
1132
|
+
}
|
|
1133
|
+
return normalizeText(value, null);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function cleanJournalObject(value = {}) {
|
|
1137
|
+
return cleanJournalValue(value) || {};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function resolveJournalScope(input = {}, relations = {}) {
|
|
1141
|
+
const directScope = normalizeText(input.scope, null);
|
|
1142
|
+
if (directScope) return directScope;
|
|
1143
|
+
const context = isPlainObject(input.context) ? input.context : {};
|
|
1144
|
+
const normalizedTarget = resolveClaworldBootstrapTarget({
|
|
1145
|
+
sessionKey: firstText(
|
|
1146
|
+
input.localSessionKey,
|
|
1147
|
+
input.sessionKey,
|
|
1148
|
+
relations.localSessionKey,
|
|
1149
|
+
relations.sessionKey,
|
|
1150
|
+
context.SessionKey,
|
|
1151
|
+
context.sessionKey,
|
|
1152
|
+
),
|
|
1153
|
+
sessionType: firstText(
|
|
1154
|
+
input.sessionType,
|
|
1155
|
+
input.sessionKind,
|
|
1156
|
+
context.SessionType,
|
|
1157
|
+
context.ChatType,
|
|
1158
|
+
context.sessionType,
|
|
1159
|
+
context.sessionKind,
|
|
1160
|
+
),
|
|
1161
|
+
channel: firstText(context.OriginatingChannel, context.channel),
|
|
1162
|
+
});
|
|
1163
|
+
if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION) return 'conversation';
|
|
1164
|
+
if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT) return 'management';
|
|
1165
|
+
if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.MAIN) return 'main';
|
|
1166
|
+
return firstText(input.sessionKind, input.sessionType, context.sessionKind, context.SessionType, 'runtime');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function resolveJournalRelations(input = {}, refs = {}) {
|
|
1170
|
+
const relations = isPlainObject(input.relations)
|
|
1171
|
+
? input.relations
|
|
1172
|
+
: isPlainObject(input.correlation)
|
|
1173
|
+
? input.correlation
|
|
1174
|
+
: {};
|
|
1175
|
+
const context = isPlainObject(input.context) ? input.context : {};
|
|
1176
|
+
const artifacts = isPlainObject(input.artifacts) ? input.artifacts : {};
|
|
1177
|
+
const requestId = firstText(
|
|
1178
|
+
relations.requestId,
|
|
1179
|
+
relations.chatRequestId,
|
|
1180
|
+
input.requestId,
|
|
1181
|
+
input.chatRequestId,
|
|
1182
|
+
refs.requestId,
|
|
1183
|
+
refs.chatRequestId,
|
|
1184
|
+
refs.friendRequestId,
|
|
1185
|
+
);
|
|
1186
|
+
return cleanJournalObject({
|
|
1187
|
+
requestId,
|
|
1188
|
+
chatRequestId: firstText(relations.chatRequestId, input.chatRequestId, refs.chatRequestId, requestId),
|
|
1189
|
+
deliveryId: firstText(relations.deliveryId, input.deliveryId, refs.deliveryId, context.RelayDeliveryId),
|
|
1190
|
+
eventId: firstText(relations.eventId, input.eventId, input.id, refs.eventId, context.RelayEventId),
|
|
1191
|
+
notificationId: firstText(relations.notificationId, refs.notificationId),
|
|
1192
|
+
inboxItemId: firstText(relations.inboxItemId, refs.inboxItemId),
|
|
1193
|
+
conversationKey: firstText(relations.conversationKey, input.conversationKey, refs.conversationKey),
|
|
1194
|
+
worldId: firstText(relations.worldId, input.worldId, refs.worldId),
|
|
1195
|
+
accountId: firstText(relations.accountId, input.accountId, refs.accountId, context.AccountId),
|
|
1196
|
+
agentCode: firstText(relations.agentCode, refs.agentCode),
|
|
1197
|
+
localAgentId: firstText(relations.localAgentId, input.localAgentId, context.AgentId, context.agentId),
|
|
1198
|
+
targetAgentId: firstText(relations.targetAgentId, input.targetAgentId, refs.targetAgentId, context.RelayTargetAgentId),
|
|
1199
|
+
fromAgentId: firstText(relations.fromAgentId, input.fromAgentId, refs.fromAgentId, context.RelayFromAgentId),
|
|
1200
|
+
sessionKey: firstText(relations.sessionKey, input.sessionKey, context.SessionKey, context.sessionKey),
|
|
1201
|
+
localSessionKey: firstText(relations.localSessionKey, input.localSessionKey, context.LocalSessionKey, context.SessionKey),
|
|
1202
|
+
relaySessionKey: firstText(relations.relaySessionKey, input.relaySessionKey, context.RelaySessionKey),
|
|
1203
|
+
sessionId: firstText(relations.sessionId, input.sessionId, artifacts.sessionId, context.SessionId),
|
|
1204
|
+
sessionFile: firstText(relations.sessionFile, input.sessionFile, artifacts.sessionFile, artifacts.sessionPath, context.SessionFile),
|
|
1205
|
+
sessionStorePath: firstText(relations.sessionStorePath, input.sessionStorePath, artifacts.sessionStorePath),
|
|
1206
|
+
transcriptPath: firstText(relations.transcriptPath, input.transcriptPath, artifacts.transcriptPath),
|
|
1207
|
+
reportPath: firstText(relations.reportPath, input.reportPath, artifacts.reportPath),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function buildJournalKey(relations = {}, fallbackEventId = null) {
|
|
1212
|
+
const event = firstText(relations.eventId, fallbackEventId);
|
|
1213
|
+
const request = firstText(relations.requestId, relations.chatRequestId);
|
|
1214
|
+
const session = firstText(relations.localSessionKey, relations.relaySessionKey, relations.sessionKey);
|
|
1215
|
+
const continuity = request
|
|
1216
|
+
? `request:${request}`
|
|
1217
|
+
: firstText(
|
|
1218
|
+
relations.conversationKey ? `conversation:${relations.conversationKey}` : null,
|
|
1219
|
+
session ? `session:${session}` : null,
|
|
1220
|
+
relations.worldId ? `world:${relations.worldId}` : null,
|
|
1221
|
+
event ? `event:${event}` : null,
|
|
1222
|
+
);
|
|
1223
|
+
return cleanJournalObject({
|
|
1224
|
+
event,
|
|
1225
|
+
continuity,
|
|
1226
|
+
request,
|
|
1227
|
+
session,
|
|
1228
|
+
sessionId: relations.sessionId,
|
|
1229
|
+
world: relations.worldId,
|
|
1230
|
+
conversation: relations.conversationKey,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
547
1234
|
export function buildClaworldMaintenanceEvent(input = {}) {
|
|
548
1235
|
const toolName = normalizeText(input.toolName, null);
|
|
549
1236
|
const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
|
|
@@ -557,14 +1244,26 @@ export function buildClaworldMaintenanceEvent(input = {}) {
|
|
|
557
1244
|
.filter(([, value]) => value != null),
|
|
558
1245
|
)
|
|
559
1246
|
: {};
|
|
1247
|
+
const id = normalizeText(input.id, `${source}:${kind}:${timestamp}`);
|
|
1248
|
+
const relations = resolveJournalRelations({ ...input, eventId: input.eventId || id }, refs);
|
|
1249
|
+
const scope = resolveJournalScope(input, relations);
|
|
560
1250
|
return {
|
|
561
|
-
|
|
1251
|
+
schema: CLAWORLD_JOURNAL_SCHEMA,
|
|
1252
|
+
id,
|
|
562
1253
|
timestamp,
|
|
563
1254
|
source,
|
|
564
1255
|
kind,
|
|
1256
|
+
eventType: normalizeText(input.eventType, kind),
|
|
1257
|
+
scope,
|
|
565
1258
|
summary,
|
|
566
1259
|
excerpt: truncateText(input.excerpt || ''),
|
|
567
1260
|
refs,
|
|
1261
|
+
key: buildJournalKey(relations, id),
|
|
1262
|
+
relations,
|
|
1263
|
+
actor: cleanJournalObject(input.actor || {}),
|
|
1264
|
+
tool: cleanJournalObject(input.tool || {}),
|
|
1265
|
+
artifacts: cleanJournalObject(input.artifacts || {}),
|
|
1266
|
+
maintenance: cleanJournalObject(input.maintenance || {}),
|
|
568
1267
|
};
|
|
569
1268
|
}
|
|
570
1269
|
|
|
@@ -597,9 +1296,13 @@ function compactResultPayload(payload = {}) {
|
|
|
597
1296
|
const keys = [
|
|
598
1297
|
'status',
|
|
599
1298
|
'tool',
|
|
1299
|
+
'action',
|
|
1300
|
+
'scope',
|
|
1301
|
+
'query',
|
|
600
1302
|
'accountId',
|
|
601
1303
|
'worldId',
|
|
602
1304
|
'displayName',
|
|
1305
|
+
'requestId',
|
|
603
1306
|
'chatRequestId',
|
|
604
1307
|
'conversationKey',
|
|
605
1308
|
'feedbackId',
|
|
@@ -616,75 +1319,288 @@ function compactResultPayload(payload = {}) {
|
|
|
616
1319
|
return compact;
|
|
617
1320
|
}
|
|
618
1321
|
|
|
1322
|
+
function readNestedObject(value, key) {
|
|
1323
|
+
const nested = value?.[key];
|
|
1324
|
+
return nested && typeof nested === 'object' && !Array.isArray(nested) ? nested : {};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function firstObjectFromArray(value) {
|
|
1328
|
+
if (!Array.isArray(value)) return {};
|
|
1329
|
+
return value.find((entry) => isPlainObject(entry)) || {};
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function nestedConversationWorldId(value = {}) {
|
|
1333
|
+
return firstText(value.worldId, value.conversation?.worldId);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function resolvePayloadItemCounts(payload = {}) {
|
|
1337
|
+
const counts = [];
|
|
1338
|
+
for (const [key, label] of [
|
|
1339
|
+
['worlds', 'worlds'],
|
|
1340
|
+
['members', 'members'],
|
|
1341
|
+
['people', 'people'],
|
|
1342
|
+
['results', 'results'],
|
|
1343
|
+
['items', 'items'],
|
|
1344
|
+
['pendingRequests', 'pendingRequests'],
|
|
1345
|
+
['recentRequests', 'recentRequests'],
|
|
1346
|
+
['chats', 'chats'],
|
|
1347
|
+
]) {
|
|
1348
|
+
if (Array.isArray(payload[key])) {
|
|
1349
|
+
counts.push(`${label}=${payload[key].length}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return counts;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function formatToolSummaryPart(key, value) {
|
|
1356
|
+
const normalized = flattenInline(value);
|
|
1357
|
+
return normalized ? `${key}=${normalized}` : null;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function buildToolCallSummary({ toolName, params = {}, payload = {}, compactPayload = {} } = {}) {
|
|
1361
|
+
const parts = [
|
|
1362
|
+
formatToolSummaryPart('action', firstText(params.action, payload.action)),
|
|
1363
|
+
formatToolSummaryPart('scope', firstText(params.scope, payload.scope)),
|
|
1364
|
+
formatToolSummaryPart('query', firstText(params.query, payload.query)),
|
|
1365
|
+
formatToolSummaryPart('status', firstText(payload.status, compactPayload.status)),
|
|
1366
|
+
formatToolSummaryPart('worldId', firstText(params.worldId, payload.worldId)),
|
|
1367
|
+
formatToolSummaryPart('chatRequestId', firstText(params.chatRequestId, payload.chatRequestId)),
|
|
1368
|
+
formatToolSummaryPart('conversationKey', firstText(params.conversationKey, payload.conversationKey)),
|
|
1369
|
+
...resolvePayloadItemCounts(payload),
|
|
1370
|
+
].filter(Boolean);
|
|
1371
|
+
return `${toolName} completed${parts.length > 0 ? ` (${parts.join(', ')})` : ''}.`;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
619
1374
|
export function buildClaworldToolMaintenanceEvent({
|
|
620
1375
|
toolName,
|
|
621
1376
|
params = {},
|
|
622
1377
|
result = null,
|
|
623
1378
|
timestamp = null,
|
|
1379
|
+
context = {},
|
|
624
1380
|
} = {}) {
|
|
625
1381
|
const normalizedToolName = normalizeText(toolName, null);
|
|
626
1382
|
if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
|
|
627
1383
|
const payload = parseToolResultPayload(result) || {};
|
|
1384
|
+
const chatRequest = readNestedObject(payload, 'chatRequest');
|
|
1385
|
+
const request = readNestedObject(payload, 'request');
|
|
1386
|
+
const kickoff = readNestedObject(payload, 'kickoff');
|
|
1387
|
+
const chat = readNestedObject(payload, 'chat');
|
|
1388
|
+
const firstChat = firstObjectFromArray(payload.chats);
|
|
1389
|
+
const conversation = readNestedObject(payload, 'conversation');
|
|
628
1390
|
const compactPayload = compactResultPayload(payload);
|
|
1391
|
+
const requestId = firstText(
|
|
1392
|
+
params.requestId,
|
|
1393
|
+
params.chatRequestId,
|
|
1394
|
+
payload.requestId,
|
|
1395
|
+
payload.chatRequestId,
|
|
1396
|
+
chatRequest.chatRequestId,
|
|
1397
|
+
chatRequest.requestId,
|
|
1398
|
+
request.requestId,
|
|
1399
|
+
chat.chatRequestId,
|
|
1400
|
+
firstChat.chatRequestId,
|
|
1401
|
+
);
|
|
1402
|
+
const requesterSessionKey = firstText(
|
|
1403
|
+
context.sessionKey,
|
|
1404
|
+
context.SessionKey,
|
|
1405
|
+
context.localSessionKey,
|
|
1406
|
+
context.LocalSessionKey,
|
|
1407
|
+
);
|
|
1408
|
+
const resultConversationSessionKey = firstText(
|
|
1409
|
+
chat.localSessionKey,
|
|
1410
|
+
firstChat.localSessionKey,
|
|
1411
|
+
payload.localSessionKey,
|
|
1412
|
+
kickoff.localSessionKey,
|
|
1413
|
+
conversation.localSessionKey,
|
|
1414
|
+
);
|
|
1415
|
+
const conversationToolNames = new Set([
|
|
1416
|
+
'claworld_manage_conversations',
|
|
1417
|
+
'claworld_chat_inbox',
|
|
1418
|
+
'claworld_request_chat',
|
|
1419
|
+
]);
|
|
1420
|
+
const localSessionKey = firstText(
|
|
1421
|
+
conversationToolNames.has(normalizedToolName) ? resultConversationSessionKey : null,
|
|
1422
|
+
requesterSessionKey,
|
|
1423
|
+
resultConversationSessionKey,
|
|
1424
|
+
);
|
|
1425
|
+
const relaySessionKey = firstText(
|
|
1426
|
+
context.RelaySessionKey,
|
|
1427
|
+
payload.sessionKey,
|
|
1428
|
+
chat.sessionKey,
|
|
1429
|
+
firstChat.sessionKey,
|
|
1430
|
+
kickoff.sessionKey,
|
|
1431
|
+
conversation.sessionKey,
|
|
1432
|
+
);
|
|
1433
|
+
const targetDiffersFromRequester = requesterSessionKey && localSessionKey && requesterSessionKey !== localSessionKey;
|
|
1434
|
+
const sessionKind = resolveJournalScope({
|
|
1435
|
+
sessionKey: localSessionKey || relaySessionKey,
|
|
1436
|
+
sessionType: targetDiffersFromRequester
|
|
1437
|
+
? null
|
|
1438
|
+
: firstText(context.sessionType, context.SessionType, context.ChatType, context.sessionKind),
|
|
1439
|
+
context: targetDiffersFromRequester ? {} : context,
|
|
1440
|
+
});
|
|
1441
|
+
const actorSessionKind = resolveJournalScope({
|
|
1442
|
+
sessionKey: requesterSessionKey || localSessionKey || relaySessionKey,
|
|
1443
|
+
sessionType: firstText(context.sessionType, context.SessionType, context.ChatType, context.sessionKind),
|
|
1444
|
+
context,
|
|
1445
|
+
});
|
|
629
1446
|
const refs = {
|
|
630
1447
|
accountId: params.accountId || payload.accountId,
|
|
631
|
-
worldId:
|
|
632
|
-
|
|
633
|
-
|
|
1448
|
+
worldId: firstText(
|
|
1449
|
+
params.worldId,
|
|
1450
|
+
payload.worldId,
|
|
1451
|
+
nestedConversationWorldId(chat),
|
|
1452
|
+
nestedConversationWorldId(firstChat),
|
|
1453
|
+
nestedConversationWorldId(chatRequest),
|
|
1454
|
+
nestedConversationWorldId(request),
|
|
1455
|
+
conversation.worldId,
|
|
1456
|
+
),
|
|
1457
|
+
requestId,
|
|
1458
|
+
chatRequestId: firstText(
|
|
1459
|
+
params.chatRequestId,
|
|
1460
|
+
payload.chatRequestId,
|
|
1461
|
+
chatRequest.chatRequestId,
|
|
1462
|
+
request.chatRequestId,
|
|
1463
|
+
chat.chatRequestId,
|
|
1464
|
+
firstChat.chatRequestId,
|
|
1465
|
+
requestId,
|
|
1466
|
+
),
|
|
1467
|
+
conversationKey: firstText(
|
|
1468
|
+
params.conversationKey,
|
|
1469
|
+
payload.conversationKey,
|
|
1470
|
+
conversation.conversationKey,
|
|
1471
|
+
kickoff.conversationKey,
|
|
1472
|
+
chat.conversationKey,
|
|
1473
|
+
firstChat.conversationKey,
|
|
1474
|
+
),
|
|
634
1475
|
agentCode: params.agentCode || payload.agentCode,
|
|
1476
|
+
sessionKey: localSessionKey || relaySessionKey,
|
|
1477
|
+
relaySessionKey,
|
|
635
1478
|
};
|
|
636
1479
|
return buildClaworldMaintenanceEvent({
|
|
637
1480
|
source: 'claworld_tool',
|
|
638
1481
|
kind: normalizedToolName,
|
|
1482
|
+
eventType: 'tool_call',
|
|
1483
|
+
scope: sessionKind,
|
|
639
1484
|
toolName: normalizedToolName,
|
|
640
1485
|
timestamp,
|
|
641
1486
|
refs,
|
|
642
|
-
|
|
1487
|
+
relations: {
|
|
1488
|
+
requestId,
|
|
1489
|
+
chatRequestId: refs.chatRequestId,
|
|
1490
|
+
conversationKey: refs.conversationKey,
|
|
1491
|
+
worldId: refs.worldId,
|
|
1492
|
+
accountId: refs.accountId,
|
|
1493
|
+
agentCode: refs.agentCode,
|
|
1494
|
+
localSessionKey,
|
|
1495
|
+
relaySessionKey,
|
|
1496
|
+
sessionKey: localSessionKey || relaySessionKey,
|
|
1497
|
+
localAgentId: firstText(context.agentId, context.AgentId),
|
|
1498
|
+
sessionId: targetDiffersFromRequester ? null : firstText(context.sessionId, context.SessionId),
|
|
1499
|
+
sessionFile: targetDiffersFromRequester ? null : firstText(context.sessionFile, context.SessionFile, context.sessionPath),
|
|
1500
|
+
transcriptPath: targetDiffersFromRequester ? null : firstText(context.transcriptPath),
|
|
1501
|
+
},
|
|
1502
|
+
actor: {
|
|
1503
|
+
sessionKind: actorSessionKind,
|
|
1504
|
+
agentId: firstText(context.agentId, context.AgentId),
|
|
1505
|
+
sessionKey: requesterSessionKey || localSessionKey || relaySessionKey,
|
|
1506
|
+
},
|
|
1507
|
+
tool: {
|
|
1508
|
+
name: normalizedToolName,
|
|
1509
|
+
action: firstText(params.action, payload.action),
|
|
1510
|
+
status: firstText(payload.status, 'succeeded'),
|
|
1511
|
+
requesterSessionKey,
|
|
1512
|
+
targetSessionKey: targetDiffersFromRequester ? localSessionKey : null,
|
|
1513
|
+
},
|
|
1514
|
+
summary: buildToolCallSummary({
|
|
1515
|
+
toolName: normalizedToolName,
|
|
1516
|
+
params,
|
|
1517
|
+
payload,
|
|
1518
|
+
compactPayload,
|
|
1519
|
+
}),
|
|
643
1520
|
excerpt: Object.keys(compactPayload).length > 0
|
|
644
1521
|
? JSON.stringify(compactPayload)
|
|
645
1522
|
: null,
|
|
646
1523
|
});
|
|
647
1524
|
}
|
|
648
1525
|
|
|
649
|
-
function
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
.
|
|
653
|
-
.
|
|
1526
|
+
function buildJournalEntryPayload(event = {}) {
|
|
1527
|
+
return cleanJournalObject({
|
|
1528
|
+
schema: CLAWORLD_JOURNAL_SCHEMA,
|
|
1529
|
+
id: event.id,
|
|
1530
|
+
key: event.key,
|
|
1531
|
+
timestamp: event.timestamp,
|
|
1532
|
+
source: event.source,
|
|
1533
|
+
scope: event.scope,
|
|
1534
|
+
eventType: event.eventType,
|
|
1535
|
+
kind: event.kind,
|
|
1536
|
+
summary: event.summary,
|
|
1537
|
+
refs: event.refs,
|
|
1538
|
+
relations: event.relations,
|
|
1539
|
+
actor: event.actor,
|
|
1540
|
+
tool: event.tool,
|
|
1541
|
+
artifacts: event.artifacts,
|
|
1542
|
+
maintenance: event.maintenance,
|
|
1543
|
+
excerpt: event.excerpt || null,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function buildJournalEntry(event = {}) {
|
|
1548
|
+
const title = flattenInline(event.summary || event.kind || 'Claworld event recorded.');
|
|
1549
|
+
const heading = [
|
|
1550
|
+
event.timestamp,
|
|
1551
|
+
event.scope || event.source,
|
|
1552
|
+
event.kind,
|
|
1553
|
+
title,
|
|
1554
|
+
].filter(Boolean).join(' · ');
|
|
1555
|
+
return [
|
|
1556
|
+
`## ${heading}`,
|
|
1557
|
+
'',
|
|
1558
|
+
'```json',
|
|
1559
|
+
JSON.stringify(buildJournalEntryPayload(event), null, 2),
|
|
1560
|
+
'```',
|
|
1561
|
+
'',
|
|
1562
|
+
].join('\n');
|
|
654
1563
|
}
|
|
655
1564
|
|
|
656
1565
|
export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
|
|
657
1566
|
const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
|
|
658
1567
|
await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
|
|
659
1568
|
const normalizedEvent = buildClaworldMaintenanceEvent(event);
|
|
660
|
-
const
|
|
1569
|
+
const dayKey = toDayKey(normalizedEvent.timestamp);
|
|
661
1570
|
const journalPath = path.join(
|
|
662
1571
|
workspaceRoot,
|
|
663
1572
|
CLAWORLD_WORKING_MEMORY_DIR,
|
|
664
1573
|
CLAWORLD_JOURNAL_DIR,
|
|
665
|
-
`${
|
|
1574
|
+
`${dayKey}.md`,
|
|
666
1575
|
);
|
|
667
1576
|
const currentContent = await readTextIfPresent(journalPath);
|
|
668
|
-
const
|
|
669
|
-
const entry = [
|
|
670
|
-
`## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
|
|
671
|
-
`- Source: ${normalizedEvent.source}`,
|
|
672
|
-
`- Summary: ${flattenInline(normalizedEvent.summary)}`,
|
|
673
|
-
normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
|
|
674
|
-
refsLine ? `- Refs: ${refsLine}` : null,
|
|
675
|
-
'',
|
|
676
|
-
].filter((line) => line != null).join('\n');
|
|
1577
|
+
const entry = buildJournalEntry(normalizedEvent);
|
|
677
1578
|
if (currentContent == null) {
|
|
678
|
-
await atomicWriteText(journalPath, `# Claworld Journal ${
|
|
1579
|
+
await atomicWriteText(journalPath, `# Claworld Journal ${dayKey}\n\n${entry}`, {
|
|
679
1580
|
backup: false,
|
|
680
1581
|
rejectEmptyOverwrite: false,
|
|
681
1582
|
});
|
|
682
1583
|
} else {
|
|
683
1584
|
await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
|
|
684
1585
|
}
|
|
1586
|
+
let sessionDirectory = null;
|
|
1587
|
+
if (appendOptions.updateSessionDirectory !== false) {
|
|
1588
|
+
try {
|
|
1589
|
+
sessionDirectory = await updateClaworldSessionDirectory(workspaceRoot, normalizedEvent, {
|
|
1590
|
+
...appendOptions,
|
|
1591
|
+
timestamp: normalizedEvent.timestamp,
|
|
1592
|
+
});
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
sessionDirectory = {
|
|
1595
|
+
ok: false,
|
|
1596
|
+
error: error?.message || String(error),
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
685
1600
|
return {
|
|
686
1601
|
ok: true,
|
|
687
1602
|
journalPath,
|
|
1603
|
+
sessionDirectory,
|
|
688
1604
|
event: normalizedEvent,
|
|
689
1605
|
};
|
|
690
1606
|
}
|
|
@@ -731,11 +1647,27 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
|
|
|
731
1647
|
})
|
|
732
1648
|
: { slices: {} };
|
|
733
1649
|
const pointerInjected = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
|
|
1650
|
+
const managementPolicyInjected = target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT;
|
|
1651
|
+
let managementMainSessionKey = null;
|
|
1652
|
+
if (managementPolicyInjected && resolvedWorkspaceRoot) {
|
|
1653
|
+
try {
|
|
1654
|
+
const sessionDirectory = await readClaworldSessionDirectory(resolvedWorkspaceRoot);
|
|
1655
|
+
managementMainSessionKey = normalizeText(
|
|
1656
|
+
sessionDirectory.directory?.main?.lastActiveSessionKey,
|
|
1657
|
+
null,
|
|
1658
|
+
);
|
|
1659
|
+
} catch {
|
|
1660
|
+
managementMainSessionKey = null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
734
1663
|
const parts = [];
|
|
735
1664
|
let truncated = false;
|
|
736
1665
|
if (pointerInjected) {
|
|
737
1666
|
const pointerBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
|
|
738
|
-
const fittedPointer = truncateBootstrapText(
|
|
1667
|
+
const fittedPointer = truncateBootstrapText(
|
|
1668
|
+
buildClaworldContextPointer({ workspaceRoot: resolvedWorkspaceRoot }),
|
|
1669
|
+
pointerBudget,
|
|
1670
|
+
);
|
|
739
1671
|
if (fittedPointer.text) {
|
|
740
1672
|
parts.push(fittedPointer.text);
|
|
741
1673
|
}
|
|
@@ -743,6 +1675,22 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
|
|
|
743
1675
|
truncated = true;
|
|
744
1676
|
}
|
|
745
1677
|
}
|
|
1678
|
+
if (managementPolicyInjected) {
|
|
1679
|
+
const policyBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
|
|
1680
|
+
const fittedPolicy = truncateBootstrapText(
|
|
1681
|
+
buildClaworldManagementStartupPrompt({
|
|
1682
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
1683
|
+
mainSessionKey: managementMainSessionKey,
|
|
1684
|
+
}),
|
|
1685
|
+
policyBudget,
|
|
1686
|
+
);
|
|
1687
|
+
if (fittedPolicy.text) {
|
|
1688
|
+
parts.push(fittedPolicy.text);
|
|
1689
|
+
}
|
|
1690
|
+
if (fittedPolicy.truncated) {
|
|
1691
|
+
truncated = true;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
746
1694
|
const sectionTitle = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN
|
|
747
1695
|
? '# Claworld Startup Memory'
|
|
748
1696
|
: target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT
|
|
@@ -773,6 +1721,7 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
|
|
|
773
1721
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
774
1722
|
files: selectedFiles.map((relativePath) => buildBootstrapFileLabel(relativePath)),
|
|
775
1723
|
pointerInjected,
|
|
1724
|
+
managementPolicyInjected,
|
|
776
1725
|
fallbackFiles: fileSections.fallbackFiles,
|
|
777
1726
|
omittedFiles: fileSections.omittedFiles,
|
|
778
1727
|
truncated,
|
|
@@ -834,6 +1783,40 @@ function assertAllowedTarget(runType, target) {
|
|
|
834
1783
|
}
|
|
835
1784
|
}
|
|
836
1785
|
|
|
1786
|
+
function normalizeMarkdownNewlines(content) {
|
|
1787
|
+
return String(content ?? '').replace(/\r\n/g, '\n');
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function assertContextMarkdownUsesBullets(target, normalizedContent, schema) {
|
|
1791
|
+
const maxBulletLength = schema.maxBulletLength || null;
|
|
1792
|
+
const lines = normalizedContent.split('\n');
|
|
1793
|
+
for (const line of lines) {
|
|
1794
|
+
const trimmed = line.trim();
|
|
1795
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
1796
|
+
if (!trimmed.startsWith('- ')) {
|
|
1797
|
+
throw new Error(`Claworld maintenance patch for ${target} must use bullet lines under schema sections.`);
|
|
1798
|
+
}
|
|
1799
|
+
if (maxBulletLength && trimmed.length > maxBulletLength) {
|
|
1800
|
+
throw new Error(`Claworld maintenance patch for ${target} has a bullet longer than ${maxBulletLength} characters.`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function assertContextFileSchema(target, content) {
|
|
1806
|
+
const schema = CLAWORLD_CONTEXT_FILE_SCHEMAS[target];
|
|
1807
|
+
if (!schema) return;
|
|
1808
|
+
const normalizedContent = normalizeMarkdownNewlines(content);
|
|
1809
|
+
if (!normalizedContent.startsWith(`${schema.title}\n`)) {
|
|
1810
|
+
throw new Error(`Claworld maintenance patch for ${target} must start with ${schema.title}.`);
|
|
1811
|
+
}
|
|
1812
|
+
for (const heading of schema.headings) {
|
|
1813
|
+
if (!normalizedContent.includes(`\n${heading}\n`)) {
|
|
1814
|
+
throw new Error(`Claworld maintenance patch for ${target} is missing required section ${heading}.`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
assertContextMarkdownUsesBullets(target, normalizedContent, schema);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
837
1820
|
function normalizeReportPatch(report, index) {
|
|
838
1821
|
const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
|
|
839
1822
|
const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
@@ -890,10 +1873,10 @@ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options
|
|
|
890
1873
|
patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
|
|
891
1874
|
}
|
|
892
1875
|
if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
|
|
893
|
-
const
|
|
1876
|
+
const dayKey = toDayKey(options.timestamp || output.timestamp || Date.now());
|
|
894
1877
|
patches.push({
|
|
895
1878
|
operation: 'append_section',
|
|
896
|
-
target: `${CLAWORLD_JOURNAL_DIR}/${
|
|
1879
|
+
target: `${CLAWORLD_JOURNAL_DIR}/${dayKey}.md`,
|
|
897
1880
|
content: String(output.journalAppendMd ?? ''),
|
|
898
1881
|
});
|
|
899
1882
|
}
|
|
@@ -926,6 +1909,9 @@ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options
|
|
|
926
1909
|
target,
|
|
927
1910
|
content: String(patch.content ?? ''),
|
|
928
1911
|
};
|
|
1912
|
+
if (operation === 'replace') {
|
|
1913
|
+
assertContextFileSchema(target, normalizedPatch.content);
|
|
1914
|
+
}
|
|
929
1915
|
const rationale = normalizeText(patch.rationale, null);
|
|
930
1916
|
if (rationale) normalizedPatch.rationale = rationale;
|
|
931
1917
|
return normalizedPatch;
|
|
@@ -955,8 +1941,8 @@ async function applyMaintenancePatch(workspaceRoot, patch) {
|
|
|
955
1941
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
956
1942
|
const currentContent = await readTextIfPresent(absolutePath);
|
|
957
1943
|
if (currentContent == null) {
|
|
958
|
-
const
|
|
959
|
-
await atomicWriteText(absolutePath, `# Claworld Journal ${
|
|
1944
|
+
const dayKey = path.basename(patch.target, '.md');
|
|
1945
|
+
await atomicWriteText(absolutePath, `# Claworld Journal ${dayKey}\n\n${patch.content}`, {
|
|
960
1946
|
backup: false,
|
|
961
1947
|
rejectEmptyOverwrite: false,
|
|
962
1948
|
});
|