stella-timeline-plugin 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +103 -0
- package/README_ZH.md +103 -0
- package/bin/openclaw-timeline-doctor.mjs +2 -0
- package/bin/openclaw-timeline-setup.mjs +2 -0
- package/dist/index.js +26 -0
- package/dist/src/core/build_consumption_view.js +52 -0
- package/dist/src/core/calendar_dates.js +23 -0
- package/dist/src/core/collect_sources.js +39 -0
- package/dist/src/core/collect_timeline_request.js +76 -0
- package/dist/src/core/materialize_generated_candidate.js +87 -0
- package/dist/src/core/resolve_window.js +83 -0
- package/dist/src/core/runtime_guard.js +170 -0
- package/dist/src/core/timeline_reasoner_contract.js +2 -0
- package/dist/src/core/trace.js +22 -0
- package/dist/src/core/world_rhythm.js +258 -0
- package/dist/src/lib/fingerprint.js +46 -0
- package/dist/src/lib/holidays.js +95 -0
- package/dist/src/lib/inherit-appearance.js +46 -0
- package/dist/src/lib/parse-memory.js +171 -0
- package/dist/src/lib/time-utils.js +49 -0
- package/dist/src/lib/timeline_semantics.js +63 -0
- package/dist/src/lib/types.js +2 -0
- package/dist/src/openclaw-sdk-compat.js +39 -0
- package/dist/src/plugin_metadata.js +9 -0
- package/dist/src/runtime/conversation_context.js +128 -0
- package/dist/src/runtime/openclaw_timeline_runtime.js +655 -0
- package/dist/src/storage/daily_log.js +60 -0
- package/dist/src/storage/lock.js +74 -0
- package/dist/src/storage/trace_log.js +70 -0
- package/dist/src/storage/write-episode.js +164 -0
- package/dist/src/tools/timeline_resolve.js +689 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +73 -0
- package/scripts/doctor-openclaw-workspace.mjs +94 -0
- package/scripts/migrate-existing-memory.mjs +153 -0
- package/scripts/release.mjs +99 -0
- package/scripts/run-openclaw-live-e2e.mjs +64 -0
- package/scripts/run-openclaw-smoke.mjs +21 -0
- package/scripts/setup-openclaw-workspace.mjs +119 -0
- package/scripts/workspace-contract.mjs +47 -0
- package/skills/timeline/SKILL.md +111 -0
- package/templates/AGENTS.fragment.md +29 -0
- package/templates/SOUL.fragment.md +17 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.assertCanonicalDailyLogPath = assertCanonicalDailyLogPath;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const DAILY_LOG_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
39
|
+
function toCanonicalPosixPath(filePath) {
|
|
40
|
+
return path.posix.normalize(filePath.replace(/\\/g, '/'));
|
|
41
|
+
}
|
|
42
|
+
function assertCanonicalDailyLogPath(filePath, calendarDate, canonicalRootName = 'memory') {
|
|
43
|
+
const normalized = toCanonicalPosixPath(filePath);
|
|
44
|
+
const base = path.posix.basename(normalized);
|
|
45
|
+
const parent = path.posix.basename(path.posix.dirname(normalized));
|
|
46
|
+
const expectedBase = `${calendarDate}.md`;
|
|
47
|
+
if (!DAILY_LOG_RE.test(base)) {
|
|
48
|
+
throw new Error(`Non-canonical daily log filename: ${base}`);
|
|
49
|
+
}
|
|
50
|
+
if (base !== expectedBase) {
|
|
51
|
+
throw new Error(`Daily log filename does not match calendar date: expected ${expectedBase}, got ${base}`);
|
|
52
|
+
}
|
|
53
|
+
if (parent !== canonicalRootName) {
|
|
54
|
+
const directoryLabel = canonicalRootName === 'memory'
|
|
55
|
+
? 'a memory/'
|
|
56
|
+
: `the configured ${canonicalRootName}/`;
|
|
57
|
+
throw new Error(`Canonical daily logs must live under ${directoryLabel} directory: ${normalized}`);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileLockError = void 0;
|
|
37
|
+
exports.withFileLock = withFileLock;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
class FileLockError extends Error {
|
|
41
|
+
code;
|
|
42
|
+
constructor(lockPath) {
|
|
43
|
+
super(`Lock already held for ${lockPath}`);
|
|
44
|
+
this.name = 'FileLockError';
|
|
45
|
+
this.code = 'LOCK_EXISTS';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.FileLockError = FileLockError;
|
|
49
|
+
async function withFileLock(targetPath, fn) {
|
|
50
|
+
const lockPath = `${targetPath}.lock`;
|
|
51
|
+
const dir = path.dirname(lockPath);
|
|
52
|
+
if (!fs.existsSync(dir)) {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
let fd = null;
|
|
56
|
+
try {
|
|
57
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
58
|
+
return await fn();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error && typeof error === 'object' && error.code === 'EEXIST') {
|
|
62
|
+
throw new FileLockError(lockPath);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (fd !== null) {
|
|
68
|
+
fs.closeSync(fd);
|
|
69
|
+
}
|
|
70
|
+
if (fs.existsSync(lockPath)) {
|
|
71
|
+
fs.unlinkSync(lockPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.appendTraceLog = appendTraceLog;
|
|
37
|
+
exports.readRecentTraceLogs = readRecentTraceLogs;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
function appendTraceLog(record, logPath) {
|
|
41
|
+
const dir = path.dirname(logPath);
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
function readRecentTraceLogs(logPath, n = 20) {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(logPath))
|
|
50
|
+
return [];
|
|
51
|
+
const content = fs.readFileSync(logPath, 'utf8').trim();
|
|
52
|
+
if (!content)
|
|
53
|
+
return [];
|
|
54
|
+
return content
|
|
55
|
+
.split('\n')
|
|
56
|
+
.slice(-n)
|
|
57
|
+
.map((line) => {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.writeEpisode = writeEpisode;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const fingerprint_1 = require("../lib/fingerprint");
|
|
40
|
+
const parse_memory_1 = require("../lib/parse-memory");
|
|
41
|
+
const holidays_1 = require("../lib/holidays");
|
|
42
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
43
|
+
function detectWriteConflict(filePath, dateStr, timestamp, location, action) {
|
|
44
|
+
const fingerprint = (0, fingerprint_1.computeFingerprint)(dateStr, location, action, timestamp);
|
|
45
|
+
if (!fs.existsSync(filePath)) {
|
|
46
|
+
return { outcome: 'clear', fingerprint };
|
|
47
|
+
}
|
|
48
|
+
const existingContent = fs.readFileSync(filePath, 'utf8');
|
|
49
|
+
const existingEpisodes = (0, parse_memory_1.parseMemoryFile)(existingContent);
|
|
50
|
+
for (const episode of existingEpisodes) {
|
|
51
|
+
const existingDate = episode.timestamp.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || dateStr;
|
|
52
|
+
const existingFingerprint = (0, fingerprint_1.computeFingerprint)(existingDate, episode.location, episode.action, episode.timestamp);
|
|
53
|
+
if (existingFingerprint === fingerprint) {
|
|
54
|
+
return { outcome: 'noop_existing', fingerprint, existingFingerprint };
|
|
55
|
+
}
|
|
56
|
+
const sameDate = existingDate === dateStr;
|
|
57
|
+
const sameTimeBucket = existingFingerprint.split('|')[3] === fingerprint.split('|')[3];
|
|
58
|
+
if (sameDate && sameTimeBucket) {
|
|
59
|
+
return { outcome: 'conflict', fingerprint, existingFingerprint };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { outcome: 'clear', fingerprint };
|
|
63
|
+
}
|
|
64
|
+
async function writeEpisode(input) {
|
|
65
|
+
const { timestamp, location, action, emotionTags, appearance, internalMonologue, filePath } = input;
|
|
66
|
+
if (!timestamp || !location || !action || !emotionTags || emotionTags.length === 0 || !appearance) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
written_at: '',
|
|
70
|
+
outcome: 'failed',
|
|
71
|
+
error_code: 'MISSING_FIELDS',
|
|
72
|
+
error: 'Missing required fields',
|
|
73
|
+
recovery_hint: 'Provide timestamp, location, action, emotionTags, and appearance before writing canon.',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const timestampParts = (0, time_utils_1.parseTimestampParts)(timestamp);
|
|
78
|
+
const dateObj = timestampParts ? null : new Date(timestamp);
|
|
79
|
+
if (!timestampParts && (!dateObj || isNaN(dateObj.getTime()))) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
written_at: '',
|
|
83
|
+
outcome: 'failed',
|
|
84
|
+
error_code: 'INVALID_TIMESTAMP',
|
|
85
|
+
error: 'Invalid timestamp format',
|
|
86
|
+
recovery_hint: 'Use an ISO timestamp or a canonical timeline timestamp before writing canon.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const yyyy = timestampParts ? timestampParts.year : dateObj.getFullYear();
|
|
90
|
+
const mm = String(timestampParts ? timestampParts.month : dateObj.getMonth() + 1).padStart(2, '0');
|
|
91
|
+
const dd = String(timestampParts ? timestampParts.day : dateObj.getDate()).padStart(2, '0');
|
|
92
|
+
const dateStr = timestampParts ? (0, time_utils_1.formatDate)(timestampParts) : `${yyyy}-${mm}-${dd}`;
|
|
93
|
+
const timeStr = timestampParts
|
|
94
|
+
? (0, time_utils_1.formatTime)(timestampParts)
|
|
95
|
+
: `${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}:${String(dateObj.getSeconds()).padStart(2, '0')}`;
|
|
96
|
+
const isWeekend = timestampParts ? [0, 6].includes((0, time_utils_1.dayOfWeek)(timestampParts)) : dateObj.getDay() === 0 || dateObj.getDay() === 6;
|
|
97
|
+
const weekday = !isWeekend;
|
|
98
|
+
const holidayKey = (0, holidays_1.getHoliday)(`${yyyy}-${mm}-${dd}`);
|
|
99
|
+
const worldHooks = {
|
|
100
|
+
weekday,
|
|
101
|
+
holiday_key: holidayKey,
|
|
102
|
+
};
|
|
103
|
+
const conflictCheck = detectWriteConflict(filePath, dateStr, timestamp, location, action);
|
|
104
|
+
if (conflictCheck.outcome === 'noop_existing') {
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
written_at: '',
|
|
108
|
+
world_hooks: worldHooks,
|
|
109
|
+
outcome: 'noop_existing',
|
|
110
|
+
idempotency_key: conflictCheck.fingerprint,
|
|
111
|
+
existing_fingerprint: conflictCheck.existingFingerprint,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (conflictCheck.outcome === 'conflict') {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
written_at: '',
|
|
118
|
+
world_hooks: worldHooks,
|
|
119
|
+
outcome: 'conflict',
|
|
120
|
+
error_code: 'CONFLICT_EXISTS',
|
|
121
|
+
error: 'A different episode already occupies the same timeline bucket.',
|
|
122
|
+
recovery_hint: 'Inspect the existing daily log entry before retrying or writing a new canon episode.',
|
|
123
|
+
idempotency_key: conflictCheck.fingerprint,
|
|
124
|
+
existing_fingerprint: conflictCheck.existingFingerprint,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const mdLines = [
|
|
128
|
+
`### [${timeStr}] ${action.substring(0, 15)}...`,
|
|
129
|
+
'',
|
|
130
|
+
`- Timestamp: ${dateStr} ${timeStr}`,
|
|
131
|
+
`- Location: ${location}`,
|
|
132
|
+
`- Action: ${action}`,
|
|
133
|
+
`- Emotion_Tags: [${emotionTags.join(', ')}]`,
|
|
134
|
+
`- Appearance: ${appearance}`,
|
|
135
|
+
];
|
|
136
|
+
if (internalMonologue) {
|
|
137
|
+
mdLines.push(`- Internal_Monologue: ${internalMonologue}`);
|
|
138
|
+
}
|
|
139
|
+
mdLines.push('');
|
|
140
|
+
const mdContent = mdLines.join('\n') + '\n';
|
|
141
|
+
const dir = path.dirname(filePath);
|
|
142
|
+
if (!fs.existsSync(dir)) {
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
fs.appendFileSync(filePath, mdContent, 'utf8');
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
written_at: new Date().toISOString(),
|
|
149
|
+
world_hooks: worldHooks,
|
|
150
|
+
outcome: 'appended',
|
|
151
|
+
idempotency_key: conflictCheck.fingerprint,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
written_at: '',
|
|
158
|
+
outcome: 'failed',
|
|
159
|
+
error_code: 'IO_ERROR',
|
|
160
|
+
error: error.message,
|
|
161
|
+
recovery_hint: 'Check file permissions and retry after confirming the timeline path is writable.',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|