@lawaty/lacode 1.3.1 → 2.1.0-beta.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/dist/index.js CHANGED
@@ -951,8 +951,6 @@ You own exactly these files and nothing else:
951
951
  150 lines.**
952
952
  - \`.opencode/context/decisions.md\` \u2014 architectural constraints that bind future
953
953
  work. **At most 120 lines.**
954
- - \`.opencode/context/active.md\` \u2014 work in flight, grouped by the ISO week it was
955
- opened. **At most 60 lines.**
956
954
 
957
955
  The unit is non-blank lines. It is what a reader actually pays for, it bounds an
958
956
  entry that wraps to three lines, and the validator can count it exactly. An
@@ -968,8 +966,6 @@ failed run, not a small one.
968
966
 
969
967
  The plugin applies this on every run. Write entries in the shape that lets it.
970
968
 
971
- - \`active.md\`: a finished task (\`[x]\` plus \`finished YYYY-MM-DD\`) is dropped
972
- after 7 days. An open task never expires, however old it is.
973
969
  - \`architecture.md\`, \`contexts.md\`, \`conventions.md\`, \`workflows.md\`,
974
970
  \`decisions.md\`: a \`[provisional]\` entry is dropped 30 days after its
975
971
  \`verified\` date unless it is re-verified. \`[binding]\` and \`[disputed]\` are
@@ -1063,8 +1059,7 @@ You are mapping a city, not auditing every house. Do not attempt to understand
1063
1059
  every file.
1064
1060
 
1065
1061
  Then write the five navigation files within their line budgets, using the entry
1066
- schema above. Also create \`.opencode/context/active.md\` with its heading and no
1067
- tasks. For each significant subsystem, give a short "Start here" list \u2014 at most
1062
+ schema above. For each significant subsystem, give a short "Start here" list \u2014 at most
1068
1063
  about five canonical files, each with a one-phrase reason. This is the single
1069
1064
  most valuable thing you produce: where an agent should open its editor next.
1070
1065
 
@@ -1098,37 +1093,6 @@ update needed".
1098
1093
  Every new or changed entry uses the schema above and stays under the file's
1099
1094
  budget. If a file is already over budget, consolidate before adding anything.
1100
1095
 
1101
- Also keep \`.opencode/context/active.md\` current: record work that is in flight,
1102
- and mark a task finished \u2014 with the date it finished \u2014 when it is.
1103
-
1104
- # Task backlog
1105
-
1106
- \`.opencode/context/active.md\` holds the work in flight. It is a rolling window,
1107
- not a history: it answers "what is open, and what just finished".
1108
-
1109
- Format \u2014 one line per task, under the ISO week the task was opened in:
1110
-
1111
- ## 2026-W43
1112
-
1113
- - [ ] Ship the release notes \u2014 opened 2026-W43
1114
- - [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
1115
-
1116
- The rules:
1117
-
1118
- - A task that is not marked \`[x]\` is OPEN. An open task is never removed,
1119
- however old it is.
1120
- - A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\` date.
1121
- Record the date it actually finished, as \`YYYY-MM-DD\`.
1122
- - The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
1123
- - A finished task is dropped once it is more than a week old. The plugin
1124
- enforces that: record the finished task with its date and let the window close
1125
- on its own. Do not delete old finished tasks by hand, and do not keep a
1126
- finished task back to preserve it \u2014 the file is meant to stay small.
1127
- - Never invent a task. Record work that actually happened, and mark a task
1128
- finished only once it is finished.
1129
- - Keep the file inside the window. If the backlog has grown past a handful of
1130
- weeks, that is a signal to close finished tasks, not to add sections.
1131
-
1132
1096
  # Minimal exploration
1133
1097
 
1134
1098
  Default shape:
@@ -1216,7 +1180,7 @@ Never drop a binding constraint to make room.
1216
1180
 
1217
1181
  Trigger: an explicit review (see the \`context-review\` command).
1218
1182
 
1219
- Audit the map files and the backlog for:
1183
+ Audit the map files for:
1220
1184
 
1221
1185
  - schema \u2014 every entry has one marker, at most three lines, a pointer, and a
1222
1186
  \`verified\` date
@@ -1231,8 +1195,6 @@ Audit the map files and the backlog for:
1231
1195
  - missing canonical entry points for significant subsystems
1232
1196
  - unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
1233
1197
  should be \`[provisional]\`
1234
- - backlog tasks that are finished but carry no date \u2014 the retention window
1235
- cannot age those out, so they stay forever
1236
1198
 
1237
1199
  Fix problems only in the files you own. Never modify source code or a project
1238
1200
  page during a review. Report what you found, what you fixed, what you left as
@@ -1704,7 +1666,7 @@ if the blocker is understanding rather than execution, report that instead.`,
1704
1666
  }
1705
1667
  },
1706
1668
  "context": {
1707
- "description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents, and keeps .opencode/context/active.md \u2014 the active backlog of work in flight, grouped by week. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
1669
+ "description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
1708
1670
  "mode": "subagent",
1709
1671
  "temperature": 0.1,
1710
1672
  prompt: CARTOGRAPHER_PROMPT,
@@ -1930,8 +1892,7 @@ exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level struc
1930
1892
  and framework config \u2192 entry points \u2192 selective source), then write the five
1931
1893
  context files inside their line budgets, using the entry schema (provenance
1932
1894
  marker, rule of at most three lines, \`\u2014 See <path>[ \xA7Section] (verified
1933
- YYYY-MM-DD)\`). Also create \`.opencode/context/active.md\` with its heading and no
1934
- tasks.
1895
+ YYYY-MM-DD)\`).
1935
1896
 
1936
1897
  Remember: write conclusions, not inventories. Point at the project's existing
1937
1898
  documentation instead of restating it. Run the drift pass and report a
@@ -1977,7 +1938,7 @@ Follow the review procedure and entry schema in your system prompt. Everything
1977
1938
  outside \`.opencode/context/\` is read-only for this run. Do not modify source
1978
1939
  code under any circumstances.
1979
1940
 
1980
- Audit the context files and the backlog for:
1941
+ Audit the context files for:
1981
1942
 
1982
1943
  - schema \u2014 every entry has one provenance marker, at most three lines, a
1983
1944
  pointer, and a \`(verified YYYY-MM-DD)\` date
@@ -1996,8 +1957,6 @@ Audit the context files and the backlog for:
1996
1957
  - missing canonical entry points for significant subsystems
1997
1958
  - unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
1998
1959
  should be \`[provisional]\`
1999
- - backlog tasks that are finished but carry no date \u2014 the retention window
2000
- cannot age those out, so they stay forever
2001
1960
 
2002
1961
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
2003
1962
  and anything you could not verify.`
@@ -4125,110 +4084,10 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
4125
4084
  });
4126
4085
 
4127
4086
  // src/plugins/context-autoupdate.ts
4128
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
4087
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
4129
4088
  import { homedir as homedir4 } from "node:os";
4130
4089
  import * as path3 from "node:path";
4131
4090
 
