opencode-self-improvement 0.3.3 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-self-improvement",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Durable memory for opencode: remember facts, decisions, and commands across sessions, plus a one-command per-repo setup (/repo-init).",
5
5
  "main": "index.ts",
6
6
  "exports": {
package/src/logfmt.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Minimal logfmt reader/writer shared by the durable store and the evidence
2
2
  // log. Field order is preserved on write so chains are diffable.
3
3
  import { dirname } from "node:path";
4
- import { mkdir } from "node:fs/promises";
4
+ import { appendFile, mkdir } from "node:fs/promises";
5
5
 
6
6
  export type Fields = Record<string, unknown>;
7
7
 
@@ -12,6 +12,16 @@ export type Fields = Record<string, unknown>;
12
12
  const ESCAPE: Record<string, string> = { "\\": "\\\\", '"': '\\"', "\n": "\\n", "\r": "\\r", "\t": "\\t" };
13
13
  const UNESCAPE: Record<string, string> = { "\\": "\\", '"': '"', n: "\n", r: "\r", t: "\t" };
14
14
 
15
+ // Lines written before these escapes existed stored backslashes raw, so in an
16
+ // old line `C:\new` is a backslash and an `n`, not a newline. A line whose
17
+ // values use the escapes ends with this marker, and only fields before it are
18
+ // unescaped; everything else is read the old way, where only `\"` is an escape.
19
+ // Fields after the marker were appended by an older version (supersede adds
20
+ // fields to the end of an existing line). Lines without backslashes, newlines
21
+ // or tabs read the same either way, so they don't carry the marker.
22
+ const MARKER = "esc";
23
+ const MARKER_VALUE = "1";
24
+
15
25
  export function quote(value: unknown): string {
16
26
  const s = String(value);
17
27
  if (s === "") return '""';
@@ -20,34 +30,47 @@ export function quote(value: unknown): string {
20
30
 
21
31
  export function formatLine(fields: Fields): string {
22
32
  const parts: string[] = [];
33
+ let escaped = false;
23
34
  for (const [k, v] of Object.entries(fields)) {
24
- if (v === undefined || v === null) continue;
25
- parts.push(`${k}=${quote(v)}`);
35
+ if (v === undefined || v === null || k === MARKER) continue;
36
+ const q = quote(v);
37
+ if (q.startsWith('"') && /[\\\n\r\t]/.test(String(v))) escaped = true;
38
+ parts.push(`${k}=${q}`);
26
39
  }
40
+ if (escaped) parts.push(`${MARKER}=${MARKER_VALUE}`);
27
41
  return parts.join(" ");
28
42
  }
29
43
 
30
44
  // Unescape a quoted logfmt value and read it back. Unknown sequences are kept
31
- // as written: older files stored backslashes unescaped (`C:\Program Files`).
45
+ // as written.
32
46
  function unquote(s: string): string {
33
47
  return s.replace(/\\(.)/g, (seq, c: string) => UNESCAPE[c] ?? seq);
34
48
  }
35
49
 
50
+ function unquoteLegacy(s: string): string {
51
+ return s.replace(/\\"/g, '"');
52
+ }
53
+
36
54
  export function parseLine(line: string): Fields | null {
37
55
  const trimmed = line.trim();
38
56
  if (!trimmed || !trimmed.includes("=")) return null;
39
- const fields: Fields = {};
57
+ const tokens: [string, string][] = [];
40
58
  const re = /([A-Za-z0-9_.-]+)=("(?:[^"\\]|\\.)*"|[^\s]*)/g;
41
59
  for (;;) {
42
60
  const m = re.exec(trimmed);
43
61
  if (!m) break;
44
- const key = m[1];
45
- let raw = m[2];
62
+ tokens.push([m[1], m[2]]);
63
+ }
64
+ const marker = tokens.findIndex(([k, v]) => k === MARKER && v === MARKER_VALUE);
65
+ const fields: Fields = {};
66
+ tokens.forEach(([key, value], i) => {
67
+ if (key === MARKER) return;
68
+ let raw = value;
46
69
  if (raw.startsWith('"') && raw.endsWith('"')) {
47
- raw = unquote(raw.slice(1, -1));
70
+ raw = (i < marker ? unquote : unquoteLegacy)(raw.slice(1, -1));
48
71
  }
49
72
  fields[key] = raw;
50
- }
73
+ });
51
74
  // parse booleans/numbers as strings is fine; callers coerce.
52
75
  return fields;
53
76
  }
@@ -59,9 +82,13 @@ export async function ensureDir(path: string): Promise<void> {
59
82
  export async function appendLog(file: string, line: string): Promise<void> {
60
83
  await ensureDir(dirname(file));
61
84
  const next = line.endsWith("\n") ? line : line + "\n";
62
- const f = Bun.file(file);
63
- const existing = (await f.exists()) ? await f.text() : "";
64
- await Bun.write(file, existing + next);
85
+ // One O_APPEND write (issue #23). The previous read-modify-write lost rows:
86
+ // 20 concurrent appends left 2 on disk. appendFile's "a" flag is
87
+ // O_APPEND|O_CREAT — atomic small writes on local POSIX filesystems,
88
+ // covering interleaved async tasks in the service AND a second process
89
+ // (the viewer) appending the same day file. Do NOT "simplify" this to
90
+ // Bun.write with an options object: on Bun 1.4.3 that silently TRUNCATES.
91
+ await appendFile(file, next);
65
92
  }
66
93
 
67
94
  export async function readLines(file: string): Promise<string[]> {
package/src/memory.ts CHANGED
@@ -179,7 +179,10 @@ export async function markSuperseded(ts: string, fields: SupersedeFields): Promi
179
179
  if (idx === -1) continue;
180
180
  const parsed = parseLine(lines[idx]);
181
181
  if (parsed && typeof parsed.superseded_by === "string") return false;
182
- const extra = formatLine({
182
+ // Rewrite the whole line rather than appending to it, so an old-format line
183
+ // comes out in the current format with its values unchanged.
184
+ lines[idx] = formatLine({
185
+ ...parsed,
183
186
  superseded_by: fields.superseded_by,
184
187
  reason: fields.reason,
185
188
  symbol: fields.symbol,
@@ -187,7 +190,6 @@ export async function markSuperseded(ts: string, fields: SupersedeFields): Promi
187
190
  to: fields.to,
188
191
  entity: fields.entity,
189
192
  });
190
- lines[idx] = `${lines[idx]} ${extra}`;
191
193
  await snapshotBefore(file);
192
194
  await Bun.write(file, lines.join("\n") + "\n");
193
195
  return true;