@litfamily/litopencode 1.0.0 → 1.0.2

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README-Ko-KR.md +8 -7
  3. package/README.md +8 -7
  4. package/dist/activation-prompt-utils.d.ts +1 -1
  5. package/dist/activation-routing.d.ts +1 -1
  6. package/dist/activation-routing.js +1 -8
  7. package/dist/activation-workflow-prompts.d.ts +0 -1
  8. package/dist/activation-workflow-prompts.js +0 -8
  9. package/dist/activation.d.ts +1 -1
  10. package/dist/activation.js +1 -1
  11. package/dist/cli/args.d.ts +1 -1
  12. package/dist/cli/args.js +1 -31
  13. package/dist/cli/loop.js +0 -19
  14. package/dist/cli/managed-skill-assets.d.ts +1 -1
  15. package/dist/cli/managed-skill-assets.js +0 -2
  16. package/dist/cli/types.d.ts +0 -4
  17. package/dist/cli.js +0 -14
  18. package/dist/commands.d.ts +1 -10
  19. package/dist/commands.js +1 -11
  20. package/dist/features.d.ts +1 -22
  21. package/dist/features.js +0 -43
  22. package/dist/hooks.js +0 -13
  23. package/dist/index.d.ts +0 -5
  24. package/dist/index.js +0 -5
  25. package/dist/skills.d.ts +1 -8
  26. package/dist/skills.js +0 -13
  27. package/dist/tool-kit.d.ts +13 -16
  28. package/dist/tool-kit.js +8 -39
  29. package/docs/assets/cover-motion.webp +0 -0
  30. package/docs/assets/readme/badge-version.svg +1 -1
  31. package/docs/privacy.md +2 -2
  32. package/docs/reference.md +5 -48
  33. package/package.json +5 -3
  34. package/skills/frontend-ui-ux/references/complete-contract.md +1 -1
  35. package/skills/lit-plan/SKILL.md +1 -1
  36. package/skills/managed-skill-manifest.json +2 -9
  37. package/skills/visual-qa/references/complete-contract.md +1 -1
  38. package/tools/check-payload-substance.mjs +74 -27
  39. package/tools/payload-substance-parity.json +10 -25
  40. package/tools/version-manifests.json +4 -4
  41. package/dist/cli/skill-loop.d.ts +0 -3
  42. package/dist/cli/skill-loop.js +0 -649
  43. package/dist/skill-loop/apply.d.ts +0 -19
  44. package/dist/skill-loop/apply.js +0 -483
  45. package/dist/skill-loop/config.d.ts +0 -50
  46. package/dist/skill-loop/config.js +0 -215
  47. package/dist/skill-loop/curator.d.ts +0 -18
  48. package/dist/skill-loop/curator.js +0 -232
  49. package/dist/skill-loop/ledger.d.ts +0 -39
  50. package/dist/skill-loop/ledger.js +0 -344
  51. package/dist/skill-loop/proposals.d.ts +0 -48
  52. package/dist/skill-loop/proposals.js +0 -290
  53. package/dist/skill-loop/storage.d.ts +0 -72
  54. package/dist/skill-loop/storage.js +0 -817
  55. package/dist/skill-loop/time.d.ts +0 -2
  56. package/dist/skill-loop/time.js +0 -23
  57. package/dist/skill-loop/transaction.d.ts +0 -62
  58. package/dist/skill-loop/transaction.js +0 -836
  59. package/dist/skill-loop/usage.d.ts +0 -31
  60. package/dist/skill-loop/usage.js +0 -146
  61. package/dist/skill-observer.d.ts +0 -22
  62. package/dist/skill-observer.js +0 -1154
  63. package/skills/skill-observer/SKILL.md +0 -148
  64. package/skills/skill-observer/references/review-contract.md +0 -95