4132
- // src/lib/active-backlog.ts
4133
- var ACTIVE_BACKLOG_FILE = "active.md";
4134
- var RETENTION_DAYS = 7;
4135
- var DAY_MS = 24 * 60 * 60 * 1e3;
4136
- var HEADER = `# Active backlog
4137
-
4138
- Tasks in flight, grouped by the week they were opened. A finished task stays
4139
- here for a week after it was finished and is then dropped; an open task stays
4140
- however long it takes. Add a line under the right week, and give a finished
4141
- task its date: \`- [x] Title \u2014 opened 2026-W43, finished 2026-10-19\`.`;
4142
- var TASK_LINE = /^\s*(?:[-*+]|\d+[.)])\s*\[([ xX])\]\s*(.*)$/;
4143
- var META = /(?:—|–|-)\s*opened\s+(\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3]))|[,—–-]\s*finished\s+(\d{4}-\d{2}-\d{2})/gi;
4144
- var isCalendarDate = (s) => {
4145
- const at = Date.parse(`${s}T00:00:00Z`);
4146
- return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === s;
4147
- };
4148
- var parseBacklog = (text) => {
4149
- const entries = [];
4150
- for (const raw of text.split(/\r?\n/)) {
4151
- const line = TASK_LINE.exec(raw);
4152
- if (!line) continue;
4153
- const finished = line[1] !== " ";
4154
- let week;
4155
- let finishedAt;
4156
- const stripped = line[2].replace(META, (fragment, openedWeek, finishedDate) => {
4157
- if (openedWeek !== void 0) {
4158
- week ??= openedWeek.toUpperCase();
4159
- return "";
4160
- }
4161
- if (finishedDate !== void 0 && isCalendarDate(finishedDate)) {
4162
- finishedAt ??= finishedDate;
4163
- return "";
4164
- }
4165
- return fragment;
4166
- });
4167
- const title = stripped.replace(/^[\s—–-]+|[\s—–-]+$/g, "");
4168
- if (!title) continue;
4169
- entries.push({ title, ...week ? { week } : {}, finished, ...finishedAt ? { finishedAt } : {} });
4170
- }
4171
- return entries;
4172
- };
4173
- var ISO_WEEK = /^(\d{4})-W(\d{2})$/i;
4174
- var newestFirst = (a, b) => {
4175
- const ma = ISO_WEEK.exec(a);
4176
- const mb = ISO_WEEK.exec(b);
4177
- if (ma && mb) return Number(mb[1]) - Number(ma[1]) || Number(mb[2]) - Number(ma[2]);
4178
- if (ma) return -1;
4179
- if (mb) return 1;
4180
- return a < b ? 1 : a > b ? -1 : 0;
4181
- };
4182
- var renderLine = (entry) => {
4183
- const box = entry.finished ? "[x]" : "[ ]";
4184
- const meta = [];
4185
- if (entry.week) meta.push(`opened ${entry.week}`);
4186
- if (entry.finished && entry.finishedAt) meta.push(`finished ${entry.finishedAt}`);
4187
- const head = `- ${box} ${entry.title}`;
4188
- if (meta.length === 0) return head;
4189
- return entry.week ? `${head} \u2014 ${meta.join(", ")}` : `${head}, ${meta.join(", ")}`;
4190
- };
4191
- var renderBacklog = (entries) => {
4192
- const buckets = /* @__PURE__ */ new Map();
4193
- for (const entry of entries) {
4194
- const key = entry.week ? entry.week : null;
4195
- const bucket = buckets.get(key);
4196
- if (bucket) bucket.push(entry);
4197
- else buckets.set(key, [entry]);
4198
- }
4199
- const weeks = [...buckets.keys()].filter((week) => week !== null).sort(newestFirst);
4200
- let out = HEADER;
4201
- for (const week of weeks) out += `
4202
-
4203
- ## ${week}
4204
-
4205
- ${buckets.get(week).map(renderLine).join("\n")}`;
4206
- const unscheduled = buckets.get(null);
4207
- if (unscheduled) out += `
4208
-
4209
- ## Unscheduled
4210
-
4211
- ${unscheduled.map(renderLine).join("\n")}`;
4212
- return `${out}
4213
- `;
4214
- };
4215
- var normalizeBacklog = (text, now = Date.now()) => {
4216
- const entries = parseBacklog(text);
4217
- const kept = [];
4218
- const removed = [];
4219
- for (const entry of entries) {
4220
- if (entry.finished && entry.finishedAt) {
4221
- const at = Date.parse(`${entry.finishedAt}T00:00:00Z`);
4222
- if (Number.isFinite(at) && now - at > RETENTION_DAYS * DAY_MS) {
4223
- removed.push(entry.title);
4224
- continue;
4225
- }
4226
- }
4227
- kept.push(entry);
4228
- }
4229
- return { text: renderBacklog(kept), removed };
4230
- };
4231
-
4232
4091
  // src/lib/map-schema.ts
4233
4092
  import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
4234
4093
  import * as path from "node:path";
@@ -4278,21 +4137,11 @@ var MAP_SPECS = [
4278
4137
  kind: "entries",
4279
4138
  provisionalDays: PROVISIONAL_DAYS,
4280
4139
  sectionRequired: true
4281
- },
4282
- {
4283
- file: ACTIVE_BACKLOG_FILE,
4284
- charter: "work in flight, grouped by the ISO week it was opened",
4285
- budget: 60,
4286
- minEntries: 0,
4287
- kind: "backlog",
4288
- provisionalDays: PROVISIONAL_DAYS,
4289
- sectionRequired: false
4290
4140
  }
4291
4141
  ];
4142
+ var NAVIGATION_FILES = MAP_SPECS.map((spec) => spec.file);
4292
4143
  var MAP_SPEC_BY_FILE = new Map(MAP_SPECS.map((spec) => [spec.file, spec]));
4293
- var MAP_FILES = MAP_SPECS.map((spec) => spec.file);
4294
- var NAVIGATION_FILES = MAP_SPECS.filter((spec) => spec.kind === "entries").map((spec) => spec.file);
4295
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
4144
+ var DAY_MS = 24 * 60 * 60 * 1e3;
4296
4145
  var ENTRY_START = /^([-*+])\s+\[(binding|provisional|disputed)\]\s+(.*)$/i;
4297
4146
  var CONTINUATION = /^\s{2,}\S/;
4298
4147
  var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
