@xfxstudio/claworld 2026.4.22-testing.1 → 2026.4.22-testing.5
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/index.js +0 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/claworld-a2a-channel-agent/SKILL.md +0 -6
- package/skills/claworld-help/SKILL.md +0 -2
- package/skills/claworld-join-and-chat/SKILL.md +0 -8
- package/src/openclaw/index.js +0 -14
- package/src/openclaw/plugin/register.js +4 -113
- package/src/openclaw/runtime/tool-contracts.js +1 -0
- package/src/product-shell/contracts/world-orchestration.js +4 -4
- package/src/openclaw/runtime/working-memory.js +0 -702
package/index.js
CHANGED
|
@@ -25,20 +25,6 @@ export {
|
|
|
25
25
|
LOCAL_AGENT_BOOTSTRAP_SCHEMA,
|
|
26
26
|
LOCAL_AGENT_BOOTSTRAP_REQUIRED,
|
|
27
27
|
} from './src/openclaw/index.js';
|
|
28
|
-
export {
|
|
29
|
-
CLAWORLD_WORKING_MEMORY_DIR,
|
|
30
|
-
CLAWORLD_WORKING_MEMORY_FILES,
|
|
31
|
-
CLAWORLD_MAINTENANCE_RUN_TYPES,
|
|
32
|
-
appendClaworldJournalEvent,
|
|
33
|
-
buildClaworldContextPointer,
|
|
34
|
-
buildClaworldMaintenanceEvent,
|
|
35
|
-
buildClaworldRuntimeMaintenanceEvent,
|
|
36
|
-
buildClaworldToolMaintenanceEvent,
|
|
37
|
-
ensureClaworldWorkingMemory,
|
|
38
|
-
readClaworldWorkingMemory,
|
|
39
|
-
runClaworldMemoryMaintenance,
|
|
40
|
-
validateClaworldMaintenanceOutput,
|
|
41
|
-
} from './src/openclaw/index.js';
|
|
42
28
|
export { createClaworldLifecycleManager } from './src/openclaw/plugin/lifecycle.js';
|
|
43
29
|
export { ClaworldRelayClient, createClaworldRelayClient } from './src/openclaw/plugin/relay-client.js';
|
|
44
30
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -30,12 +30,6 @@ description: |
|
|
|
30
30
|
message.
|
|
31
31
|
- That document is internal. It is for you only. Do not quote it, paraphrase
|
|
32
32
|
it, summarize it to the peer, or mention its section names.
|
|
33
|
-
- If workspace-local Claworld working memory is available and the current
|
|
34
|
-
intent needs prior Claworld progress, read `.claworld/INDEX.md` first and
|
|
35
|
-
follow its read order. Prefer summarized `NOW`, `MEMORY`, `PROFILE`, journal,
|
|
36
|
-
and report content over raw transcripts.
|
|
37
|
-
- Do not write `.claworld/` content into global `MEMORY.md`, and do not load
|
|
38
|
-
raw Claworld transcripts by default.
|
|
39
33
|
- A single local session transcript may contain multiple accepted-chat intents
|
|
40
34
|
over time. When a new full baseline appears, treat it as a fresh intent even
|
|
41
35
|
if the same local session file is reused.
|
|
@@ -26,8 +26,6 @@ description: |
|
|
|
26
26
|
|
|
27
27
|
先诊断,再修复;先走 canonical tool,再走 CLI fallback。
|
|
28
28
|
|
|
29
|
-
如果用户问的是 Claworld 历史进展、已加入的 worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索,先读工作区本地 `.claworld/INDEX.md`,再按它的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。不要默认加载 raw transcripts,也不要把 `.claworld/` 内容自动 promotion 到全局 `MEMORY.md`。
|
|
30
|
-
|
|
31
29
|
默认第一步:
|
|
32
30
|
|
|
33
31
|
```json
|
|
@@ -29,14 +29,6 @@ description: |
|
|
|
29
29
|
- 如果必须引用技术信息,先翻译成人话,再附上最少量必要原文;不要整段转储工具返回。
|
|
30
30
|
- 汇报重点放在:现在发生了什么、这对用户意味着什么、下一步该怎么做。
|
|
31
31
|
|
|
32
|
-
## 本地 Claworld 工作记忆
|
|
33
|
-
|
|
34
|
-
如果用户问的是 Claworld 历史进展、worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索、之前推进到哪一步,先读工作区本地 `.claworld/INDEX.md`,再按里面的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。
|
|
35
|
-
|
|
36
|
-
- `.claworld/` 是本地私有工作记忆,不是 backend public state。
|
|
37
|
-
- 默认只读摘要和索引,不加载 raw transcripts。
|
|
38
|
-
- 不要把 `.claworld/` 内容自动写入全局 `MEMORY.md`。
|
|
39
|
-
|
|
40
32
|
## claworld channel inter-session 汇报:announce / ANNOUNCE_SKIP 规则
|
|
41
33
|
|
|
42
34
|
这份 skill 虽然不负责 claworld channel 内 live chat runtime 的对话推进,但 main session 仍可能收到来自 claworld channel session 的 inter-session 汇报、总结、完成事件或阶段性判断。
|
package/src/openclaw/index.js
CHANGED
|
@@ -22,20 +22,6 @@ export {
|
|
|
22
22
|
} from './plugin/relay-client.js';
|
|
23
23
|
export { createRelayEventProtocol } from './protocol/relay-event-protocol.js';
|
|
24
24
|
export { OPENCLAW_RUNTIME_PATH, createRuntimePathTrace } from './runtime/runtime-path.js';
|
|
25
|
-
export {
|
|
26
|
-
CLAWORLD_WORKING_MEMORY_DIR,
|
|
27
|
-
CLAWORLD_WORKING_MEMORY_FILES,
|
|
28
|
-
CLAWORLD_MAINTENANCE_RUN_TYPES,
|
|
29
|
-
appendClaworldJournalEvent,
|
|
30
|
-
buildClaworldContextPointer,
|
|
31
|
-
buildClaworldMaintenanceEvent,
|
|
32
|
-
buildClaworldRuntimeMaintenanceEvent,
|
|
33
|
-
buildClaworldToolMaintenanceEvent,
|
|
34
|
-
ensureClaworldWorkingMemory,
|
|
35
|
-
readClaworldWorkingMemory,
|
|
36
|
-
runClaworldMemoryMaintenance,
|
|
37
|
-
validateClaworldMaintenanceOutput,
|
|
38
|
-
} from './runtime/working-memory.js';
|
|
39
25
|
export { createInboundSessionRouter } from './runtime/inbound-session-router.js';
|
|
40
26
|
export { createOutboundSessionBridge } from './runtime/outbound-session-bridge.js';
|
|
41
27
|
export { createSystemMessageOrchestrator } from './runtime/system-message-orchestrator.js';
|
|
@@ -11,12 +11,6 @@ import {
|
|
|
11
11
|
projectToolWorldSearchResponse,
|
|
12
12
|
} from '../runtime/tool-contracts.js';
|
|
13
13
|
import { CLAWORLD_TOOL_CONTRACT_VERSION } from '../runtime/tool-inventory.js';
|
|
14
|
-
import {
|
|
15
|
-
appendClaworldJournalEvent,
|
|
16
|
-
buildClaworldContextPointer,
|
|
17
|
-
buildClaworldToolMaintenanceEvent,
|
|
18
|
-
ensureClaworldWorkingMemory,
|
|
19
|
-
} from '../runtime/working-memory.js';
|
|
20
14
|
import { setClaworldRuntime } from './runtime.js';
|
|
21
15
|
import {
|
|
22
16
|
CHAT_REQUEST_APPROVAL_POLICY_MODES,
|
|
@@ -78,74 +72,6 @@ function buildClaworldStatusRoute(plugin) {
|
|
|
78
72
|
};
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
function firstWorkspaceCandidate(...sources) {
|
|
82
|
-
for (const source of sources) {
|
|
83
|
-
if (!source || typeof source !== 'object') continue;
|
|
84
|
-
const candidates = [
|
|
85
|
-
source.workspaceRoot,
|
|
86
|
-
source.workspaceDir,
|
|
87
|
-
source.workspacePath,
|
|
88
|
-
source.workspace,
|
|
89
|
-
source.cwd,
|
|
90
|
-
source.agent?.workspaceRoot,
|
|
91
|
-
source.agent?.workspaceDir,
|
|
92
|
-
source.agent?.workspace,
|
|
93
|
-
source.context?.workspaceRoot,
|
|
94
|
-
source.context?.workspaceDir,
|
|
95
|
-
source.context?.workspace,
|
|
96
|
-
source.session?.workspaceRoot,
|
|
97
|
-
source.session?.workspaceDir,
|
|
98
|
-
source.session?.workspace,
|
|
99
|
-
];
|
|
100
|
-
for (const candidate of candidates) {
|
|
101
|
-
const normalized = normalizeText(candidate, null);
|
|
102
|
-
if (normalized) return normalized;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function resolveHookWorkspaceRoot(api, event = {}, ctx = {}) {
|
|
109
|
-
const directCandidate = firstWorkspaceCandidate(event, ctx);
|
|
110
|
-
if (directCandidate) return directCandidate;
|
|
111
|
-
const agentId = normalizeText(ctx?.agentId ?? event?.agentId, null);
|
|
112
|
-
if (!agentId) return null;
|
|
113
|
-
const cfg = await loadCurrentConfig(api);
|
|
114
|
-
const agentEntry = cfg?.agents?.list?.[agentId] || cfg?.agents?.[agentId] || null;
|
|
115
|
-
return firstWorkspaceCandidate(agentEntry);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function getHookLogger(api) {
|
|
119
|
-
return api?.logger || api?.runtime?.logger || console;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function parseHookToolPayload(result) {
|
|
123
|
-
if (!result || typeof result !== 'object') return null;
|
|
124
|
-
const text = Array.isArray(result.content)
|
|
125
|
-
? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
|
|
126
|
-
: null;
|
|
127
|
-
if (!text) return null;
|
|
128
|
-
try {
|
|
129
|
-
const parsed = JSON.parse(text);
|
|
130
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isSuccessfulHookToolCall(event = {}) {
|
|
137
|
-
if (event?.error || event?.isError === true) return false;
|
|
138
|
-
const result = event?.result ?? event?.output ?? event?.response ?? null;
|
|
139
|
-
if (result?.isError === true) return false;
|
|
140
|
-
const payload = parseHookToolPayload(result);
|
|
141
|
-
if (normalizeText(payload?.status, null) === 'error') return false;
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function hookToolResult(event = {}) {
|
|
146
|
-
return event?.result ?? event?.output ?? event?.response ?? null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
75
|
const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
|
|
150
76
|
'inbound',
|
|
151
77
|
'outbound',
|
|
@@ -1017,6 +943,7 @@ function buildRegisteredTools(api, plugin) {
|
|
|
1017
943
|
'For world-scoped chat or re-engagement, use the displayName and agentCode returned by claworld_join_world or claworld_get_candidate_feed candidate delivery.',
|
|
1018
944
|
'The backend resolves the target by agentCode.',
|
|
1019
945
|
'If the current displayName for that agentCode no longer matches, the tool can still route by the current owner and return an explicit warning with the current displayName.',
|
|
946
|
+
'openingMessage is required and must contain non-blank kickoff intent; missing or blank opener text fails with opening_message_required.',
|
|
1020
947
|
'Do not use this tool for replying inside an already-open Claworld chat, for runtime live turns, or for pulling progress from a local chat session.',
|
|
1021
948
|
'After creation, use claworld_chat_inbox to inspect pending, expired, rejected, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
|
|
1022
949
|
'Once accepted, the runtime owns the live conversation loop.',
|
|
@@ -1046,8 +973,8 @@ function buildRegisteredTools(api, plugin) {
|
|
|
1046
973
|
],
|
|
1047
974
|
}),
|
|
1048
975
|
parameters: objectParam({
|
|
1049
|
-
description: 'In the main session, create a new direct or world-scoped chat request, or re-engage a previously silent or ended relationship, for one target agent. Provide the target displayName and
|
|
1050
|
-
required: ['accountId', 'displayName', 'agentCode'],
|
|
976
|
+
description: 'In the main session, create a new direct or world-scoped chat request, or re-engage a previously silent or ended relationship, for one target agent. Provide the target displayName, agentCode, and non-blank openingMessage. Do not use this payload for current live replies.',
|
|
977
|
+
required: ['accountId', 'displayName', 'agentCode', 'openingMessage'],
|
|
1051
978
|
properties: {
|
|
1052
979
|
accountId: accountIdProperty,
|
|
1053
980
|
displayName: stringParam({
|
|
@@ -1061,7 +988,7 @@ function buildRegisteredTools(api, plugin) {
|
|
|
1061
988
|
examples: ['ZX82QP'],
|
|
1062
989
|
}),
|
|
1063
990
|
openingMessage: stringParam({
|
|
1064
|
-
description: '
|
|
991
|
+
description: 'Required non-blank request or re-engagement brief that the backend uses when the peer accepts. Missing or blank opener text fails with opening_message_required. This is kickoff intent, not a live runtime reply payload.',
|
|
1065
992
|
minLength: 1,
|
|
1066
993
|
examples: ['Hi, want to compare trail-running routes in Shanghai?'],
|
|
1067
994
|
}),
|
|
@@ -1647,21 +1574,6 @@ export function registerClaworldPluginFull(api, plugin) {
|
|
|
1647
1574
|
throw new Error('registerClaworldPluginFull requires a plugin instance');
|
|
1648
1575
|
}
|
|
1649
1576
|
if (typeof api.on === 'function') {
|
|
1650
|
-
api.on('before_prompt_build', async (event = {}, ctx = {}) => {
|
|
1651
|
-
const logger = getHookLogger(api);
|
|
1652
|
-
try {
|
|
1653
|
-
const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
|
|
1654
|
-
if (workspaceRoot) {
|
|
1655
|
-
await ensureClaworldWorkingMemory(workspaceRoot);
|
|
1656
|
-
}
|
|
1657
|
-
} catch (error) {
|
|
1658
|
-
logger?.warn?.('[claworld:working-memory] unable to ensure workspace memory', error);
|
|
1659
|
-
}
|
|
1660
|
-
return {
|
|
1661
|
-
appendSystemContext: buildClaworldContextPointer(),
|
|
1662
|
-
};
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
1577
|
api.on('before_tool_call', async (event, ctx) => {
|
|
1666
1578
|
if (event?.toolName !== 'claworld_request_chat') return;
|
|
1667
1579
|
const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
|
|
@@ -1673,27 +1585,6 @@ export function registerClaworldPluginFull(api, plugin) {
|
|
|
1673
1585
|
},
|
|
1674
1586
|
};
|
|
1675
1587
|
});
|
|
1676
|
-
|
|
1677
|
-
api.on('after_tool_call', async (event = {}, ctx = {}) => {
|
|
1678
|
-
if (!isSuccessfulHookToolCall(event)) return;
|
|
1679
|
-
const toolName = normalizeText(event?.toolName ?? ctx?.toolName, null);
|
|
1680
|
-
if (!toolName || !toolName.startsWith('claworld_')) return;
|
|
1681
|
-
const logger = getHookLogger(api);
|
|
1682
|
-
try {
|
|
1683
|
-
const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
|
|
1684
|
-
if (!workspaceRoot) return;
|
|
1685
|
-
const maintenanceEvent = buildClaworldToolMaintenanceEvent({
|
|
1686
|
-
toolName,
|
|
1687
|
-
params: event?.params || {},
|
|
1688
|
-
result: hookToolResult(event),
|
|
1689
|
-
timestamp: event?.timestamp || ctx?.timestamp || null,
|
|
1690
|
-
});
|
|
1691
|
-
if (!maintenanceEvent) return;
|
|
1692
|
-
await appendClaworldJournalEvent(workspaceRoot, maintenanceEvent);
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
logger?.warn?.('[claworld:working-memory] unable to append tool event', error);
|
|
1695
|
-
}
|
|
1696
|
-
});
|
|
1697
1588
|
}
|
|
1698
1589
|
if (typeof api.registerHttpRoute === 'function') {
|
|
1699
1590
|
api.registerHttpRoute(buildClaworldStatusRoute(plugin));
|
|
@@ -615,9 +615,9 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
|
|
|
615
615
|
const requestChatAction = {
|
|
616
616
|
action: 'request_chat',
|
|
617
617
|
worldId: normalizedFeed.worldId,
|
|
618
|
-
requiredFields: ['worldId', 'displayName', 'agentCode'],
|
|
618
|
+
requiredFields: ['worldId', 'displayName', 'agentCode', 'openingMessage'],
|
|
619
619
|
summary:
|
|
620
|
-
'After the user chooses a candidate, request_chat with this worldId, displayName, and
|
|
620
|
+
'After the user chooses a candidate, request_chat with this worldId, displayName, agentCode, and a non-blank openingMessage.',
|
|
621
621
|
};
|
|
622
622
|
const candidateSummaries = normalizedFeed.candidates.slice(0, summaryLimit).map((candidate, index) => {
|
|
623
623
|
const name = candidate.profileSummary.displayName || `Candidate ${index + 1}`;
|
|
@@ -695,8 +695,8 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
|
|
|
695
695
|
user: [heading, promptBody].filter(Boolean).join('\n\n'),
|
|
696
696
|
followUp: deliveredCandidateCount > 0
|
|
697
697
|
? (remainingCandidateCount > 0
|
|
698
|
-
? `Share these ${deliveredCandidateCount} candidate summaries first. If the user chooses someone now, continue with request_chat using that candidate's {worldId, displayName, agentCode}. If they want more options first, continue with the remaining ${remainingCandidateCount} candidate${remainingCandidateCount === 1 ? '' : 's'} from the same feed.`
|
|
699
|
-
: 'Share these candidate summaries and, if the user chooses one, continue with request_chat using the attached {worldId, displayName, agentCode} payload
|
|
698
|
+
? `Share these ${deliveredCandidateCount} candidate summaries first. If the user chooses someone now, continue with request_chat using that candidate's {worldId, displayName, agentCode} plus a non-blank openingMessage from the user. If they want more options first, continue with the remaining ${remainingCandidateCount} candidate${remainingCandidateCount === 1 ? '' : 's'} from the same feed.`
|
|
699
|
+
: 'Share these candidate summaries and, if the user chooses one, continue with request_chat using the attached {worldId, displayName, agentCode} payload plus a non-blank openingMessage from the user.')
|
|
700
700
|
: 'Tell the user candidate delivery can be retried later through the same backend-authored world flow.',
|
|
701
701
|
},
|
|
702
702
|
};
|
|
@@ -1,702 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
|
|
6
|
-
export const CLAWORLD_CONTEXT_DIR = 'context';
|
|
7
|
-
export const CLAWORLD_JOURNAL_DIR = 'journal';
|
|
8
|
-
export const CLAWORLD_REPORTS_DIR = 'reports';
|
|
9
|
-
|
|
10
|
-
export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
|
|
11
|
-
index: 'INDEX.md',
|
|
12
|
-
now: 'context/NOW.md',
|
|
13
|
-
profile: 'context/PROFILE.md',
|
|
14
|
-
memory: 'context/MEMORY.md',
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
|
|
18
|
-
CLAWORLD_WORKING_MEMORY_DIR,
|
|
19
|
-
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
|
|
20
|
-
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
|
|
21
|
-
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
export const CLAWORLD_MAINTENANCE_RUN_TYPES = Object.freeze({
|
|
25
|
-
L1_NOW_REFRESH: 'L1_NOW_REFRESH',
|
|
26
|
-
L2_MEMORY_PROFILE_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
|
|
27
|
-
L2_PROFILE_MEMORY_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const MAINTENANCE_RUN_TYPE_ALIASES = Object.freeze({
|
|
31
|
-
L1_NOW_REFRESH: CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH,
|
|
32
|
-
L2_MEMORY_PROFILE_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
|
|
33
|
-
L2_PROFILE_MEMORY_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const FILE_TARGET_ALIASES = Object.freeze({
|
|
37
|
-
INDEX: CLAWORLD_WORKING_MEMORY_FILES.index,
|
|
38
|
-
'INDEX.md': CLAWORLD_WORKING_MEMORY_FILES.index,
|
|
39
|
-
NOW: CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
40
|
-
'NOW.md': CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
41
|
-
PROFILE: CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
42
|
-
'PROFILE.md': CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const L1_ALLOWED_TARGETS = new Set([
|
|
46
|
-
CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
const L2_ALLOWED_TARGETS = new Set([
|
|
50
|
-
CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
51
|
-
CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
52
|
-
CLAWORLD_WORKING_MEMORY_FILES.memory,
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
const MAX_EVENT_EXCERPT_CHARS = 600;
|
|
56
|
-
const MAX_MEMORY_SLICE_CHARS = 4000;
|
|
57
|
-
|
|
58
|
-
export function buildClaworldContextPointer() {
|
|
59
|
-
return [
|
|
60
|
-
'# Claworld Context Pointer',
|
|
61
|
-
'',
|
|
62
|
-
'Claworld working memory is available at `.claworld/INDEX.md`.',
|
|
63
|
-
'When the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress, read `.claworld/INDEX.md` first.',
|
|
64
|
-
'Do not load raw Claworld transcripts by default.',
|
|
65
|
-
].join('\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function buildClaworldWorkingMemoryTemplates() {
|
|
69
|
-
return {
|
|
70
|
-
[CLAWORLD_WORKING_MEMORY_FILES.index]: [
|
|
71
|
-
'# Claworld Working Memory',
|
|
72
|
-
'',
|
|
73
|
-
'This directory is the workspace-local private working memory for Claworld.',
|
|
74
|
-
'Read this file first when the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress.',
|
|
75
|
-
'',
|
|
76
|
-
'## Read Order',
|
|
77
|
-
'- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
|
|
78
|
-
'- `context/MEMORY.md` for durable Claworld facts and decisions.',
|
|
79
|
-
'- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
|
|
80
|
-
'- `journal/YYYY-MM.md` for append-only summarized events.',
|
|
81
|
-
'- `reports/` for generated local progress reports.',
|
|
82
|
-
'',
|
|
83
|
-
'## Rules',
|
|
84
|
-
'- Do not load raw Claworld transcripts by default.',
|
|
85
|
-
'- Do not write this content into global `MEMORY.md` automatically.',
|
|
86
|
-
'- Prefer short summaries and references over raw chat history.',
|
|
87
|
-
'- `context/PROFILE.md` and `context/MEMORY.md` are updated only by L2 maintenance review.',
|
|
88
|
-
'',
|
|
89
|
-
].join('\n'),
|
|
90
|
-
[CLAWORLD_WORKING_MEMORY_FILES.now]: [
|
|
91
|
-
'# Claworld Now',
|
|
92
|
-
'',
|
|
93
|
-
'## Current Focus',
|
|
94
|
-
'- No active Claworld focus recorded yet.',
|
|
95
|
-
'',
|
|
96
|
-
'## Recent Activity',
|
|
97
|
-
'- No recent Claworld activity recorded yet.',
|
|
98
|
-
'',
|
|
99
|
-
'## Open Questions',
|
|
100
|
-
'- none',
|
|
101
|
-
'',
|
|
102
|
-
].join('\n'),
|
|
103
|
-
[CLAWORLD_WORKING_MEMORY_FILES.profile]: [
|
|
104
|
-
'# Claworld Profile',
|
|
105
|
-
'',
|
|
106
|
-
'## Stable Preferences',
|
|
107
|
-
'- No Claworld-specific preferences recorded yet.',
|
|
108
|
-
'',
|
|
109
|
-
'## People And Context',
|
|
110
|
-
'- No Claworld people context recorded yet.',
|
|
111
|
-
'',
|
|
112
|
-
].join('\n'),
|
|
113
|
-
[CLAWORLD_WORKING_MEMORY_FILES.memory]: [
|
|
114
|
-
'# Claworld Memory',
|
|
115
|
-
'',
|
|
116
|
-
'## Durable Facts',
|
|
117
|
-
'- No durable Claworld facts recorded yet.',
|
|
118
|
-
'',
|
|
119
|
-
'## Decisions',
|
|
120
|
-
'- No durable Claworld decisions recorded yet.',
|
|
121
|
-
'',
|
|
122
|
-
].join('\n'),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function buildClaworldWorkingMemoryFileSpecs() {
|
|
127
|
-
const templates = buildClaworldWorkingMemoryTemplates();
|
|
128
|
-
return Object.entries(templates).map(([relativePath, content]) => ({
|
|
129
|
-
relativePath: `${CLAWORLD_WORKING_MEMORY_DIR}/${relativePath}`,
|
|
130
|
-
workingMemoryRelativePath: relativePath,
|
|
131
|
-
policy: 'durable',
|
|
132
|
-
content,
|
|
133
|
-
}));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function normalizeText(value, fallback = null) {
|
|
137
|
-
if (value == null) return fallback;
|
|
138
|
-
const normalized = String(value).trim();
|
|
139
|
-
return normalized || fallback;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function expandUserPath(inputPath, homeDir = os.homedir()) {
|
|
143
|
-
const text = normalizeText(inputPath, null);
|
|
144
|
-
if (!text) return null;
|
|
145
|
-
if (text === '~') return homeDir;
|
|
146
|
-
if (text.startsWith('~/') || text.startsWith('~\\')) {
|
|
147
|
-
return path.join(homeDir, text.slice(2));
|
|
148
|
-
}
|
|
149
|
-
return text;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function resolveClaworldWorkspaceRoot(options = {}, homeDir = os.homedir()) {
|
|
153
|
-
const source = typeof options === 'string'
|
|
154
|
-
? options
|
|
155
|
-
: options?.workspaceRoot
|
|
156
|
-
?? options?.workspacePath
|
|
157
|
-
?? options?.workspaceDir
|
|
158
|
-
?? options?.workspace
|
|
159
|
-
?? options?.cwd
|
|
160
|
-
?? process.cwd();
|
|
161
|
-
return path.resolve(expandUserPath(source, homeDir) || process.cwd());
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function resolveClaworldMemoryRoot(options = {}, homeDir = os.homedir()) {
|
|
165
|
-
return path.join(resolveClaworldWorkspaceRoot(options, homeDir), CLAWORLD_WORKING_MEMORY_DIR);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function readTextIfPresent(filePath) {
|
|
169
|
-
try {
|
|
170
|
-
return await fs.readFile(filePath, 'utf8');
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (error && error.code === 'ENOENT') return null;
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function atomicWriteText(filePath, content, {
|
|
178
|
-
backup = true,
|
|
179
|
-
rejectEmptyOverwrite = true,
|
|
180
|
-
} = {}) {
|
|
181
|
-
const nextContent = String(content ?? '');
|
|
182
|
-
const currentContent = await readTextIfPresent(filePath);
|
|
183
|
-
if (
|
|
184
|
-
rejectEmptyOverwrite
|
|
185
|
-
&& currentContent != null
|
|
186
|
-
&& normalizeText(currentContent, null)
|
|
187
|
-
&& !normalizeText(nextContent, null)
|
|
188
|
-
) {
|
|
189
|
-
throw new Error(`Refusing to overwrite non-empty Claworld memory file with empty content: ${filePath}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
193
|
-
if (backup && currentContent != null && currentContent !== nextContent) {
|
|
194
|
-
await fs.writeFile(`${filePath}.bak`, currentContent, 'utf8');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const tempPath = path.join(
|
|
198
|
-
path.dirname(filePath),
|
|
199
|
-
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
200
|
-
);
|
|
201
|
-
await fs.writeFile(tempPath, nextContent, 'utf8');
|
|
202
|
-
await fs.rename(tempPath, filePath);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export async function ensureClaworldWorkingMemory(options = {}, ensureOptions = {}) {
|
|
206
|
-
const workspaceRoot = resolveClaworldWorkspaceRoot(options, ensureOptions.homeDir || os.homedir());
|
|
207
|
-
const memoryRoot = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR);
|
|
208
|
-
const directories = CLAWORLD_WORKING_MEMORY_DIRECTORIES.map((relativePath) => ({
|
|
209
|
-
relativePath,
|
|
210
|
-
absolutePath: path.join(workspaceRoot, relativePath),
|
|
211
|
-
}));
|
|
212
|
-
const files = buildClaworldWorkingMemoryFileSpecs().map((file) => ({
|
|
213
|
-
...file,
|
|
214
|
-
absolutePath: path.join(workspaceRoot, file.relativePath),
|
|
215
|
-
}));
|
|
216
|
-
const actions = [];
|
|
217
|
-
|
|
218
|
-
if (ensureOptions.dryRun === true) {
|
|
219
|
-
for (const directory of directories) {
|
|
220
|
-
actions.push(`mkdir -p ${directory.absolutePath}`);
|
|
221
|
-
}
|
|
222
|
-
for (const file of files) {
|
|
223
|
-
actions.push(`seed ${file.absolutePath} if missing`);
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
ok: true,
|
|
227
|
-
dryRun: true,
|
|
228
|
-
workspaceRoot,
|
|
229
|
-
memoryRoot,
|
|
230
|
-
directories,
|
|
231
|
-
files,
|
|
232
|
-
actions,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
for (const directory of directories) {
|
|
237
|
-
await fs.mkdir(directory.absolutePath, { recursive: true });
|
|
238
|
-
actions.push(`ensured ${directory.absolutePath}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const file of files) {
|
|
242
|
-
const currentContent = await readTextIfPresent(file.absolutePath);
|
|
243
|
-
if (currentContent == null) {
|
|
244
|
-
await atomicWriteText(file.absolutePath, file.content, {
|
|
245
|
-
backup: false,
|
|
246
|
-
rejectEmptyOverwrite: false,
|
|
247
|
-
});
|
|
248
|
-
actions.push(`created ${file.absolutePath}`);
|
|
249
|
-
} else {
|
|
250
|
-
actions.push(`preserved ${file.absolutePath}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
ok: true,
|
|
256
|
-
dryRun: false,
|
|
257
|
-
workspaceRoot,
|
|
258
|
-
memoryRoot,
|
|
259
|
-
directories,
|
|
260
|
-
files,
|
|
261
|
-
actions,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function toIsoTimestamp(value = null) {
|
|
266
|
-
const date = value instanceof Date ? value : new Date(value || Date.now());
|
|
267
|
-
if (Number.isNaN(date.getTime())) return new Date().toISOString();
|
|
268
|
-
return date.toISOString();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function toMonthKey(timestamp) {
|
|
272
|
-
return toIsoTimestamp(timestamp).slice(0, 7);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
|
|
276
|
-
const text = normalizeText(value, '');
|
|
277
|
-
if (text.length <= maxChars) return text;
|
|
278
|
-
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function flattenInline(value) {
|
|
282
|
-
return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function buildClaworldMaintenanceEvent(input = {}) {
|
|
286
|
-
const toolName = normalizeText(input.toolName, null);
|
|
287
|
-
const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
|
|
288
|
-
const kind = normalizeText(input.kind, toolName || 'milestone');
|
|
289
|
-
const timestamp = toIsoTimestamp(input.timestamp);
|
|
290
|
-
const summary = normalizeText(input.summary, toolName ? `${toolName} succeeded.` : 'Claworld milestone recorded.');
|
|
291
|
-
const refs = input.refs && typeof input.refs === 'object' && !Array.isArray(input.refs)
|
|
292
|
-
? Object.fromEntries(
|
|
293
|
-
Object.entries(input.refs)
|
|
294
|
-
.map(([key, value]) => [key, normalizeText(value, null)])
|
|
295
|
-
.filter(([, value]) => value != null),
|
|
296
|
-
)
|
|
297
|
-
: {};
|
|
298
|
-
return {
|
|
299
|
-
id: normalizeText(input.id, `${source}:${kind}:${timestamp}`),
|
|
300
|
-
timestamp,
|
|
301
|
-
source,
|
|
302
|
-
kind,
|
|
303
|
-
summary,
|
|
304
|
-
excerpt: truncateText(input.excerpt || ''),
|
|
305
|
-
refs,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function buildClaworldRuntimeMaintenanceEvent(input = {}) {
|
|
310
|
-
return buildClaworldMaintenanceEvent({
|
|
311
|
-
...input,
|
|
312
|
-
source: normalizeText(input.source, 'claworld_runtime'),
|
|
313
|
-
kind: normalizeText(input.kind, 'milestone'),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function parseToolResultPayload(result) {
|
|
318
|
-
if (!result || typeof result !== 'object') return null;
|
|
319
|
-
if (result.payload && typeof result.payload === 'object' && !Array.isArray(result.payload)) {
|
|
320
|
-
return result.payload;
|
|
321
|
-
}
|
|
322
|
-
const textContent = Array.isArray(result.content)
|
|
323
|
-
? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
|
|
324
|
-
: null;
|
|
325
|
-
if (!textContent) return null;
|
|
326
|
-
try {
|
|
327
|
-
const parsed = JSON.parse(textContent);
|
|
328
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
329
|
-
} catch {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function compactResultPayload(payload = {}) {
|
|
335
|
-
const keys = [
|
|
336
|
-
'status',
|
|
337
|
-
'tool',
|
|
338
|
-
'accountId',
|
|
339
|
-
'worldId',
|
|
340
|
-
'displayName',
|
|
341
|
-
'chatRequestId',
|
|
342
|
-
'conversationKey',
|
|
343
|
-
'candidateId',
|
|
344
|
-
'feedbackId',
|
|
345
|
-
'nextAction',
|
|
346
|
-
'requiredAction',
|
|
347
|
-
'summary',
|
|
348
|
-
'message',
|
|
349
|
-
];
|
|
350
|
-
const compact = {};
|
|
351
|
-
for (const key of keys) {
|
|
352
|
-
const value = normalizeText(payload[key], null);
|
|
353
|
-
if (value != null) compact[key] = value;
|
|
354
|
-
}
|
|
355
|
-
return compact;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function buildClaworldToolMaintenanceEvent({
|
|
359
|
-
toolName,
|
|
360
|
-
params = {},
|
|
361
|
-
result = null,
|
|
362
|
-
timestamp = null,
|
|
363
|
-
} = {}) {
|
|
364
|
-
const normalizedToolName = normalizeText(toolName, null);
|
|
365
|
-
if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
|
|
366
|
-
const payload = parseToolResultPayload(result) || {};
|
|
367
|
-
const compactPayload = compactResultPayload(payload);
|
|
368
|
-
const refs = {
|
|
369
|
-
accountId: params.accountId || payload.accountId,
|
|
370
|
-
worldId: params.worldId || payload.worldId,
|
|
371
|
-
chatRequestId: params.chatRequestId || payload.chatRequestId,
|
|
372
|
-
conversationKey: params.conversationKey || payload.conversationKey,
|
|
373
|
-
candidateId: params.candidateId || payload.candidateId,
|
|
374
|
-
agentCode: params.agentCode || payload.agentCode,
|
|
375
|
-
};
|
|
376
|
-
return buildClaworldMaintenanceEvent({
|
|
377
|
-
source: 'claworld_tool',
|
|
378
|
-
kind: normalizedToolName,
|
|
379
|
-
toolName: normalizedToolName,
|
|
380
|
-
timestamp,
|
|
381
|
-
refs,
|
|
382
|
-
summary: `${normalizedToolName} succeeded.`,
|
|
383
|
-
excerpt: Object.keys(compactPayload).length > 0
|
|
384
|
-
? JSON.stringify(compactPayload)
|
|
385
|
-
: null,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function formatRefs(refs = {}) {
|
|
390
|
-
return Object.entries(refs)
|
|
391
|
-
.filter(([, value]) => value != null)
|
|
392
|
-
.map(([key, value]) => `${key}=${flattenInline(value)}`)
|
|
393
|
-
.join(', ');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
|
|
397
|
-
const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
|
|
398
|
-
await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
|
|
399
|
-
const normalizedEvent = buildClaworldMaintenanceEvent(event);
|
|
400
|
-
const monthKey = toMonthKey(normalizedEvent.timestamp);
|
|
401
|
-
const journalPath = path.join(
|
|
402
|
-
workspaceRoot,
|
|
403
|
-
CLAWORLD_WORKING_MEMORY_DIR,
|
|
404
|
-
CLAWORLD_JOURNAL_DIR,
|
|
405
|
-
`${monthKey}.md`,
|
|
406
|
-
);
|
|
407
|
-
const currentContent = await readTextIfPresent(journalPath);
|
|
408
|
-
const refsLine = formatRefs(normalizedEvent.refs);
|
|
409
|
-
const entry = [
|
|
410
|
-
`## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
|
|
411
|
-
`- Source: ${normalizedEvent.source}`,
|
|
412
|
-
`- Summary: ${flattenInline(normalizedEvent.summary)}`,
|
|
413
|
-
normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
|
|
414
|
-
refsLine ? `- Refs: ${refsLine}` : null,
|
|
415
|
-
'',
|
|
416
|
-
].filter((line) => line != null).join('\n');
|
|
417
|
-
if (currentContent == null) {
|
|
418
|
-
await atomicWriteText(journalPath, `# Claworld Journal ${monthKey}\n\n${entry}`, {
|
|
419
|
-
backup: false,
|
|
420
|
-
rejectEmptyOverwrite: false,
|
|
421
|
-
});
|
|
422
|
-
} else {
|
|
423
|
-
await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
ok: true,
|
|
427
|
-
journalPath,
|
|
428
|
-
event: normalizedEvent,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export async function readClaworldWorkingMemory(options = {}, readOptions = {}) {
|
|
433
|
-
const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
|
|
434
|
-
const maxCharsPerFile = Number.isInteger(readOptions.maxCharsPerFile) && readOptions.maxCharsPerFile > 0
|
|
435
|
-
? readOptions.maxCharsPerFile
|
|
436
|
-
: MAX_MEMORY_SLICE_CHARS;
|
|
437
|
-
const slices = {};
|
|
438
|
-
for (const relativePath of Object.values(CLAWORLD_WORKING_MEMORY_FILES)) {
|
|
439
|
-
const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, relativePath);
|
|
440
|
-
const content = await readTextIfPresent(absolutePath);
|
|
441
|
-
slices[relativePath] = content == null
|
|
442
|
-
? null
|
|
443
|
-
: {
|
|
444
|
-
content: content.length > maxCharsPerFile ? content.slice(0, maxCharsPerFile) : content,
|
|
445
|
-
truncated: content.length > maxCharsPerFile,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
workspaceRoot,
|
|
450
|
-
memoryRoot: path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR),
|
|
451
|
-
slices,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function normalizePatchOperation(operation) {
|
|
456
|
-
const normalized = normalizeText(operation, 'replace');
|
|
457
|
-
if (normalized === 'replace' || normalized === 'append_section' || normalized === 'no_op') {
|
|
458
|
-
return normalized;
|
|
459
|
-
}
|
|
460
|
-
throw new Error(`Unsupported Claworld maintenance patch operation: ${normalized}`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function normalizeMaintenanceRunType(runType) {
|
|
464
|
-
const normalized = normalizeText(runType, null);
|
|
465
|
-
const alias = MAINTENANCE_RUN_TYPE_ALIASES[normalized];
|
|
466
|
-
if (alias) return alias;
|
|
467
|
-
throw new Error(`Unsupported Claworld maintenance run type: ${runType}`);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function stripClaworldPrefix(rawTarget) {
|
|
471
|
-
let target = String(rawTarget || '').replace(/\\/g, '/').trim();
|
|
472
|
-
target = target.replace(/^\/+/, '');
|
|
473
|
-
if (target.startsWith(`${CLAWORLD_WORKING_MEMORY_DIR}/`)) {
|
|
474
|
-
target = target.slice(CLAWORLD_WORKING_MEMORY_DIR.length + 1);
|
|
475
|
-
}
|
|
476
|
-
return target;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function normalizePatchTarget(rawTarget) {
|
|
480
|
-
const stripped = stripClaworldPrefix(rawTarget);
|
|
481
|
-
const aliasTarget = FILE_TARGET_ALIASES[stripped] || null;
|
|
482
|
-
if (aliasTarget) return aliasTarget;
|
|
483
|
-
if (stripped === 'MEMORY.md') {
|
|
484
|
-
throw new Error('Global MEMORY.md is not a valid Claworld working-memory target; use context/MEMORY.md.');
|
|
485
|
-
}
|
|
486
|
-
const normalized = path.posix.normalize(stripped);
|
|
487
|
-
if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized === '..' || path.posix.isAbsolute(normalized)) {
|
|
488
|
-
throw new Error(`Invalid Claworld maintenance patch target: ${rawTarget}`);
|
|
489
|
-
}
|
|
490
|
-
return normalized;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function isAllowedTarget(runType, target) {
|
|
494
|
-
if (target.startsWith(`${CLAWORLD_JOURNAL_DIR}/`) || target.startsWith(`${CLAWORLD_REPORTS_DIR}/`)) {
|
|
495
|
-
return true;
|
|
496
|
-
}
|
|
497
|
-
if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH) {
|
|
498
|
-
return L1_ALLOWED_TARGETS.has(target);
|
|
499
|
-
}
|
|
500
|
-
if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW) {
|
|
501
|
-
return L2_ALLOWED_TARGETS.has(target);
|
|
502
|
-
}
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function assertAllowedTarget(runType, target) {
|
|
507
|
-
if (!isAllowedTarget(runType, target)) {
|
|
508
|
-
throw new Error(`Claworld maintenance run ${runType} cannot write target ${target}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function normalizeReportPatch(report, index) {
|
|
513
|
-
const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
|
|
514
|
-
const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
515
|
-
return {
|
|
516
|
-
operation: 'replace',
|
|
517
|
-
target: `${CLAWORLD_REPORTS_DIR}/${normalizedFilename}`,
|
|
518
|
-
content: String(report?.md ?? report?.content ?? report?.text ?? ''),
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function hasOwn(value, key) {
|
|
523
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function readPatchContent(patch, fieldName) {
|
|
527
|
-
if (hasOwn(patch, 'content')) return patch.content;
|
|
528
|
-
if (hasOwn(patch, 'md')) return patch.md;
|
|
529
|
-
if (hasOwn(patch, 'text')) return patch.text;
|
|
530
|
-
throw new Error(`Claworld maintenance FilePatch ${fieldName} requires content for replace or append_section.`);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function normalizeFilePatchValue(value, target, fieldName) {
|
|
534
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
535
|
-
const operation = normalizePatchOperation(value.operation);
|
|
536
|
-
const normalized = {
|
|
537
|
-
operation,
|
|
538
|
-
target,
|
|
539
|
-
content: operation === 'no_op' ? '' : String(readPatchContent(value, fieldName) ?? ''),
|
|
540
|
-
};
|
|
541
|
-
const rationale = normalizeText(value.rationale, null);
|
|
542
|
-
if (rationale) normalized.rationale = rationale;
|
|
543
|
-
return normalized;
|
|
544
|
-
}
|
|
545
|
-
return {
|
|
546
|
-
operation: 'replace',
|
|
547
|
-
target,
|
|
548
|
-
content: String(value ?? ''),
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export function normalizeClaworldMaintenanceOutput(runType, output = {}, options = {}) {
|
|
553
|
-
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
554
|
-
throw new Error('Claworld maintenance output must be an object.');
|
|
555
|
-
}
|
|
556
|
-
const normalizedRunType = normalizeMaintenanceRunType(runType);
|
|
557
|
-
const patches = [];
|
|
558
|
-
if (Object.prototype.hasOwnProperty.call(output, 'nowMd')) {
|
|
559
|
-
patches.push(normalizeFilePatchValue(output.nowMd, CLAWORLD_WORKING_MEMORY_FILES.now, 'nowMd'));
|
|
560
|
-
}
|
|
561
|
-
if (Object.prototype.hasOwnProperty.call(output, 'profileMd')) {
|
|
562
|
-
patches.push(normalizeFilePatchValue(output.profileMd, CLAWORLD_WORKING_MEMORY_FILES.profile, 'profileMd'));
|
|
563
|
-
}
|
|
564
|
-
if (Object.prototype.hasOwnProperty.call(output, 'memoryMd')) {
|
|
565
|
-
patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
|
|
566
|
-
}
|
|
567
|
-
if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
|
|
568
|
-
const monthKey = toMonthKey(options.timestamp || output.timestamp || Date.now());
|
|
569
|
-
patches.push({
|
|
570
|
-
operation: 'append_section',
|
|
571
|
-
target: `${CLAWORLD_JOURNAL_DIR}/${monthKey}.md`,
|
|
572
|
-
content: String(output.journalAppendMd ?? ''),
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
if (Array.isArray(output.reports)) {
|
|
576
|
-
output.reports.forEach((report, index) => {
|
|
577
|
-
patches.push(normalizeReportPatch(report, index));
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
if (Array.isArray(output.patches)) {
|
|
581
|
-
for (const patch of output.patches) {
|
|
582
|
-
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
583
|
-
throw new Error('Claworld maintenance patches entries must be FilePatch objects.');
|
|
584
|
-
}
|
|
585
|
-
const operation = normalizePatchOperation(patch.operation);
|
|
586
|
-
patches.push({
|
|
587
|
-
operation,
|
|
588
|
-
target: normalizePatchTarget(patch.target ?? patch.path ?? patch.relativePath),
|
|
589
|
-
content: operation === 'no_op' ? '' : String(readPatchContent(patch, 'patches[]') ?? ''),
|
|
590
|
-
rationale: normalizeText(patch.rationale, null),
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const normalizedPatches = patches.map((patch) => {
|
|
596
|
-
const target = normalizePatchTarget(patch.target);
|
|
597
|
-
const operation = normalizePatchOperation(patch.operation);
|
|
598
|
-
assertAllowedTarget(normalizedRunType, target);
|
|
599
|
-
const normalizedPatch = {
|
|
600
|
-
operation,
|
|
601
|
-
target,
|
|
602
|
-
content: String(patch.content ?? ''),
|
|
603
|
-
};
|
|
604
|
-
const rationale = normalizeText(patch.rationale, null);
|
|
605
|
-
if (rationale) normalizedPatch.rationale = rationale;
|
|
606
|
-
return normalizedPatch;
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
return {
|
|
610
|
-
runType: normalizedRunType,
|
|
611
|
-
noOpReason: normalizeText(output.noOpReason, null),
|
|
612
|
-
patches: normalizedPatches,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
export function validateClaworldMaintenanceOutput(runType, output = {}, options = {}) {
|
|
617
|
-
return normalizeClaworldMaintenanceOutput(runType, output, options);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async function applyMaintenancePatch(workspaceRoot, patch) {
|
|
621
|
-
if (patch.operation === 'no_op') {
|
|
622
|
-
return {
|
|
623
|
-
operation: patch.operation,
|
|
624
|
-
target: patch.target,
|
|
625
|
-
applied: false,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, patch.target);
|
|
629
|
-
if (patch.operation === 'append_section') {
|
|
630
|
-
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
631
|
-
const currentContent = await readTextIfPresent(absolutePath);
|
|
632
|
-
if (currentContent == null) {
|
|
633
|
-
const monthKey = path.basename(patch.target, '.md');
|
|
634
|
-
await atomicWriteText(absolutePath, `# Claworld Journal ${monthKey}\n\n${patch.content}`, {
|
|
635
|
-
backup: false,
|
|
636
|
-
rejectEmptyOverwrite: false,
|
|
637
|
-
});
|
|
638
|
-
} else {
|
|
639
|
-
await fs.appendFile(
|
|
640
|
-
absolutePath,
|
|
641
|
-
`${currentContent.endsWith('\n') ? '' : '\n'}${patch.content}`,
|
|
642
|
-
'utf8',
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
return {
|
|
646
|
-
operation: patch.operation,
|
|
647
|
-
target: patch.target,
|
|
648
|
-
applied: true,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
await atomicWriteText(absolutePath, patch.content, {
|
|
653
|
-
backup: true,
|
|
654
|
-
rejectEmptyOverwrite: true,
|
|
655
|
-
});
|
|
656
|
-
return {
|
|
657
|
-
operation: patch.operation,
|
|
658
|
-
target: patch.target,
|
|
659
|
-
applied: true,
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export async function runClaworldMemoryMaintenance(runType, requestBundle = {}, options = {}) {
|
|
664
|
-
const normalizedRunType = normalizeMaintenanceRunType(runType);
|
|
665
|
-
const workspaceRoot = resolveClaworldWorkspaceRoot(
|
|
666
|
-
options.workspaceRoot || requestBundle.workspaceRoot || requestBundle.workspaceDir || process.cwd(),
|
|
667
|
-
options.homeDir || os.homedir(),
|
|
668
|
-
);
|
|
669
|
-
await ensureClaworldWorkingMemory(workspaceRoot, options);
|
|
670
|
-
const workingMemory = await readClaworldWorkingMemory(workspaceRoot, options);
|
|
671
|
-
const request = {
|
|
672
|
-
...requestBundle,
|
|
673
|
-
runType: normalizedRunType,
|
|
674
|
-
workspaceRoot,
|
|
675
|
-
workingMemory,
|
|
676
|
-
};
|
|
677
|
-
const output = options.output
|
|
678
|
-
?? (typeof options.maintenanceRunner === 'function'
|
|
679
|
-
? await options.maintenanceRunner(request)
|
|
680
|
-
: null);
|
|
681
|
-
if (!output) {
|
|
682
|
-
return {
|
|
683
|
-
ok: true,
|
|
684
|
-
runType: normalizedRunType,
|
|
685
|
-
noOpReason: 'no_maintenance_runner',
|
|
686
|
-
request,
|
|
687
|
-
applied: [],
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
const normalized = validateClaworldMaintenanceOutput(normalizedRunType, output, options);
|
|
691
|
-
const applied = [];
|
|
692
|
-
for (const patch of normalized.patches) {
|
|
693
|
-
applied.push(await applyMaintenancePatch(workspaceRoot, patch));
|
|
694
|
-
}
|
|
695
|
-
return {
|
|
696
|
-
ok: true,
|
|
697
|
-
runType: normalizedRunType,
|
|
698
|
-
noOpReason: normalized.noOpReason,
|
|
699
|
-
request,
|
|
700
|
-
applied,
|
|
701
|
-
};
|
|
702
|
-
}
|