@@ -1,1154 +0,0 @@
1
- // Durable store for skill-improvement observations.
2
- //
3
- // The capability's whole value rests on one restraint: it proposes, and something with judgement
4
- // decides. A tool that edited skills from its own inferences would compound its mistakes silently,
5
- // and the skill corpus would drift away from anything a reviewer approved. So every record here
6
- // carries `applied: false`, and nothing in this module opens a skill file for writing.
7
- //
8
- // Observations are text the session did not author -- a user's correction quoted back, a pattern
9
- // noticed in a transcript. That text is stored as data: bounded, single-line, credential-free, and
10
- // never interpreted. An observation asking to rewrite every skill is a curiosity to record, not an
11
- // instruction to follow.
12
- import { createHash, randomUUID } from "node:crypto";
13
- import { closeSync, constants, ftruncateSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, readFileSync, readlinkSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import path from "node:path";
16
- import { performance } from "node:perf_hooks";
17
- import { TextDecoder } from "node:util";
18
- import { containsIngressSecret, containsSecret } from "./secret-shapes.js";
19
- import { runtimeDirectoryName } from "./state.js";
20
- import { sameStableDirectoryIdentity, sameStableStatIdentity, stableStatIdentity } from "./stable-identity.js";
21
- /** The three signals worth capturing. A fourth needs a contract revision, not a free-text value. */
22
- export const OBSERVER_SIGNALS = Object.freeze(["correction", "repetition", "coverage-gap"]);
23
- export const OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED = "OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED";
24
- export const OBSERVER_RECORD_STATE_UNKNOWN = "OBSERVER_RECORD_STATE_UNKNOWN";
25
- const allowedFields = Object.freeze(["signal", "skillId", "observed", "proposal"]);
26
- const persistedFields = Object.freeze([...allowedFields, "applied"]);
27
- const textCeiling = 1000;
28
- const skillIdShape = /^[a-z][a-z0-9-]{0,63}$/u;
29
- export const OBSERVER_LOG_MAX_BYTES = 1_048_576;
30
- const observerLockWaitMs = 2_000;
31
- const observerLockPollMs = 10;
32
- const observerLockStaleMs = 30_000;
33
- const observerLockOwnerMaxBytes = 512;
34
- const observerLockDirectoryName = "litopencode-skill-observer";
35
- const observerLockOwnerTempShape = /^\.owner-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
36
- const noFollow = constants.O_NOFOLLOW ?? 0;
37
- const lockWaitState = new Int32Array(new SharedArrayBuffer(4));
38
- export class SkillObserverError extends Error {
39
- code;
40
- constructor(code, message) {
41
- super(message);
42
- this.name = "SkillObserverError";
43
- this.code = code;
44
- }
45
- }
46
- export function observationLogPath(root) {
47
- return path.join(path.resolve(root), runtimeDirectoryName, "skill-observer", "observations.jsonl");
48
- }
49
- export function observerLockPath(root) {
50
- const projectRoot = path.resolve(root);
51
- const projectKey = createHash("sha256").update(projectRoot, "utf8").digest("hex");
52
- return path.join(tmpdir(), `${observerLockDirectoryName}-${projectKey}.lock`);
53
- }
54
- // One line, bounded, control characters removed. A newline would break the record format and is the
55
- // simplest way to forge a second entry, so whitespace is collapsed rather than preserved. Format
56
- // controls are zero-width: replacing one with a space would both misrender the text and hide a
57
- // credential split mid-token from the flattened-surface secret check, so they are stripped instead.
58
- function flatten(value) {
59
- return value
60
- .replace(/\p{Cf}/gu, "")
61
- .replace(/\p{Cc}/gu, " ")
62
- .replace(/\s+/gu, " ")
63
- .trim()
64
- .slice(0, textCeiling);
65
- }
66
- function errorCode(error) {
67
- if (typeof error !== "object" || error === null || !("code" in error))
68
- return undefined;
69
- const code = error.code;
70
- return typeof code === "string" ? code : undefined;
71
- }
72
- function observerError(code, message) {
73
- throw new SkillObserverError(code, message);
74
- }
75
- function committedCleanupError() {
76
- return observerError(OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED, "the observation record was committed, but cleanup failed; read the log before retrying");
77
- }
78
- function unknownRecordStateError() {
79
- return observerError(OBSERVER_RECORD_STATE_UNKNOWN, "rollback failed; the observation record state is unknown; read the log before retrying");
80
- }
81
- function isUnknownRecordState(error) {
82
- return error instanceof SkillObserverError && error.code === OBSERVER_RECORD_STATE_UNKNOWN;
83
- }
84
- function containsObserverSecret(value, ingress) {
85
- return ingress ? containsIngressSecret(value) : containsSecret(value);
86
- }
87
- function isWithinRoot(root, target) {
88
- const relative = path.relative(root, target);
89
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
90
- }
91
- function safePaths(root) {
92
- let projectRoot;
93
- try {
94
- projectRoot = path.resolve(root);
95
- }
96
- catch {
97
- return observerError("OBSERVER_PATH_UNSAFE", "the observation path is unsafe");
98
- }
99
- const logPath = observationLogPath(root);
100
- const parentPath = path.dirname(logPath);
101
- if (!isWithinRoot(projectRoot, logPath) || !isWithinRoot(projectRoot, parentPath)) {
102
- return observerError("OBSERVER_PATH_UNSAFE", "the observation path escapes the requested root");
103
- }
104
- return { projectRoot, parentPath, logPath };
105
- }
106
- function assertDirectory(directoryPath) {
107
- let stat;
108
- try {
109
- stat = lstatSync(directoryPath);
110
- }
111
- catch {
112
- return observerError("OBSERVER_PATH_UNSAFE", "an observation parent path cannot be inspected safely");
113
- }
114
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
115
- observerError("OBSERVER_PATH_UNSAFE", "an observation parent path is not a regular directory");
116
- }
117
- }
118
- function isTrustedSystemAlias(candidate) {
119
- if (process.platform !== "darwin")
120
- return false;
121
- const targets = {
122
- "/etc": "/private/etc",
123
- "/tmp": "/private/tmp",
124
- "/var": "/private/var"
125
- };
126
- try {
127
- return path.resolve(path.dirname(candidate), readlinkSync(candidate)) === targets[candidate];
128
- }
129
- catch {
130
- return false;
131
- }
132
- }
133
- function assertSafeRequestedRootParents(projectRoot) {
134
- const filesystemRoot = path.parse(projectRoot).root;
135
- let parent = path.dirname(projectRoot);
136
- while (true) {
137
- try {
138
- const stat = lstatSync(parent);
139
- if (stat.isSymbolicLink()) {
140
- if (!isTrustedSystemAlias(parent))
141
- observerError("OBSERVER_PATH_UNSAFE", "the requested root has a symlinked parent");
142
- }
143
- else if (!stat.isDirectory()) {
144
- observerError("OBSERVER_PATH_UNSAFE", "the requested root parent is not a regular directory");
145
- }
146
- }
147
- catch (error) {
148
- if (errorCode(error) !== "ENOENT") {
149
- observerError("OBSERVER_PATH_UNSAFE", "the requested root parent cannot be inspected safely");
150
- }
151
- }
152
- if (parent === filesystemRoot)
153
- return;
154
- const next = path.dirname(parent);
155
- if (next === parent)
156
- observerError("OBSERVER_PATH_UNSAFE", "the requested root parent does not exist safely");
157
- parent = next;
158
- }
159
- }
160
- function ensureSafeDirectoryTree(projectRoot, targetDirectory, create) {
161
- assertSafeRequestedRootParents(projectRoot);
162
- let rootExists = true;
163
- try {
164
- const stat = lstatSync(projectRoot);
165
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
166
- observerError("OBSERVER_PATH_UNSAFE", "the requested root is not a regular directory");
167
- }
168
- }
169
- catch (error) {
170
- if (errorCode(error) !== "ENOENT") {
171
- observerError("OBSERVER_PATH_UNSAFE", "the requested root cannot be inspected safely");
172
- }
173
- rootExists = false;
174
- }
175
- if (!rootExists) {
176
- if (!create)
177
- return false;
178
- try {
179
- mkdirSync(projectRoot, { recursive: true, mode: 0o700 });
180
- }
181
- catch {
182
- observerError("OBSERVER_PATH_UNSAFE", "the requested root cannot be created safely");
183
- }
184
- assertDirectory(projectRoot);
185
- }
186
- let current = projectRoot;
187
- const relative = path.relative(projectRoot, targetDirectory);
188
- for (const segment of relative.split(path.sep).filter(Boolean)) {
189
- current = path.join(current, segment);
190
- let stat;
191
- try {
192
- stat = lstatSync(current);
193
- }
194
- catch (error) {
195
- if (errorCode(error) !== "ENOENT") {
196
- observerError("OBSERVER_PATH_UNSAFE", "an observation parent path cannot be inspected safely");
197
- }
198
- stat = undefined;
199
- }
200
- if (stat !== undefined) {
201
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
202
- observerError("OBSERVER_PATH_UNSAFE", "an observation parent path is not a regular directory");
203
- }
204
- continue;
205
- }
206
- if (stat === undefined) {
207
- if (!create)
208
- return false;
209
- try {
210
- mkdirSync(current, { mode: 0o700 });
211
- }
212
- catch (error) {
213
- if (errorCode(error) !== "EEXIST") {
214
- observerError("OBSERVER_PATH_UNSAFE", "an observation parent path cannot be created safely");
215
- }
216
- }
217
- assertDirectory(current);
218
- }
219
- }
220
- return true;
221
- }
222
- function assertRegularUnlinkedFile(stat) {
223
- if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
224
- observerError("OBSERVER_LOG_UNSAFE", "the observation log must be a regular file with one link");
225
- }
226
- if ((stat.mode & 0o077) !== 0) {
227
- observerError("OBSERVER_LOG_PERMISSIONS_UNSAFE", "the observation log must not be group or world accessible");
228
- }
229
- }
230
- function existingLogStat(logPath) {
231
- try {
232
- const stat = lstatSync(logPath);
233
- assertRegularUnlinkedFile(stat);
234
- return stat;
235
- }
236
- catch (error) {
237
- if (error instanceof SkillObserverError)
238
- throw error;
239
- if (errorCode(error) === "ENOENT")
240
- return undefined;
241
- observerError("OBSERVER_LOG_UNSAFE", "the observation log cannot be inspected safely");
242
- }
243
- }
244
- function directoryIdentity(directoryPath) {
245
- let stat;
246
- try {
247
- stat = lstatSync(directoryPath);
248
- }
249
- catch {
250
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path cannot be inspected safely");
251
- }
252
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
253
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path is not a regular directory");
254
- }
255
- return { ...stableStatIdentity(stat), nlink: stat.nlink };
256
- }
257
- function sameFile(left, right) {
258
- return left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
259
- }
260
- function sameStableFile(left, right) {
261
- return sameStableStatIdentity(stableStatIdentity(left), stableStatIdentity(right)) && left.nlink === right.nlink;
262
- }
263
- function sameDirectory(left, right) {
264
- return sameStableDirectoryIdentity(stableStatIdentity(left), stableStatIdentity(right));
265
- }
266
- function fileState(stat) {
267
- return {
268
- dev: stat.dev,
269
- ino: stat.ino,
270
- nlink: stat.nlink,
271
- size: stat.size,
272
- ctimeMs: stat.ctimeMs,
273
- mtimeMs: stat.mtimeMs,
274
- birthtimeMs: stat.birthtimeMs
275
- };
276
- }
277
- function sameFileState(left, right) {
278
- return sameFile(left, right)
279
- && left.size === right.size
280
- && left.ctimeMs === right.ctimeMs
281
- && left.mtimeMs === right.mtimeMs
282
- && left.birthtimeMs === right.birthtimeMs;
283
- }
284
- function openLog(logPath, flags, mode) {
285
- const creates = (flags & constants.O_CREAT) !== 0;
286
- let descriptor;
287
- let created = false;
288
- try {
289
- descriptor = openSync(logPath, creates ? flags | constants.O_EXCL : flags, mode);
290
- created = creates;
291
- }
292
- catch (error) {
293
- if (error instanceof SkillObserverError)
294
- throw error;
295
- if (!creates || errorCode(error) !== "EEXIST") {
296
- if (errorCode(error) === "ENOENT")
297
- return undefined;
298
- observerError("OBSERVER_LOG_UNSAFE", "the observation log cannot be opened safely");
299
- }
300
- try {
301
- descriptor = openSync(logPath, flags, mode);
302
- }
303
- catch (fallbackError) {
304
- if (fallbackError instanceof SkillObserverError)
305
- throw fallbackError;
306
- if (errorCode(fallbackError) === "ENOENT")
307
- return undefined;
308
- observerError("OBSERVER_LOG_UNSAFE", "the observation log cannot be opened safely");
309
- }
310
- }
311
- if (descriptor === undefined)
312
- return undefined;
313
- return { descriptor, created };
314
- }
315
- function removeCreatedLog(logPath, identity) {
316
- let pathStat;
317
- try {
318
- pathStat = lstatSync(logPath);
319
- }
320
- catch (error) {
321
- if (errorCode(error) === "ENOENT")
322
- return;
323
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log cannot be inspected safely");
324
- }
325
- if (pathStat.isSymbolicLink() || !pathStat.isFile() || !sameStableFile(identity, pathStat)) {
326
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log cannot be removed safely");
327
- }
328
- try {
329
- unlinkSync(logPath);
330
- }
331
- catch {
332
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log cannot be removed safely");
333
- }
334
- try {
335
- lstatSync(logPath);
336
- }
337
- catch (error) {
338
- if (errorCode(error) === "ENOENT")
339
- return;
340
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log was not removed safely");
341
- }
342
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log was not removed safely");
343
- }
344
- function assertLogHandle(projectRoot, parentPath, logPath, handle) {
345
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false)) {
346
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path is unavailable");
347
- }
348
- if (!sameDirectory(handle.parentIdentity, directoryIdentity(parentPath))) {
349
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path changed during log access");
350
- }
351
- const descriptorStat = fstatSync(handle.descriptor);
352
- assertRegularUnlinkedFile(descriptorStat);
353
- if (!sameFile(handle.identity, descriptorStat)) {
354
- observerError("OBSERVER_LOG_UNSAFE", "the observation log descriptor changed identity");
355
- }
356
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false)) {
357
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path changed during log access");
358
- }
359
- const pathStat = existingLogStat(logPath);
360
- if (pathStat === undefined || !sameStableFile(pathStat, descriptorStat)) {
361
- observerError("OBSERVER_LOG_UNSAFE", "the observation log path changed identity");
362
- }
363
- }
364
- function openValidatedLog(projectRoot, parentPath, logPath, flags, mode) {
365
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false))
366
- return undefined;
367
- const parentIdentity = directoryIdentity(parentPath);
368
- const opened = openLog(logPath, flags, mode);
369
- if (opened === undefined)
370
- return undefined;
371
- let handle;
372
- let cleanupError;
373
- try {
374
- const descriptorStat = fstatSync(opened.descriptor);
375
- handle = {
376
- descriptor: opened.descriptor,
377
- identity: descriptorStat,
378
- parentIdentity,
379
- created: opened.created
380
- };
381
- assertRegularUnlinkedFile(descriptorStat);
382
- assertLogHandle(projectRoot, parentPath, logPath, handle);
383
- return handle;
384
- }
385
- catch (error) {
386
- if (handle?.created) {
387
- try {
388
- removeCreatedLog(logPath, handle.identity);
389
- }
390
- catch (removalError) {
391
- cleanupError = removalError;
392
- }
393
- }
394
- try {
395
- closeSync(opened.descriptor);
396
- }
397
- catch (closeError) {
398
- cleanupError ??= closeError;
399
- }
400
- if (cleanupError instanceof SkillObserverError)
401
- throw cleanupError;
402
- if (cleanupError !== undefined) {
403
- observerError("OBSERVER_LOG_UNSAFE", "the newly created observation log cleanup failed");
404
- }
405
- throw error;
406
- }
407
- }
408
- function malformedLog() {
409
- return observerError("OBSERVER_LOG_MALFORMED", "the observation log contains malformed JSONL");
410
- }
411
- function validateObservation(value, persisted) {
412
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
413
- throw new SkillObserverError("OBSERVER_SHAPE_INVALID", "an observation must be an object");
414
- }
415
- const candidate = value;
416
- const fields = persisted ? persistedFields : allowedFields;
417
- const unknown = Object.keys(candidate).filter((key) => !fields.includes(key));
418
- if (unknown.length > 0) {
419
- throw new SkillObserverError("OBSERVER_FIELD_UNKNOWN", "the observation contains an unknown field");
420
- }
421
- if (persisted) {
422
- const missing = persistedFields.filter((field) => !Object.hasOwn(candidate, field));
423
- if (missing.length > 0) {
424
- throw new SkillObserverError("OBSERVER_FIELD_MISSING", `missing observation field: ${missing.join(", ")}`);
425
- }
426
- }
427
- if (!OBSERVER_SIGNALS.includes(candidate.signal)) {
428
- throw new SkillObserverError("OBSERVER_SIGNAL_UNKNOWN", `signal must be one of ${OBSERVER_SIGNALS.join(", ")}`);
429
- }
430
- if (typeof candidate.skillId !== "string" || !skillIdShape.test(candidate.skillId)) {
431
- throw new SkillObserverError("OBSERVER_SKILL_ID_INVALID", "skillId must be a lowercase skill identifier");
432
- }
433
- for (const field of ["observed", "proposal"]) {
434
- const value = candidate[field];
435
- if (typeof value !== "string" || value.trim() === "") {
436
- throw new SkillObserverError("OBSERVER_TEXT_MISSING", `${field} must be non-empty text`);
437
- }
438
- // Normalization runs before detection: format-control characters can reconstruct a credential
439
- // the raw text split, two ways. Stripping them rejoins a token split mid-word (xoxb<Cf>-...),
440
- // while replacing them with a space restores a pattern that needs the space (bearer<Cf>TOKEN).
441
- // Both surfaces are checked so neither split evades, and the full field is refused. The
442
- // offending value is never echoed: an error message is one more place a secret can land.
443
- const flattened = flatten(value);
444
- const spaced = value.replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/gu, " ").trim();
445
- if (containsObserverSecret(value, !persisted) ||
446
- containsObserverSecret(flattened, !persisted) ||
447
- containsObserverSecret(spaced, !persisted)) {
448
- throw new SkillObserverError("OBSERVER_SECRET_REFUSED", `${field} looks like it carries a credential and was refused before any write`);
449
- }
450
- if (flattened.length === 0) {
451
- throw new SkillObserverError("OBSERVER_TEXT_MISSING", `${field} must contain usable text`);
452
- }
453
- if (persisted && flattened !== value) {
454
- throw new SkillObserverError("OBSERVER_TEXT_INVALID", `${field} is not a canonical observation value`);
455
- }
456
- }
457
- if (persisted && candidate.applied !== false) {
458
- throw new SkillObserverError("OBSERVER_APPLIED_INVALID", "applied must be the boolean false");
459
- }
460
- }
461
- function hasDuplicateJsonObjectKeys(text) {
462
- let index = 0;
463
- function skipWhitespace() {
464
- while (/\s/u.test(text[index] ?? ""))
465
- index += 1;
466
- }
467
- function parseString() {
468
- if (text[index] !== '"')
469
- throw new Error("expected JSON string");
470
- const start = index;
471
- index += 1;
472
- while (index < text.length) {
473
- const character = text[index];
474
- index += 1;
475
- if (character === "\\") {
476
- index += 1;
477
- }
478
- else if (character === '"') {
479
- return JSON.parse(text.slice(start, index));
480
- }
481
- }
482
- throw new Error("unterminated JSON string");
483
- }
484
- function parsePrimitive() {
485
- while (index < text.length && !",]}".includes(text[index] ?? ""))
486
- index += 1;
487
- }
488
- function parseValue() {
489
- skipWhitespace();
490
- if (text[index] === '"') {
491
- parseString();
492
- return false;
493
- }
494
- if (text[index] === "{")
495
- return parseObject();
496
- if (text[index] === "[")
497
- return parseArray();
498
- parsePrimitive();
499
- return false;
500
- }
501
- function parseObject() {
502
- index += 1;
503
- skipWhitespace();
504
- const keys = new Set();
505
- let duplicate = false;
506
- if (text[index] === "}") {
507
- index += 1;
508
- return false;
509
- }
510
- while (index < text.length) {
511
- const key = parseString();
512
- if (keys.has(key))
513
- duplicate = true;
514
- keys.add(key);
515
- skipWhitespace();
516
- if (text[index] !== ":")
517
- throw new Error("expected JSON object separator");
518
- index += 1;
519
- if (parseValue())
520
- duplicate = true;
521
- skipWhitespace();
522
- if (text[index] === "}") {
523
- index += 1;
524
- return duplicate;
525
- }
526
- if (text[index] !== ",")
527
- throw new Error("expected JSON object delimiter");
528
- index += 1;
529
- skipWhitespace();
530
- }
531
- throw new Error("unterminated JSON object");
532
- }
533
- function parseArray() {
534
- index += 1;
535
- skipWhitespace();
536
- let duplicate = false;
537
- if (text[index] === "]") {
538
- index += 1;
539
- return false;
540
- }
541
- while (index < text.length) {
542
- if (parseValue())
543
- duplicate = true;
544
- skipWhitespace();
545
- if (text[index] === "]") {
546
- index += 1;
547
- return duplicate;
548
- }
549
- if (text[index] !== ",")
550
- throw new Error("expected JSON array delimiter");
551
- index += 1;
552
- skipWhitespace();
553
- }
554
- throw new Error("unterminated JSON array");
555
- }
556
- try {
557
- const duplicate = parseValue();
558
- skipWhitespace();
559
- return index !== text.length || duplicate;
560
- }
561
- catch {
562
- return false;
563
- }
564
- }
565
- function parseLog(rawBytes) {
566
- if (rawBytes.length > OBSERVER_LOG_MAX_BYTES) {
567
- observerError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log exceeds its 1 MiB limit");
568
- }
569
- let raw;
570
- try {
571
- raw = new TextDecoder("utf-8", { fatal: true }).decode(rawBytes);
572
- }
573
- catch {
574
- return malformedLog();
575
- }
576
- const records = [];
577
- if (raw === "")
578
- return records;
579
- const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
580
- for (const line of lines) {
581
- if (line.trim() === "")
582
- return malformedLog();
583
- let parsed;
584
- try {
585
- parsed = JSON.parse(line);
586
- if (hasDuplicateJsonObjectKeys(line)) {
587
- throw new SkillObserverError("OBSERVER_LOG_MALFORMED", "duplicate JSON object keys are not allowed");
588
- }
589
- validateObservation(parsed, true);
590
- }
591
- catch (error) {
592
- if (error instanceof SkillObserverError && error.code === "OBSERVER_LOG_SIZE_EXCEEDED")
593
- throw error;
594
- return malformedLog();
595
- }
596
- records.push(parsed);
597
- }
598
- return records;
599
- }
600
- function readLog(handle) {
601
- const beforeReadStat = fstatSync(handle.descriptor);
602
- assertRegularUnlinkedFile(beforeReadStat);
603
- const beforeRead = fileState(beforeReadStat);
604
- if (!sameFile(handle.identity, beforeRead)) {
605
- observerError("OBSERVER_LOG_UNSAFE", "the observation log descriptor changed identity");
606
- }
607
- if (typeof beforeRead.size === "number" && beforeRead.size > OBSERVER_LOG_MAX_BYTES) {
608
- observerError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log exceeds its 1 MiB limit");
609
- }
610
- const bytes = readFileSync(handle.descriptor);
611
- const afterReadStat = fstatSync(handle.descriptor);
612
- assertRegularUnlinkedFile(afterReadStat);
613
- const afterRead = fileState(afterReadStat);
614
- if (!sameFileState(beforeRead, afterRead) || bytes.length !== beforeRead.size) {
615
- observerError("OBSERVER_LOG_UNSAFE", "the observation log changed during the read");
616
- }
617
- return { bytes, records: parseLog(bytes), identity: beforeRead };
618
- }
619
- function writeFully(descriptor, bytes) {
620
- let offset = 0;
621
- while (offset < bytes.length) {
622
- const remaining = bytes.length - offset;
623
- const written = writeSync(descriptor, bytes, offset, remaining);
624
- if (!Number.isSafeInteger(written) || written < 1 || written > remaining) {
625
- observerError("OBSERVER_WRITE_FAILED", "the observation log write returned an invalid byte count");
626
- }
627
- offset += written;
628
- }
629
- }
630
- function appendLog(projectRoot, parentPath, logPath, handle, before, priorLength, contents) {
631
- const descriptor = handle.descriptor;
632
- const expectedLength = priorLength + Buffer.byteLength(contents, "utf8");
633
- const after = fstatSync(descriptor);
634
- assertRegularUnlinkedFile(after);
635
- if (!sameFile(before, after)) {
636
- observerError("OBSERVER_LOG_UNSAFE", "the observation log changed identity before it was written");
637
- }
638
- if (typeof after.size !== "number" || after.size !== priorLength) {
639
- observerError("OBSERVER_LOG_UNSAFE", "the observation log changed size before it was written");
640
- }
641
- try {
642
- writeFully(descriptor, Buffer.from(contents, "utf8"));
643
- fsyncSync(descriptor);
644
- const afterWrite = fstatSync(descriptor);
645
- assertRegularUnlinkedFile(afterWrite);
646
- if (!sameFile(before, afterWrite) || afterWrite.size !== expectedLength) {
647
- observerError("OBSERVER_LOG_UNSAFE", "the observation log changed identity or size after the write");
648
- }
649
- assertLogHandle(projectRoot, parentPath, logPath, handle);
650
- }
651
- catch (error) {
652
- try {
653
- ftruncateSync(descriptor, priorLength);
654
- }
655
- catch {
656
- unknownRecordStateError();
657
- }
658
- try {
659
- fsyncSync(descriptor);
660
- }
661
- catch {
662
- unknownRecordStateError();
663
- }
664
- if (error instanceof SkillObserverError)
665
- throw error;
666
- observerError("OBSERVER_WRITE_FAILED", "the observation log write failed");
667
- }
668
- }
669
- function waitForLock(milliseconds) {
670
- Atomics.wait(lockWaitState, 0, 0, milliseconds);
671
- }
672
- function lockDeadline() {
673
- return performance.now() + observerLockWaitMs;
674
- }
675
- function waitForLockUntil(deadline) {
676
- const remaining = deadline - performance.now();
677
- if (remaining <= 0) {
678
- observerError("OBSERVER_LOCK_TIMEOUT", "the observation lock wait exceeded its bounded limit");
679
- }
680
- waitForLock(Math.min(observerLockPollMs, Math.max(1, remaining)));
681
- }
682
- function processIsAlive(pid) {
683
- try {
684
- process.kill(pid, 0);
685
- return true;
686
- }
687
- catch (error) {
688
- return errorCode(error) !== "ESRCH";
689
- }
690
- }
691
- function readLockOwner(ownerPath) {
692
- let stat;
693
- try {
694
- stat = lstatSync(ownerPath);
695
- }
696
- catch (error) {
697
- if (errorCode(error) === "ENOENT")
698
- return { status: "ownerless" };
699
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock owner cannot be inspected safely");
700
- }
701
- if (stat.isSymbolicLink() ||
702
- !stat.isFile() ||
703
- stat.nlink !== 1 ||
704
- (stat.mode & 0o077) !== 0 ||
705
- stat.size > observerLockOwnerMaxBytes) {
706
- return { status: "invalid" };
707
- }
708
- let raw;
709
- try {
710
- raw = readFileSync(ownerPath, "utf8");
711
- }
712
- catch (error) {
713
- if (errorCode(error) === "ENOENT")
714
- return { status: "changed" };
715
- return { status: "invalid" };
716
- }
717
- let finalStat;
718
- try {
719
- finalStat = lstatSync(ownerPath);
720
- }
721
- catch (error) {
722
- if (errorCode(error) === "ENOENT")
723
- return { status: "changed" };
724
- return { status: "invalid" };
725
- }
726
- if (finalStat.isSymbolicLink() || !finalStat.isFile() || !sameStableFile(stat, finalStat)) {
727
- return { status: "changed" };
728
- }
729
- if (raw === "" || Buffer.byteLength(raw, "utf8") > observerLockOwnerMaxBytes) {
730
- return { status: "invalid" };
731
- }
732
- try {
733
- const parsed = JSON.parse(raw);
734
- if (typeof parsed !== "object" ||
735
- parsed === null ||
736
- Array.isArray(parsed) ||
737
- typeof parsed.token !== "string" ||
738
- parsed.token.length === 0 ||
739
- parsed.token.length > 128 ||
740
- !Number.isSafeInteger(parsed.pid) ||
741
- (parsed.pid) <= 0) {
742
- return { status: "invalid" };
743
- }
744
- return {
745
- status: "valid",
746
- token: parsed.token,
747
- pid: parsed.pid
748
- };
749
- }
750
- catch {
751
- return { status: "invalid" };
752
- }
753
- }
754
- function readLockSnapshot(lockPath) {
755
- let stat;
756
- try {
757
- stat = lstatSync(lockPath);
758
- }
759
- catch (error) {
760
- if (errorCode(error) === "ENOENT")
761
- return undefined;
762
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock cannot be inspected safely");
763
- }
764
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
765
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock must be a regular directory");
766
- }
767
- return { stat, owner: readLockOwner(path.join(lockPath, "owner")) };
768
- }
769
- function sameLockGeneration(left, right) {
770
- return sameDirectory(left.stat, right.stat);
771
- }
772
- function sameLockOwner(left, right) {
773
- if (left.status !== right.status)
774
- return false;
775
- if (left.status === "changed")
776
- return false;
777
- if (left.status !== "valid" || right.status !== "valid")
778
- return true;
779
- return left.token === right.token && left.pid === right.pid;
780
- }
781
- function lockAgeIsStale(stat) {
782
- try {
783
- const mtimeMs = typeof stat.mtimeMs === "bigint" ? Number(stat.mtimeMs) : stat.mtimeMs;
784
- return Date.now() - mtimeMs >= observerLockStaleMs;
785
- }
786
- catch {
787
- return false;
788
- }
789
- }
790
- function lockCanBeRecovered(snapshot) {
791
- if (snapshot.owner.status === "invalid") {
792
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock owner metadata is malformed");
793
- }
794
- if (snapshot.owner.status === "changed")
795
- return false;
796
- if (snapshot.owner.status === "valid")
797
- return !processIsAlive(snapshot.owner.pid);
798
- return lockAgeIsStale(snapshot.stat);
799
- }
800
- function restoreMovedLock(lockPath, movedPath) {
801
- try {
802
- lstatSync(lockPath);
803
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock changed during recovery");
804
- }
805
- catch (error) {
806
- if (error instanceof SkillObserverError)
807
- throw error;
808
- if (errorCode(error) !== "ENOENT") {
809
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock could not be restored safely");
810
- }
811
- }
812
- try {
813
- renameSync(movedPath, lockPath);
814
- }
815
- catch {
816
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock could not be restored safely");
817
- }
818
- }
819
- function removeMovedLock(movedPath, owner) {
820
- let entries;
821
- try {
822
- entries = readdirSync(movedPath, { withFileTypes: true });
823
- }
824
- catch {
825
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock cannot be inspected safely");
826
- }
827
- const temporaryOwners = owner === "ownerless"
828
- ? entries.filter((entry) => observerLockOwnerTempShape.test(entry.name))
829
- : [];
830
- if ((owner === "ownerless" && entries.length !== temporaryOwners.length) ||
831
- (owner === "owned" && entries.length !== 1) ||
832
- (owner === "owned" && (entries[0]?.name !== "owner" ||
833
- entries[0]?.isSymbolicLink() ||
834
- !entries[0]?.isFile()))) {
835
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock contains unexpected entries");
836
- }
837
- for (const entry of temporaryOwners) {
838
- if (entry.isSymbolicLink() || !entry.isFile()) {
839
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner cannot be removed safely");
840
- }
841
- let stat;
842
- try {
843
- stat = lstatSync(path.join(movedPath, entry.name));
844
- }
845
- catch {
846
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner cannot be inspected safely");
847
- }
848
- if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0) {
849
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner cannot be removed safely");
850
- }
851
- try {
852
- unlinkSync(path.join(movedPath, entry.name));
853
- }
854
- catch {
855
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner could not be removed safely");
856
- }
857
- }
858
- if (owner === "owned") {
859
- try {
860
- unlinkSync(path.join(movedPath, "owner"));
861
- }
862
- catch {
863
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner could not be removed safely");
864
- }
865
- }
866
- try {
867
- rmSync(movedPath, { force: true, recursive: true });
868
- }
869
- catch {
870
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock could not be removed safely");
871
- }
872
- }
873
- function tryRecoverStaleLock(lockPath) {
874
- const observed = readLockSnapshot(lockPath);
875
- if (observed === undefined || !lockCanBeRecovered(observed))
876
- return false;
877
- const movedPath = `${lockPath}.abandoned-${randomUUID()}`;
878
- try {
879
- renameSync(lockPath, movedPath);
880
- }
881
- catch (error) {
882
- if (errorCode(error) === "ENOENT")
883
- return false;
884
- observerError("OBSERVER_LOCK_UNSAFE", "the stale observation lock could not be retired safely");
885
- }
886
- const moved = readLockSnapshot(movedPath);
887
- if (moved === undefined ||
888
- !sameLockGeneration(observed, moved) ||
889
- !sameLockOwner(observed.owner, moved.owner) ||
890
- !lockCanBeRecovered(moved)) {
891
- if (moved !== undefined && moved.owner.status === "invalid") {
892
- observerError("OBSERVER_LOCK_UNSAFE", "the retired observation lock owner metadata is malformed");
893
- }
894
- restoreMovedLock(lockPath, movedPath);
895
- return false;
896
- }
897
- removeMovedLock(movedPath, moved.owner.status === "ownerless" ? "ownerless" : "owned");
898
- return true;
899
- }
900
- function removeCreatedLock(lockPath, expected) {
901
- const movedPath = `${lockPath}.failed-${randomUUID()}`;
902
- try {
903
- renameSync(lockPath, movedPath);
904
- }
905
- catch (error) {
906
- if (errorCode(error) === "ENOENT")
907
- return;
908
- observerError("OBSERVER_LOCK_UNSAFE", "the failed observation lock could not be retired safely");
909
- }
910
- const moved = readLockSnapshot(movedPath);
911
- if (moved === undefined || !sameDirectory(expected, moved.stat) || moved.owner.status !== "ownerless") {
912
- restoreMovedLock(lockPath, movedPath);
913
- observerError("OBSERVER_LOCK_UNSAFE", "the failed observation lock changed during cleanup");
914
- }
915
- removeMovedLock(movedPath, "ownerless");
916
- }
917
- function releaseLock(lockPath, token) {
918
- const movedPath = `${lockPath}.released-${randomUUID()}`;
919
- try {
920
- renameSync(lockPath, movedPath);
921
- }
922
- catch {
923
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock ownership could not be proved");
924
- }
925
- const owner = readLockOwner(path.join(movedPath, "owner"));
926
- if (owner.status !== "valid" || owner.token !== token) {
927
- restoreMovedLock(lockPath, movedPath);
928
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock ownership could not be proved");
929
- }
930
- try {
931
- removeMovedLock(movedPath, "owned");
932
- }
933
- catch (error) {
934
- try {
935
- restoreMovedLock(lockPath, movedPath);
936
- }
937
- catch {
938
- // Leave the retired lock in place when restoration cannot be proved.
939
- }
940
- throw error;
941
- }
942
- }
943
- function lockIsHeld(lockPath) {
944
- try {
945
- const stat = lstatSync(lockPath);
946
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
947
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock must be a regular directory");
948
- }
949
- return true;
950
- }
951
- catch (error) {
952
- if (errorCode(error) === "ENOENT")
953
- return false;
954
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock cannot be inspected safely");
955
- }
956
- }
957
- function waitUntilObserverLockAvailable(lockPath, legacyLockPath, deadline) {
958
- while (true) {
959
- if (lockIsHeld(legacyLockPath)) {
960
- waitForLockUntil(deadline);
961
- continue;
962
- }
963
- if (!lockIsHeld(lockPath))
964
- return;
965
- if (tryRecoverStaleLock(lockPath))
966
- continue;
967
- waitForLockUntil(deadline);
968
- }
969
- }
970
- function withObserverLock(projectRoot, parentPath, logPath, deadline, operation) {
971
- // Keep the lock outside the mutable state tree. A state-directory ancestor can be replaced
972
- // between its safety check and lock creation, so a lock below that tree would follow the swap.
973
- const lockPath = observerLockPath(projectRoot);
974
- const legacyLockPath = `${logPath}.lock`;
975
- const ownerPath = path.join(lockPath, "owner");
976
- const token = randomUUID();
977
- while (true) {
978
- let createdLock = false;
979
- let createdLockIdentity;
980
- let ownerCreated = false;
981
- try {
982
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false)) {
983
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path is unavailable");
984
- }
985
- if (lockIsHeld(legacyLockPath)) {
986
- waitForLockUntil(deadline);
987
- continue;
988
- }
989
- if (lockIsHeld(lockPath)) {
990
- if (tryRecoverStaleLock(lockPath))
991
- continue;
992
- waitForLockUntil(deadline);
993
- continue;
994
- }
995
- try {
996
- mkdirSync(lockPath, { mode: 0o700 });
997
- createdLock = true;
998
- createdLockIdentity = directoryIdentity(lockPath);
999
- }
1000
- catch (error) {
1001
- if (errorCode(error) !== "EEXIST") {
1002
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock cannot be created safely");
1003
- }
1004
- }
1005
- if (!createdLock) {
1006
- if (tryRecoverStaleLock(lockPath))
1007
- continue;
1008
- waitForLockUntil(deadline);
1009
- continue;
1010
- }
1011
- const ownerTempPath = path.join(lockPath, `.owner-${randomUUID()}`);
1012
- const descriptor = openSync(ownerTempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600);
1013
- try {
1014
- writeFileSync(descriptor, Buffer.from(`${JSON.stringify({ token, pid: process.pid })}\n`, "utf8"));
1015
- }
1016
- finally {
1017
- closeSync(descriptor);
1018
- }
1019
- renameSync(ownerTempPath, ownerPath);
1020
- ownerCreated = true;
1021
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false)) {
1022
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path changed during lock creation");
1023
- }
1024
- break;
1025
- }
1026
- catch (error) {
1027
- if (createdLock) {
1028
- if (ownerCreated) {
1029
- try {
1030
- releaseLock(lockPath, token);
1031
- }
1032
- catch {
1033
- // Leave an unproven lock in place.
1034
- }
1035
- }
1036
- else {
1037
- try {
1038
- if (createdLockIdentity === undefined) {
1039
- observerError("OBSERVER_LOCK_UNSAFE", "the created observation lock identity is unavailable");
1040
- }
1041
- removeCreatedLock(lockPath, createdLockIdentity);
1042
- }
1043
- catch {
1044
- // Leave an unproven lock in place.
1045
- }
1046
- }
1047
- }
1048
- if (error instanceof SkillObserverError)
1049
- throw error;
1050
- observerError("OBSERVER_LOCK_UNSAFE", "the observation lock cannot be acquired safely");
1051
- }
1052
- }
1053
- let operationError;
1054
- try {
1055
- return operation();
1056
- }
1057
- catch (error) {
1058
- operationError = error;
1059
- throw error;
1060
- }
1061
- finally {
1062
- try {
1063
- releaseLock(lockPath, token);
1064
- }
1065
- catch (error) {
1066
- if (!isUnknownRecordState(operationError))
1067
- throw error;
1068
- }
1069
- }
1070
- }
1071
- export function recordObservation(root, observation) {
1072
- validateObservation(observation, false);
1073
- const record = {
1074
- signal: observation.signal,
1075
- skillId: observation.skillId,
1076
- observed: flatten(observation.observed),
1077
- proposal: flatten(observation.proposal),
1078
- applied: false
1079
- };
1080
- const { projectRoot, parentPath, logPath } = safePaths(root);
1081
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, true)) {
1082
- observerError("OBSERVER_PATH_UNSAFE", "the observation parent path is unavailable");
1083
- }
1084
- const deadline = lockDeadline();
1085
- waitUntilObserverLockAvailable(observerLockPath(projectRoot), `${logPath}.lock`, deadline);
1086
- let handle;
1087
- let committed = false;
1088
- let primaryError;
1089
- try {
1090
- return withObserverLock(projectRoot, parentPath, logPath, deadline, () => {
1091
- const opened = openValidatedLog(projectRoot, parentPath, logPath, constants.O_RDWR | constants.O_CREAT | constants.O_APPEND | noFollow, 0o600);
1092
- if (opened === undefined) {
1093
- observerError("OBSERVER_LOG_UNSAFE", "the observation log cannot be opened safely");
1094
- }
1095
- handle = opened;
1096
- assertLogHandle(projectRoot, parentPath, logPath, opened);
1097
- const existing = readLog(opened);
1098
- const line = `${JSON.stringify(record)}\n`;
1099
- const separator = existing.bytes.length > 0 && existing.bytes[existing.bytes.length - 1] !== 0x0a ? "\n" : "";
1100
- const appendContents = `${separator}${line}`;
1101
- const recordBytes = Buffer.byteLength(line, "utf8");
1102
- if (recordBytes > OBSERVER_LOG_MAX_BYTES) {
1103
- observerError("OBSERVER_RECORD_TOO_LARGE", "the new observation exceeds the 1 MiB limit");
1104
- }
1105
- if (existing.bytes.length + Buffer.byteLength(appendContents, "utf8") > OBSERVER_LOG_MAX_BYTES) {
1106
- observerError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log would exceed its 1 MiB limit");
1107
- }
1108
- assertLogHandle(projectRoot, parentPath, logPath, opened);
1109
- appendLog(projectRoot, parentPath, logPath, opened, existing.identity, existing.bytes.length, appendContents);
1110
- committed = true;
1111
- return record;
1112
- });
1113
- }
1114
- catch (error) {
1115
- primaryError = error;
1116
- if (committed)
1117
- committedCleanupError();
1118
- throw error;
1119
- }
1120
- finally {
1121
- try {
1122
- if (handle !== undefined)
1123
- closeSync(handle.descriptor);
1124
- }
1125
- catch (error) {
1126
- if (committed)
1127
- committedCleanupError();
1128
- if (!isUnknownRecordState(primaryError))
1129
- throw error;
1130
- }
1131
- }
1132
- }
1133
- export function readObservations(root) {
1134
- const { projectRoot, parentPath, logPath } = safePaths(root);
1135
- if (!ensureSafeDirectoryTree(projectRoot, parentPath, false))
1136
- return [];
1137
- if (existingLogStat(logPath) === undefined)
1138
- return [];
1139
- const handle = openValidatedLog(projectRoot, parentPath, logPath, constants.O_RDONLY | noFollow);
1140
- if (handle === undefined) {
1141
- observerError("OBSERVER_LOG_UNSAFE", "the observation log disappeared before it could be opened");
1142
- }
1143
- try {
1144
- return withObserverLock(projectRoot, parentPath, logPath, lockDeadline(), () => {
1145
- assertLogHandle(projectRoot, parentPath, logPath, handle);
1146
- const records = readLog(handle).records;
1147
- assertLogHandle(projectRoot, parentPath, logPath, handle);
1148
- return records;
1149
- });
1150
- }
1151
- finally {
1152
- closeSync(handle.descriptor);
1153
- }
1154
- }