@@ -4303,7 +4152,7 @@ var VERIFIED = /\(verified\s+(\d{4}-\d{2}-\d{2})\)\s*$/i;
4303
4152
  var POINTER = /(?:—|–|→|->)\s*See\s+`([^`\n]+)`(?:\s*§\s*([^()\n]+?))?(?:\s*\(verified\s+(\d{4}-\d{2}-\d{2})\))?\s*$/i;
4304
4153
  var DECISION_RECORD = /^\s*(?:#{1,6}\s*)?(?:\*\*)?(Context|Decision|Consequences|Alternatives|Status|Trade-?offs|Rationale)(?:\*\*)?\s*(?::|$)/im;
4305
4154
  var ARCHIVE_LANGUAGE = /\b(?:archive\s+of\s+record|this\s+(?:is|file\s+is|map\s+is)\s+the\s+only\s+(?:copy|record)|the\s+only\s+place\s+[^.\n]{0,80}?\b(?:remains|survives)\b|the\s+only\s+(?:copy|record)\s+[^.\n]{0,80}?\b(?:remains|survives|is\s+kept)\b|(?:survives|remains|is\s+kept)\s+only\s+here|no\s+longer\s+exists\s+(?:in|anywhere)|exists\s+only\s+(?:in|here))\b/i;
4306
- var isCalendarDate2 = (value) => {
4155
+ var isCalendarDate = (value) => {
4307
4156
  const at = Date.parse(`${value}T00:00:00Z`);
4308
4157
  return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
4309
4158
  };
@@ -4521,10 +4370,6 @@ var validateMapText = (spec, text, options) => {
4521
4370
  if (nonBlank > spec.budget) {
4522
4371
  push("budget", `${nonBlank} non-blank lines exceeds the ${spec.budget}-line budget; consolidate, do not pad`);
4523
4372
  }
4524
- if (spec.kind === "backlog") {
4525
- if (!text.startsWith("# ")) push("charter", "active backlog must start with a top-level heading");
4526
- return { spec, lines: nonBlank, entries: [], violations };
4527
- }
4528
4373
  const entries = parseMapEntries(text);
4529
4374
  let inFence = false;
4530
4375
  let tableReported = false;
@@ -4566,9 +4411,9 @@ var validateMapText = (spec, text, options) => {
4566
4411
  }
4567
4412
  if (!entry.verified) {
4568
4413
  push("schema", "entry has no (verified YYYY-MM-DD); the lifecycle and drift checks need it", line);
4569
- } else if (!isCalendarDate2(entry.verified)) {
4414
+ } else if (!isCalendarDate(entry.verified)) {
4570
4415
  push("schema", `verified date ${entry.verified} is not a real calendar date`, line);
4571
- } else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS2) {
4416
+ } else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS) {
4572
4417
  push("schema", `verified date ${entry.verified} is more than a day in the future`, line);
4573
4418
  }
4574
4419
  if (!entry.pointer) {
@@ -4618,14 +4463,6 @@ var validateMapText = (spec, text, options) => {
4618
4463
  };
4619
4464
  var normalizeMapText = (spec, text, options = {}) => {
4620
4465
  const now = options.now ?? Date.now();
4621
- if (spec.kind === "backlog") {
4622
- const result = normalizeBacklog(text, now);
4623
- return {
4624
- text: result.text,
4625
- removed: result.removed.map((title) => ({ line: 0, text: title })),
4626
- kept: 0
4627
- };
4628
- }
4629
4466
  const entries = parseMapEntries(text);
4630
4467
  const drop = /* @__PURE__ */ new Set();
4631
4468
  const removed = [];
@@ -4635,7 +4472,7 @@ var normalizeMapText = (spec, text, options = {}) => {
4635
4472
  if (options.canResolve && !options.canResolve(entry.pointer, entry.section)) continue;
4636
4473
  const at = Date.parse(`${entry.verified}T00:00:00Z`);
4637
4474
  if (!Number.isFinite(at)) continue;
4638
- if (now - at <= spec.provisionalDays * DAY_MS2) continue;
4475
+ if (now - at <= spec.provisionalDays * DAY_MS) continue;
4639
4476
  for (let i = entry.startLine; i < entry.endLine; i++) drop.add(i);
4640
4477
  removed.push({ line: entry.startLine + 1, text: truncate(entry.rule) });
4641
4478
  }
@@ -4665,8 +4502,6 @@ var maintainMap = (root, mapDir, now = Date.now()) => {
4665
4502
  if (result.removed.length > 0) {
4666
4503
  removedTotal += result.removed.length;
4667
4504
  files.push({ file: spec.file, changed, removed: result.removed });
4668
- } else if (changed) {
4669
- files.push({ file: spec.file, changed, removed: [] });
4670
4505
  }
4671
4506
  }
4672
4507
  return { files, removedTotal };
@@ -4692,21 +4527,19 @@ var auditMap = (root, mapDir, now = Date.now()) => {
4692
4527
  const result = validateMapText(spec, text, { root, now });
4693
4528
  files.push(result);
4694
4529
  violations.push(...result.violations);
4695
- if (spec.kind === "entries") {
4696
- for (const entry of result.entries) {
4697
- const key = normalizeRule(entry.rule);
4698
- if (key.length < 12) continue;
4699
- const home = rulesSeen.get(key);
4700
- if (home !== void 0 && home !== spec.file) {
4701
- violations.push({
4702
- file: spec.file,
4703
- line: entry.startLine + 1,
4704
- kind: "duplicate",
4705
- detail: `same rule is already stated in ${home}; one fact has one home`
4706
- });
4707
- } else if (home === void 0) {
4708
- rulesSeen.set(key, spec.file);
4709
- }
4530
+ for (const entry of result.entries) {
4531
+ const key = normalizeRule(entry.rule);
4532
+ if (key.length < 12) continue;
4533
+ const home = rulesSeen.get(key);
4534
+ if (home !== void 0 && home !== spec.file) {
4535
+ violations.push({
4536
+ file: spec.file,
4537
+ line: entry.startLine + 1,
4538
+ kind: "duplicate",
4539
+ detail: `same rule is already stated in ${home}; one fact has one home`
4540
+ });
4541
+ } else if (home === void 0) {
4542
+ rulesSeen.set(key, spec.file);
4710
4543
  }
4711
4544
  }
4712
4545
  }
@@ -4974,7 +4807,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
4974
4807
  });
4975
4808
  const root = path3.resolve(directory);
4976
4809
  const contextAbs = path3.join(root, CONTEXT_DIR2);
4977
- const activeBacklogPath = path3.join(contextAbs, ACTIVE_BACKLOG_FILE);
4978
4810
  const config = findProjectConfig();
4979
4811
  const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
4980
4812
  if (writerProblem) void log("error", writerProblem);
@@ -4984,22 +4816,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
4984
4816
  if (absolute === contextAbs || absolute.startsWith(contextAbs + path3.sep)) return false;
4985
4817
  return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
4986
4818
  };
4987
- const normalizeActiveBacklog = async () => {
4988
- try {
4989
- if (!existsSync4(activeBacklogPath)) return { removed: 0 };
4990
- const before = readFileSync7(activeBacklogPath, "utf8");
4991
- const { text, removed } = normalizeBacklog(before);
4992
- if (text !== before) writeFileSync6(activeBacklogPath, text);
4993
- if (removed.length > 0) {
4994
- await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
4995
- removed: removed.length
4996
- });
4997
- }
4998
- return { removed: removed.length };
4999
- } catch {
5000
- return { removed: 0 };
5001
- }
5002
- };
5003
4819
  const applyLifecycle = async () => {
5004
4820
  try {
5005
4821
  const result = maintainMap(root, contextAbs);
@@ -5059,7 +4875,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5059
4875
  "",
5060
4876
  ENTRY_CONTRACT,
5061
4877
  "",
5062
- "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets. Also create `.opencode/context/active.md` with its heading and no tasks. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
4878
+ "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
5063
4879
  ].join("\n") : [
5064
4880
  "Incremental context update. Project root: " + directory,
5065
4881
  "",
@@ -5070,17 +4886,13 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5070
4886
  "",
5071
4887
  ENTRY_CONTRACT,
5072
4888
  "",
5073
- 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or an invariant not yet recorded. Do not edit documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed". Also keep `.opencode/context/active.md` current: record work that is in flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.'
4889
+ 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or an invariant not yet recorded. Do not update documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed".'
5074
4890
  ].join("\n");
5075
4891
  return {
5076
4892
  event: async ({ event }) => {
5077
4893
  try {
5078
4894
  if (event.type === "file.edited") {
5079
4895
  const file = event.properties.file;
5080
- if (path3.resolve(root, file) === activeBacklogPath) {
5081
- await normalizeActiveBacklog();
5082
- return;
5083
- }
5084
4896
  if (isTracked(file)) pending.add(file);
5085
4897
  return;
5086
4898
  }
@@ -5192,7 +5004,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5192
5004
  });
5193
5005
 
5194
5006
  // src/lib/feedback.ts
5195
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
5007
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
5196
5008
  import { homedir as homedir5 } from "node:os";
5197
5009
  import { dirname as dirname4, join as join9 } from "node:path";
5198
5010
  var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
@@ -5200,7 +5012,7 @@ var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you us
5200
5012
  var feedbackStatePath = () => join9(homedir5(), ".local", "share", "opencode", "lacode", "feedback.json");
5201
5013
  function readFeedbackState(path4 = feedbackStatePath()) {
5202
5014
  try {
5203
- const parsed = JSON.parse(readFileSync8(path4, "utf8"));
5015
+ const parsed = JSON.parse(readFileSync7(path4, "utf8"));
5204
5016
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
5205
5017
  return parsed;
5206
5018
  } catch (error) {
@@ -5274,7 +5086,7 @@ async function feedbackHooks(input, options, deps = {}) {
5274
5086
 
5275
5087
  // src/plugins/image-paste.ts
5276
5088
  import { createHash, randomUUID } from "node:crypto";
5277
- import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
5089
+ import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
5278
5090
  import { homedir as homedir6 } from "node:os";
5279
5091
  import { basename as basename3, join as join10 } from "node:path";
5280
5092
 
@@ -5531,7 +5343,7 @@ async function imagePasteHooks(input, options, deps) {
5531
5343
  }
5532
5344
  let bytes;
5533
5345
  try {
5534
- bytes = readFileSync9(filePath);
5346
+ bytes = readFileSync8(filePath);
5535
5347
  } catch {
5536
5348
  skipped.push(name);
5537
5349
  continue;
@@ -6677,27 +6489,36 @@ async function selfUpdateHooks(input, options) {
6677
6489
  }
6678
6490
 
6679
6491
  // src/plugins/timeline.ts
6680
- import { writeFileSync as writeFileSync9, renameSync as renameSync6, rmSync as rmSync3 } from "node:fs";
6492
+ import { existsSync as existsSync5, writeFileSync as writeFileSync10, renameSync as renameSync7, rmSync as rmSync3 } from "node:fs";
6493
+ import { join as join12 } from "node:path";
6681
6494
  import { tool as tool2 } from "@opencode-ai/plugin";
6682
6495
 
6683
6496
  // src/lib/timeline.ts
6684
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
6497
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync9, renameSync as renameSync6, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "node:fs";
6685
6498
  import { homedir as homedir7 } from "node:os";
6686
6499
  import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
6500
+ var TIMELINE_STATE_VERSION = 1;
6687
6501
  function timelineDir() {
6688
6502
  const env = process.env.LACODE_TIMELINE_DIR;
6689
6503
  if (env) return resolve4(env);
6690
6504
  return join11(homedir7(), ".local/share/opencode/lacode/timeline");
6691
6505
  }
6692
- function timelineFile(sessionID) {
6506
+ function assertValidSessionID(sessionID) {
6693
6507
  if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
6508
+ }
6509
+ function timelineFile(sessionID) {
6510
+ assertValidSessionID(sessionID);
6694
6511
  return join11(timelineDir(), sessionID + ".jsonl");
6695
6512
  }
6513
+ function timelineStateFile(sessionID) {
6514
+ assertValidSessionID(sessionID);
6515
+ return join11(timelineDir(), sessionID + ".state.json");
6516
+ }
6696
6517
  function normalizeThrough(through) {
6697
6518
  if (!through || typeof through !== "object") return void 0;
6698
6519
  const out = {};
6699
- if (typeof through.messageID === "string") out.messageID = through.messageID;
6700
- if (typeof through.time === "number") out.time = through.time;
6520
+ if (typeof through.messageID === "string" && through.messageID.length > 0) out.messageID = through.messageID;
6521
+ if (typeof through.time === "number" && Number.isFinite(through.time)) out.time = through.time;
6701
6522
  if (out.messageID === void 0 && out.time === void 0) return void 0;
6702
6523
  return out;
6703
6524
  }
@@ -6738,7 +6559,7 @@ function appendEntriesAt(file, texts, now, through) {
6738
6559
  function readEntriesAt(file, limit) {
6739
6560
  let raw = "";
6740
6561
  try {
6741
- raw = readFileSync10(file, "utf8");
6562
+ raw = readFileSync9(file, "utf8");
6742
6563
  } catch {
6743
6564
  return [];
6744
6565
  }
@@ -6772,6 +6593,70 @@ function removeEntries(sessionID) {
6772
6593
  } catch {
6773
6594
  }
6774
6595
  }
6596
+ function normalizePersistedState(raw) {
6597
+ const obj = raw && typeof raw === "object" ? raw : {};
6598
+ const cursor = normalizeThrough(obj.cursor);
6599
+ const failures = typeof obj.failures === "number" && Number.isFinite(obj.failures) ? Math.max(0, Math.trunc(obj.failures)) : 0;
6600
+ const pending = obj.pending === true;
6601
+ const nextAttemptAt = typeof obj.nextAttemptAt === "number" && Number.isFinite(obj.nextAttemptAt) && obj.nextAttemptAt > 0 ? obj.nextAttemptAt : void 0;
6602
+ const updatedAt = typeof obj.updatedAt === "number" && Number.isFinite(obj.updatedAt) ? obj.updatedAt : Date.now();
6603
+ const state2 = {
6604
+ version: TIMELINE_STATE_VERSION,
6605
+ failures,
6606
+ pending,
6607
+ updatedAt
6608
+ };
6609
+ if (cursor) state2.cursor = cursor;
6610
+ if (nextAttemptAt !== void 0) state2.nextAttemptAt = nextAttemptAt;
6611
+ return state2;
6612
+ }
6613
+ function readTimelineState(sessionID) {
6614
+ try {
6615
+ const raw = readFileSync9(timelineStateFile(sessionID), "utf8");
6616
+ return normalizePersistedState(JSON.parse(raw));
6617
+ } catch {
6618
+ return void 0;
6619
+ }
6620
+ }
6621
+ function writeTimelineState(sessionID, state2) {
6622
+ const file = timelineStateFile(sessionID);
6623
+ mkdirSync8(dirname5(file), { recursive: true });
6624
+ const tmp = file + ".tmp." + process.pid + "." + Math.random().toString(36).slice(2);
6625
+ try {
6626
+ writeFileSync9(tmp, JSON.stringify(state2) + "\n");
6627
+ renameSync6(tmp, file);
6628
+ } catch (err) {
6629
+ try {
6630
+ rmSync2(tmp, { force: true });
6631
+ } catch {
6632
+ }
6633
+ throw err;
6634
+ }
6635
+ }
6636
+ function removeTimelineState(sessionID) {
6637
+ try {
6638
+ rmSync2(timelineStateFile(sessionID), { force: true });
6639
+ } catch {
6640
+ }
6641
+ }
6642
+ function listTimelineStates() {
6643
+ let names;
6644
+ try {
6645
+ names = readdirSync3(timelineDir());
6646
+ } catch {
6647
+ return [];
6648
+ }
6649
+ const out = [];
6650
+ for (const name of names) {
6651
+ if (!name.endsWith(".state.json")) continue;
6652
+ const sessionID = name.slice(0, -".state.json".length);
6653
+ if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) continue;
6654
+ const state2 = readTimelineState(sessionID);
6655
+ if (!state2) continue;
6656
+ out.push({ sessionID, state: state2 });
6657
+ }
6658
+ return out;
6659
+ }
6775
6660
  function collapse(s) {
6776
6661
  return s.replace(/\s+/g, " ").trim();
6777
6662
  }
@@ -6877,6 +6762,49 @@ function summarizeToolResult(state2) {
6877
6762
  }
6878
6763
  return "";
6879
6764
  }
6765
+ function isSideTool(tool3, input) {
6766
+ if (tool3 === "timeline_reprocess" || tool3 === "pool_status" || tool3 === "pool_recheck") return true;
6767
+ if (tool3 === "task" && input !== null && input !== void 0 && typeof input === "object") {
6768
+ return input.subagent_type === "context";
6769
+ }
6770
+ return false;
6771
+ }
6772
+ function mentionsSideMaintenance(text) {
6773
+ return /\b(?:timeline_reprocess|pool_status|pool_recheck|reprocess-timeline|cartographer)\b/i.test(text);
6774
+ }
6775
+ function isSideMaintenanceTask(input, rootDirectory, projectOwnsTooling) {
6776
+ if (projectOwnsTooling) return false;
6777
+ let description = "";
6778
+ let prompt = "";
6779
+ if (input !== null && input !== void 0 && typeof input === "object") {
6780
+ const rec = input;
6781
+ if (typeof rec.description === "string") description = rec.description;
6782
+ if (typeof rec.prompt === "string") prompt = rec.prompt;
6783
+ }
6784
+ const text = [description, prompt].filter((s) => typeof s === "string" && s.length > 0).join("\n");
6785
+ if (text === "") return false;
6786
+ if (/\.config\/(?:opencode|lacode)|\.local\/share\/opencode/i.test(text)) return true;
6787
+ if (mentionsSideMaintenance(text)) return true;
6788
+ const root = rootDirectory.replace(/\\/g, "/").replace(/\/+$/, "");
6789
+ for (const match of text.matchAll(/\/(?:home|Users|root|opt|srv|mnt|workspace|workspaces)\/[^\s"'`),;]+/gi)) {
6790
+ const cleaned = match[0].replace(/[.,:;!?]+$/, "");
6791
+ if (!(cleaned.includes("/Projects/") || cleaned.includes("/projects/"))) continue;
6792
+ if (cleaned === root || cleaned.startsWith(root + "/")) continue;
6793
+ return true;
6794
+ }
6795
+ return false;
6796
+ }
6797
+ function taskChildSessionId(part) {
6798
+ const sessionId = part.state?.metadata?.sessionId;
6799
+ if (typeof sessionId === "string" && sessionId.length > 0) return sessionId;
6800
+ const output = part.state?.output;
6801
+ if (typeof output !== "string") return void 0;
6802
+ const taskMatch = output.match(/task id="([A-Za-z0-9_.-]+)"/);
6803
+ if (taskMatch) return taskMatch[1];
6804
+ const sesMatch = output.match(/ses_[A-Za-z0-9]+/);
6805
+ if (sesMatch) return sesMatch[0];
6806
+ return void 0;
6807
+ }
6880
6808
  function renderMessage(m) {
6881
6809
  const role = m.info.role;
6882
6810
  const lines = [];
@@ -6890,18 +6818,29 @@ function renderMessage(m) {
6890
6818
  }
6891
6819
  } else if (role === "assistant") {
6892
6820
  for (const p of m.parts) {
6821
+ if (p.side === true) continue;
6893
6822
  if (p.type === "text") {
6894
6823
  const text = collapse(p.text ?? "");
6895
6824
  lines.push(`[assistant] ${trunc(text, 1500)}`);
6896
6825
  } else if (p.type === "tool") {
6826
+ if (p.side === true) continue;
6897
6827
  const tool3 = p.tool ?? "";
6898
- if (tool3 === "timeline_reprocess") continue;
6828
+ if (isSideTool(tool3, p.state?.input)) continue;
6899
6829
  const status = p.state?.status;
6900
6830
  const summary = inputSummary(p.state?.input);
6901
- const result = summarizeToolResult(p.state);
6831
+ let result = summarizeToolResult(p.state);
6832
+ let suppressInput = false;
6833
+ if (tool3 === "task" && typeof p.childTitle === "string" && collapse(p.childTitle) !== "") {
6834
+ const title = trunc(collapse(p.childTitle), 160);
6835
+ result = title;
6836
+ if (typeof p.childOutcome === "string" && collapse(p.childOutcome) !== "") {
6837
+ result = `${title}: ${trunc(collapse(p.childOutcome), 600)}`;
6838
+ }
6839
+ if (summary && title.startsWith(summary)) suppressInput = true;
6840
+ }
6902
6841
  let line = `[tool] ${tool3}`;
6903
6842
  if (status) line += ` (${status})`;
6904
- const inputPart = summary ? trunc(summary, 220) : "";
6843
+ const inputPart = summary && !suppressInput ? trunc(summary, 220) : "";
6905
6844
  let rest = "";
6906
6845
  if (inputPart) rest += ` ${inputPart}`;
6907
6846
  if (result) rest += ` -> ${result}`;
@@ -7026,12 +6965,17 @@ var DEFAULT_MAX_ENTRIES = 8;
7026
6965
  var MAX_ENTRIES = 10;
7027
6966
  var PROMPT_TIMEOUT_MS = 9e4;
7028
6967
  var SYSTEM = `You are the session timeline summarizer. You receive a slice of a coding session transcript.
7029
- Write 1-8 timeline entries describing the work as tasks, features, fixes, or stages.
6968
+ Maintain a timeline of the project's own work: features, fixes, refactors, behavior changes, user-visible capabilities, architectural decisions, data/model changes, and documentation that defines project behavior.
6969
+ Write 1-8 timeline entries describing that project work.
7030
6970
 
7031
6971
  Rules:
7032
6972
  - One entry per line, each starting with "- ".
7033
6973
  - Each entry is 6-16 words, past tense, plain text.
7034
- - Cover each distinct task in the slice. Do not drop a provider, feature, fix, config, docs, or test change; merge only exact duplicates.
6974
+ - Cover each distinct project task in the slice. Do not drop a feature, fix, refactor, behavior change, user-visible capability, architectural decision, data/model change, or documentation that defines project behavior; merge only exact duplicates.
6975
+ - Record project test changes at the capability level, not as housekeeping; do not report counts.
6976
+ - Ignore self-maintenance and infrastructure: the context map/cartographer, timeline rebuilding/summarization, model-pool status/rechecks, plugin install/update, agent/command configuration, formatting/lint/dependency churn, and build/dist artifacts.
6977
+ - Name modules, components, services, or capabilities when they are the subject. Do not list file paths, line numbers, counts, commit hashes, or build artifacts.
6978
+ - Record decisions or findings that changed the plan, not routine searches or reads.
7035
6979
  - Describe the TASK or OUTCOME, not the implementation. "Pool: added bucket-based routing" is right; "added b:<name> to pool.ts and agent-pool.ts" is too detailed.
7036
6980
  - Never describe the mechanism: no flags, aliases, overrides, endpoints, probes, config keys, declarations, tags, or file changes. State what changed for the system: which capability now works, which bug is fixed, which behavior changed.
7037
6981
  - Never use these words in an entry: tag, alias, override, flag, probe, endpoint, config key, catalog, file, line, count.
@@ -7039,13 +6983,12 @@ Rules:
7039
6983
  - Use a short area prefix when it clarifies: "Pool:", "Providers:", "Config:", "Docs:", "Tests:", "Fix:", "Feature:".
7040
6984
  - Name the capability, behavior, feature, bug, or decision. Do not name files, paths, functions, commands, environment variables, API keys, raw model IDs, test file names, or build artifacts. Provider and product names are allowed when they identify the task.
7041
6985
  - Do not include counts of slots, providers, tests, lines, or files.
7042
- - Omit internal housekeeping: builds, dist rebuilds, test-file additions, formatting, and dependency churn. If the slice only contains housekeeping, output nothing.
7043
6986
  - Describe what was DONE or DECIDED. Never write "User asked...", "Assistant said...", "Discussed...", "Reviewed...", "Explained...", or "Considered...".
7044
6987
  - Merge related changes into one stage; do not repeat the same task in different words.
7045
6988
  - No markdown: no bold, italics, backticks, or code blocks.
7046
6989
  - If a technical detail is the essence of the task, name it at the capability level: "Added Cloudflare Workers AI as a provider", not its account-id variables.
7047
- - If the slice contains no substantive work or decision, output nothing.
7048
- - Output only the entries. No headings, no preamble, no closing sentence.
6990
+ - If the slice contains no project work, output exactly NO_PROJECT_CHANGES and nothing else. Otherwise output only the bullets.
6991
+ - Output only the entries or NO_PROJECT_CHANGES. No headings, no preamble, no closing sentence.
7049
6992
 
7050
6993
  Example output:
7051
6994
  - Pool: added bucket-based routing for model groups.
@@ -7073,7 +7016,7 @@ This:
7073
7016
  - Pool: moved shared-window models to the end.
7074
7017
  - Providers: replaced the retired Gemini 2.5 Flash with Gemini 3.5 Flash.
7075
7018
 
7076
- Your entire response must consist only of lines beginning with "- ".`;
7019
+ Your entire response must consist only of lines beginning with "- ", or exactly NO_PROJECT_CHANGES.`;
7077
7020
  function errorText(value) {
7078
7021
  if (value instanceof Error) return value.message;
7079
7022
  if (typeof value === "string") return value;
@@ -7088,6 +7031,85 @@ function errorText(value) {
7088
7031
  }
7089
7032
  return String(value);
7090
7033
  }
7034
+ var MAX_TASK_CHILDREN_EXPANDED = 40;
7035
+ async function expandTaskSessions(client, messages, directory, owned, onError, cursor) {
7036
+ const projectOwnsTooling = existsSync5(join12(directory, "src/plugins/timeline.ts"));
7037
+ let attempts = 0;
7038
+ const expandedMessages = [];
7039
+ let startIndex = 0;
7040
+ if (cursor) {
7041
+ const cursorIndex = messages.findIndex((m) => m.info.id === cursor);
7042
+ if (cursorIndex >= 0) startIndex = cursorIndex + 1;
7043
+ }
7044
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
7045
+ const message = messages[messageIndex];
7046
+ if (messageIndex < startIndex) {
7047
+ expandedMessages.push(message);
7048
+ continue;
7049
+ }
7050
+ const parts = message.parts ?? [];
7051
+ let changed = false;
7052
+ const nextParts = [];
7053
+ for (const part of parts) {
7054
+ if (message.info.role === "assistant" && part.type === "text" && !projectOwnsTooling && mentionsSideMaintenance(part.text ?? "")) {
7055
+ nextParts.push({ ...part, side: true });
7056
+ changed = true;
7057
+ continue;
7058
+ }
7059
+ if (part.type !== "tool" || part.tool !== "task" || attempts >= MAX_TASK_CHILDREN_EXPANDED) {
7060
+ nextParts.push(part);
7061
+ continue;
7062
+ }
7063
+ if (isSideMaintenanceTask(part.state?.input, directory, projectOwnsTooling)) {
7064
+ nextParts.push({ ...part, side: true });
7065
+ continue;
7066
+ }
7067
+ if (isSideTool("task", part.state?.input)) {
7068
+ nextParts.push({ ...part, side: true });
7069
+ continue;
7070
+ }
7071
+ if (typeof part.childTitle === "string" && part.childTitle.trim() !== "") {
7072
+ nextParts.push(part);
7073
+ continue;
7074
+ }
7075
+ const childId = taskChildSessionId(part);
7076
+ if (!childId || owned.has(childId)) {
7077
+ nextParts.push(part);
7078
+ continue;
7079
+ }
7080
+ attempts++;
7081
+ try {
7082
+ const fetched = await client.session.get({ path: { id: childId }, query: { directory } });
7083
+ const info = fetched?.data;
7084
+ const rawTitle = typeof info?.title === "string" ? info.title.trim() : "";
7085
+ if (!rawTitle || isInternalTitle(info?.title)) {
7086
+ nextParts.push(part);
7087
+ continue;
7088
+ }
7089
+ const childRes = await client.session.messages({ path: { id: childId }, query: { directory } });
7090
+ const childMessages = childRes?.data ?? [];
7091
+ let childOutcome;
7092
+ for (let i = childMessages.length - 1; i >= 0; i--) {
7093
+ const childMessage = childMessages[i];
7094
+ if (childMessage?.info?.role !== "assistant") continue;
7095
+ const textParts = (childMessage.parts ?? []).filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "");
7096
+ const joined = textParts.join("\n").replace(/\s+/g, " ").trim();
7097
+ if (!joined) continue;
7098
+ childOutcome = joined.length > 800 ? joined.slice(0, 800) : joined;
7099
+ break;
7100
+ }
7101
+ const cloned = childOutcome ? { ...part, childTitle: rawTitle, childOutcome } : { ...part, childTitle: rawTitle };
7102
+ nextParts.push(cloned);
7103
+ changed = true;
7104
+ } catch (error) {
7105
+ if (onError) onError("expand-task-session failed", { childId, error });
7106
+ nextParts.push(part);
7107
+ }
7108
+ }
7109
+ expandedMessages.push(changed ? { ...message, parts: nextParts } : message);
7110
+ }
7111
+ return expandedMessages;
7112
+ }
7091
7113
  function timelineHooks(input, options) {
7092
7114
  const { client, directory } = input;
7093
7115
  const opts = options ?? {};
@@ -7104,6 +7126,11 @@ function timelineHooks(input, options) {
7104
7126
  const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
7105
7127
  const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
7106
7128
  const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
7129
+ const rawRetryMs = Number(config?.retryMs ?? 3e4);
7130
+ const retryMs = Number.isFinite(rawRetryMs) ? Math.max(1, rawRetryMs) : 3e4;
7131
+ const rawMaxBackoffMs = Number(config?.maxBackoffMs ?? 30 * 6e4);
7132
+ const maxBackoffMs = Math.max(retryMs, Number.isFinite(rawMaxBackoffMs) ? Math.max(1, rawMaxBackoffMs) : 30 * 6e4);
7133
+ const retryDelay = (failures) => Math.min(retryMs * 2 ** Math.min(Math.max(failures - 1, 0), 20), maxBackoffMs);
7107
7134
  const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
7108
7135
  sessions: /* @__PURE__ */ new Map(),
7109
7136
  owned: /* @__PURE__ */ new Set(),
@@ -7120,21 +7147,37 @@ function timelineHooks(input, options) {
7120
7147
  if (!id) return void 0;
7121
7148
  return { providerID: provider, modelID: id };
7122
7149
  };
7150
+ const persist = (id, state2) => {
7151
+ try {
7152
+ writeTimelineState(id, {
7153
+ version: TIMELINE_STATE_VERSION,
7154
+ cursor: state2.cursor,
7155
+ failures: state2.failures,
7156
+ pending: state2.pending,
7157
+ nextAttemptAt: state2.nextAttemptAt,
7158
+ updatedAt: Date.now()
7159
+ });
7160
+ } catch (error) {
7161
+ debug("persist failed", { sessionID: id, error });
7162
+ }
7163
+ };
7123
7164
  const stateFor = (id) => {
7124
7165
  let state2 = sessions.get(id);
7125
7166
  if (!state2) {
7167
+ const persisted = readTimelineState(id);
7126
7168
  state2 = {
7127
7169
  running: false,
7128
- pending: false,
7129
- failures: 0,
7130
- cursor: readEntries(id, 1)[0]?.through
7170
+ pending: persisted?.pending ?? false,
7171
+ failures: persisted?.failures ?? 0,
7172
+ cursor: persisted?.cursor ?? readEntries(id, 1)[0]?.through,
7173
+ nextAttemptAt: persisted?.nextAttemptAt
7131
7174
  };
7132
7175
  sessions.set(id, state2);
7133
7176
  while (sessions.size > 500) {
7134
7177
  let evicted = false;
7135
7178
  for (const [key, st] of sessions) {
7136
7179
  if (key === id) continue;
7137
- if (!st.timer && !st.running) {
7180
+ if (!st.timer && !st.running && !st.pending && st.nextAttemptAt === void 0) {
7138
7181
  sessions.delete(key);
7139
7182
  evicted = true;
7140
7183
  break;
@@ -7149,16 +7192,29 @@ function timelineHooks(input, options) {
7149
7192
  if (disabled || disposed) return;
7150
7193
  const state2 = stateFor(id);
7151
7194
  if (state2.root === false) return;
7195
+ const wasPending = state2.pending;
7152
7196
  state2.pending = true;
7197
+ if (!wasPending) persist(id, state2);
7153
7198
  if (state2.timer || state2.running) return;
7154
7199
  schedule(id, state2);
7155
7200
  };
7156
7201
  function flush(id) {
7157
7202
  if (disabled || disposed) return;
7158
- const state2 = sessions.get(id);
7159
- if (!state2) return;
7203
+ let state2 = sessions.get(id);
7204
+ if (!state2) {
7205
+ if (readTimelineState(id)?.pending === true) state2 = stateFor(id);
7206
+ else return;
7207
+ }
7160
7208
  if (state2.root === false) return;
7161
7209
  if (!state2.pending || state2.running) return;
7210
+ if (state2.nextAttemptAt && state2.nextAttemptAt > Date.now()) {
7211
+ if (state2.timer) {
7212
+ clearTimeout(state2.timer);
7213
+ state2.timer = void 0;
7214
+ }
7215
+ schedule(id, state2);
7216
+ return;
7217
+ }
7162
7218
  if (state2.timer) {
7163
7219
  clearTimeout(state2.timer);
7164
7220
  state2.timer = void 0;
@@ -7167,10 +7223,11 @@ function timelineHooks(input, options) {
7167
7223
  }
7168
7224
  function schedule(id, state2) {
7169
7225
  if (state2.timer) clearTimeout(state2.timer);
7226
+ const delay = typeof state2.nextAttemptAt === "number" ? Math.max(0, state2.nextAttemptAt - Date.now()) : state2.failures > 0 ? retryDelay(state2.failures) : intervalMs;
7170
7227
  state2.timer = setTimeout(() => {
7171
7228
  state2.timer = void 0;
7172
7229
  void tick(id, state2);
7173
- }, intervalMs);
7230
+ }, delay);
7174
7231
  state2.timer.unref?.();
7175
7232
  }
7176
7233
  async function summarize(id, title, excerptText, prior, abort) {
@@ -7260,13 +7317,17 @@ function timelineHooks(input, options) {
7260
7317
  const textParts = parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n").trim();
7261
7318
  const reasoningParts = parts.filter((part) => part.type === "reasoning").map((part) => part.text ?? "").join("\n").trim();
7262
7319
  const text = textParts || reasoningParts;
7320
+ const trimmed = text.trim();
7321
+ if (trimmed === "NO_PROJECT_CHANGES") {
7322
+ return { ok: true, bullets: [] };
7323
+ }
7324
+ if (trimmed === "") {
7325
+ return { ok: false, bullets: [], error: "empty summarizer output" };
7326
+ }
7263
7327
  const bullets = parseBullets(text, maxEntries);
7264
7328
  if (bullets.length > 0) {
7265
7329
  return { ok: true, bullets };
7266
7330
  }
7267
- if (text.trim() === "") {
7268
- return { ok: true, bullets: [] };
7269
- }
7270
7331
  const salvaged = salvageBullets(text, maxEntries);
7271
7332
  if (salvaged.length > 0) {
7272
7333
  log("warn", "timeline: salvaged non-bulleted output", { sessionID: id, text: text.slice(0, 300) });
@@ -7289,13 +7350,13 @@ function timelineHooks(input, options) {
7289
7350
  if (reprocessing.has(id)) return;
7290
7351
  try {
7291
7352
  state2.running = true;
7292
- state2.pending = false;
7293
7353
  let title;
7294
7354
  let info = {};
7295
7355
  if (state2.root === void 0) {
7296
7356
  info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
7297
7357
  if (!info) {
7298
7358
  sessions.delete(id);
7359
+ removeTimelineState(id);
7299
7360
  return;
7300
7361
  }
7301
7362
  title = info.title;
@@ -7303,13 +7364,19 @@ function timelineHooks(input, options) {
7303
7364
  state2.root = !info.parentID && !isInternalTitle(info.title);
7304
7365
  if (!state2.root) {
7305
7366
  sessions.delete(id);
7367
+ removeTimelineState(id);
7306
7368
  return;
7307
7369
  }
7308
7370
  }
7309
7371
  const response = await client.session.messages({ path: { id }, query: { directory } });
7310
7372
  const messages = response?.data ?? [];
7311
- const excerpt = buildExcerpt(messages, state2.cursor);
7373
+ const expanded = await expandTaskSessions(client, messages, directory, owned, debug, state2.cursor?.messageID);
7374
+ const excerpt = buildExcerpt(expanded, state2.cursor);
7312
7375
  if (!excerpt.text) {
7376
+ state2.pending = false;
7377
+ state2.failures = 0;
7378
+ state2.nextAttemptAt = void 0;
7379
+ persist(id, state2);
7313
7380
  return;
7314
7381
  }
7315
7382
  const result = await summarize(id, state2.title ?? info?.title ?? "untitled", excerpt.text, readEntries(id, 4));
@@ -7317,8 +7384,14 @@ function timelineHooks(input, options) {
7317
7384
  if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
7318
7385
  state2.cursor = excerpt.next;
7319
7386
  state2.failures = 0;
7387
+ state2.pending = false;
7388
+ state2.nextAttemptAt = void 0;
7389
+ persist(id, state2);
7320
7390
  } else {
7321
7391
  state2.failures++;
7392
+ state2.pending = true;
7393
+ state2.nextAttemptAt = Date.now() + retryDelay(state2.failures);
7394
+ persist(id, state2);
7322
7395
  log("warn", "timeline: summarize failed", { sessionID: id, error: result.error });
7323
7396
  }
7324
7397
  } catch (error) {
@@ -7356,8 +7429,8 @@ function timelineHooks(input, options) {
7356
7429
  const info = infoRes?.data;
7357
7430
  if (!info) return "Session not found.";
7358
7431
  const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
7359
- const messages = msgRes?.data ?? [];
7360
- writeFileSync9(temp, "");
7432
+ let messages = msgRes?.data ?? [];
7433
+ writeFileSync10(temp, "");
7361
7434
  for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
7362
7435
  if (abort?.aborted) {
7363
7436
  try {
@@ -7366,6 +7439,7 @@ function timelineHooks(input, options) {
7366
7439
  }
7367
7440
  return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
7368
7441
  }
7442
+ messages = await expandTaskSessions(client, messages, directory, owned, debug, cursor?.messageID);
7369
7443
  const excerpt = buildExcerpt(messages, cursor);
7370
7444
  if (!excerpt.text) break;
7371
7445
  const prior = readEntriesAt(temp, 4);
@@ -7397,13 +7471,21 @@ function timelineHooks(input, options) {
7397
7471
  }
7398
7472
  return "No entries generated; previous timeline kept.";
7399
7473
  }
7400
- renameSync6(temp, targetFile);
7474
+ renameSync7(temp, targetFile);
7401
7475
  const state2 = sessions.get(rootId);
7402
7476
  if (state2) {
7403
7477
  state2.cursor = cursor;
7404
7478
  state2.failures = 0;
7405
7479
  state2.pending = false;
7480
+ state2.nextAttemptAt = void 0;
7406
7481
  }
7482
+ persist(rootId, {
7483
+ running: state2?.running ?? false,
7484
+ pending: false,
7485
+ failures: 0,
7486
+ cursor,
7487
+ nextAttemptAt: void 0
7488
+ });
7407
7489
  return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
7408
7490
  } catch (error) {
7409
7491
  try {
@@ -7415,6 +7497,28 @@ function timelineHooks(input, options) {
7415
7497
  reprocessing.delete(rootId);
7416
7498
  }
7417
7499
  }
7500
+ function resume() {
7501
+ if (disabled || disposed) return;
7502
+ let listed;
7503
+ try {
7504
+ listed = listTimelineStates();
7505
+ } catch (error) {
7506
+ debug("resume failed", error);
7507
+ return;
7508
+ }
7509
+ listed.sort((a, b) => b.state.updatedAt - a.state.updatedAt);
7510
+ let scheduled = 0;
7511
+ for (const { sessionID, state: state2 } of listed) {
7512
+ if (state2.pending !== true) continue;
7513
+ if (sessions.has(sessionID)) continue;
7514
+ if (scheduled >= 50) continue;
7515
+ const loaded = stateFor(sessionID);
7516
+ if (loaded.root === false) continue;
7517
+ schedule(sessionID, loaded);
7518
+ scheduled++;
7519
+ }
7520
+ }
7521
+ resume();
7418
7522
  const timelineReprocess = tool2({
7419
7523
  description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
7420
7524
  args: {},
@@ -7464,6 +7568,7 @@ function timelineHooks(input, options) {
7464
7568
  }
7465
7569
  sessions.delete(id);
7466
7570
  removeEntries(id);
7571
+ removeTimelineState(id);
7467
7572
  owned.delete(id);
7468
7573
  return;
7469
7574
  }
@@ -7488,6 +7593,96 @@ function timelineHooks(input, options) {
7488
7593
  };
7489
7594
  }
7490
7595
 
7596
+ // src/lib/timer-migration.ts
7597
+ import { execFile } from "node:child_process";
7598
+ import { existsSync as existsSync6, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "node:fs";
7599
+ import { homedir as homedir8 } from "node:os";
7600
+ import { dirname as dirname6, join as join13 } from "node:path";
7601
+ var TIMER_MIGRATION_ENV = "LACODE_NO_TIMER_MIGRATION";
7602
+ var TIMER_MIGRATION_STATE_VERSION = 1;
7603
+ var defaultRun2 = (file, args, options) => new Promise((resolve5, reject) => {
7604
+ execFile(file, args, { stdio: options?.stdio ?? "ignore" }, (error) => {
7605
+ if (error) reject(error);
7606
+ else resolve5();
7607
+ });
7608
+ });
7609
+ function timerMigrationStatePath(home = homedir8()) {
7610
+ return join13(home, ".local", "share", "opencode", "lacode", "migrations.json");
7611
+ }
7612
+ function timerMigrationUnitPath(home = homedir8()) {
7613
+ return join13(home, ".config", "systemd", "user", "lacode-refresh-pool.timer");
7614
+ }
7615
+ function readState2(path4) {
7616
+ try {
7617
+ return JSON.parse(readFileSync10(path4, "utf8"));
7618
+ } catch {
7619
+ return {};
7620
+ }
7621
+ }
7622
+ function writeState2(path4, state2) {
7623
+ mkdirSync9(dirname6(path4), { recursive: true });
7624
+ const tmp = `${path4}.${process.pid}.tmp`;
7625
+ writeFileSync11(tmp, JSON.stringify(state2) + "\n");
7626
+ renameSync8(tmp, path4);
7627
+ }
7628
+ async function runTimerMigration(options = {}) {
7629
+ const env = options.env ?? process.env;
7630
+ if (env[TIMER_MIGRATION_ENV] === "1" || env.NODE_TEST_CONTEXT !== void 0) return { action: "disabled" };
7631
+ const platform = options.platform ?? process.platform;
7632
+ if (platform !== "linux") return { action: "skipped", reason: "not-linux" };
7633
+ const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
7634
+ const version = options.version ?? readInstalledVersion(packageRoot);
7635
+ if (!version) return { action: "skipped", reason: "no-version" };
7636
+ const home = options.home ?? homedir8();
7637
+ const statePath = options.statePath ?? timerMigrationStatePath(home);
7638
+ const state2 = readState2(statePath);
7639
+ if (state2.timersVersion === version && existsSync6(timerMigrationUnitPath(home))) {
7640
+ return { action: "up-to-date", version };
7641
+ }
7642
+ const run = options.run ?? defaultRun2;
7643
+ try {
7644
+ await run("systemctl", ["--user", "show-environment"], { stdio: "ignore" });
7645
+ } catch {
7646
+ return { action: "skipped", reason: "no-systemd" };
7647
+ }
7648
+ const script = join13(packageRoot, "scripts", "refresh-pool.mjs");
7649
+ if (!existsSync6(script)) return { action: "failed", version, error: `timer script not found: ${script}` };
7650
+ try {
7651
+ await run(process.execPath, [script, "--install-timer", "--stable"], { stdio: "ignore" });
7652
+ } catch (error) {
7653
+ return { action: "failed", version, error: String(error?.message ?? error) };
7654
+ }
7655
+ try {
7656
+ writeState2(statePath, {
7657
+ version: TIMER_MIGRATION_STATE_VERSION,
7658
+ timersVersion: version,
7659
+ installedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
7660
+ });
7661
+ } catch (error) {
7662
+ options.log?.("warn", `timer migration state not written: ${String(error?.message ?? error)}`);
7663
+ }
7664
+ return { action: "installed", version };
7665
+ }
7666
+
7667
+ // src/plugins/timer-migration.ts
7668
+ function timerMigrationHooks(input, options) {
7669
+ if (options?.enabled === false) return {};
7670
+ const log = (level, message, extra) => {
7671
+ void input.client.app.log({ body: { service: "lacode", level, message, extra } }).catch(() => {
7672
+ });
7673
+ };
7674
+ void runTimerMigration({
7675
+ packageRoot: PACKAGE_ROOT,
7676
+ version: readInstalledVersion(PACKAGE_ROOT),
7677
+ log
7678
+ }).then((result) => {
7679
+ if (result.action === "installed") log("info", `timer migration installed for ${result.version}`);
7680
+ else if (result.action === "failed") log("warn", `timer migration failed: ${result.error}`);
7681
+ else log("debug", `timer migration: ${result.action}`);
7682
+ }).catch((error) => log("error", `timer migration threw: ${String(error)}`));
7683
+ return {};
7684
+ }
7685
+
7491
7686
  // src/index.ts
7492
7687
  var inertHooks = {
7493
7688
  config: async () => {
@@ -7528,6 +7723,7 @@ var LaCode = (async (input, options) => {
7528
7723
  const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
7529
7724
  const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
7530
7725
  const selfupdate = await settled("self-update", () => selfUpdateHooks(input, options)) ?? inertHooks;
7726
+ void timerMigrationHooks(input, options);
7531
7727
  const feedback = await settled("feedback", () => feedbackHooks(input, options)) ?? inertHooks;
7532
7728
  const omniroute = await settled("omniroute", () => omniRouteHooks(input, options)) ?? inertHooks;
7533
7729
  const compat = await settled("provider-compat", () => providerCompatHooks(input)) ?? inertHooks;