@lawaty/lacode 1.3.0-beta.9 → 2.0.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/CHANGELOG.md +30 -1
- package/README.md +3 -6
- package/dist/agents.js +3 -41
- package/dist/commands.js +6 -5
- package/dist/index.js +200 -235
- package/docs/CONTEXT.md +17 -33
- package/docs/USER-STORIES.md +27 -2
- package/package.json +5 -3
- package/scripts/refresh-pool.mjs +335 -0
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.
|
|
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
|
|
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
|
|
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)\`).
|
|
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
|
|
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.`
|
|
@@ -2085,9 +2044,13 @@ $ARGUMENTS
|
|
|
2085
2044
|
|
|
2086
2045
|
Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
|
|
2087
2046
|
},
|
|
2047
|
+
// `agent: "implement"` is a subagent, so without `subtask: false` this command
|
|
2048
|
+
// would run in a child session and `timeline_reprocess` would rebuild the
|
|
2049
|
+
// child's timeline instead of the caller's.
|
|
2088
2050
|
"reprocess-timeline": {
|
|
2089
2051
|
description: "Rebuild this session's timeline from its full transcript",
|
|
2090
2052
|
agent: "implement",
|
|
2053
|
+
subtask: false,
|
|
2091
2054
|
template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
|
|
2092
2055
|
},
|
|
2093
2056
|
"image-in-cb": {
|
|
@@ -4121,110 +4084,10 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
|
|
|
4121
4084
|
});
|
|
4122
4085
|
|
|
4123
4086
|
// src/plugins/context-autoupdate.ts
|
|
4124
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2,
|
|
4087
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4125
4088
|
import { homedir as homedir4 } from "node:os";
|
|
4126
4089
|
import * as path3 from "node:path";
|
|
4127
4090
|
|
|
4128
|
-
// src/lib/active-backlog.ts
|
|
4129
|
-
var ACTIVE_BACKLOG_FILE = "active.md";
|
|
4130
|
-
var RETENTION_DAYS = 7;
|
|
4131
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4132
|
-
var HEADER = `# Active backlog
|
|
4133
|
-
|
|
4134
|
-
Tasks in flight, grouped by the week they were opened. A finished task stays
|
|
4135
|
-
here for a week after it was finished and is then dropped; an open task stays
|
|
4136
|
-
however long it takes. Add a line under the right week, and give a finished
|
|
4137
|
-
task its date: \`- [x] Title \u2014 opened 2026-W43, finished 2026-10-19\`.`;
|
|
4138
|
-
var TASK_LINE = /^\s*(?:[-*+]|\d+[.)])\s*\[([ xX])\]\s*(.*)$/;
|
|
4139
|
-
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;
|
|
4140
|
-
var isCalendarDate = (s) => {
|
|
4141
|
-
const at = Date.parse(`${s}T00:00:00Z`);
|
|
4142
|
-
return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === s;
|
|
4143
|
-
};
|
|
4144
|
-
var parseBacklog = (text) => {
|
|
4145
|
-
const entries = [];
|
|
4146
|
-
for (const raw of text.split(/\r?\n/)) {
|
|
4147
|
-
const line = TASK_LINE.exec(raw);
|
|
4148
|
-
if (!line) continue;
|
|
4149
|
-
const finished = line[1] !== " ";
|
|
4150
|
-
let week;
|
|
4151
|
-
let finishedAt;
|
|
4152
|
-
const stripped = line[2].replace(META, (fragment, openedWeek, finishedDate) => {
|
|
4153
|
-
if (openedWeek !== void 0) {
|
|
4154
|
-
week ??= openedWeek.toUpperCase();
|
|
4155
|
-
return "";
|
|
4156
|
-
}
|
|
4157
|
-
if (finishedDate !== void 0 && isCalendarDate(finishedDate)) {
|
|
4158
|
-
finishedAt ??= finishedDate;
|
|
4159
|
-
return "";
|
|
4160
|
-
}
|
|
4161
|
-
return fragment;
|
|
4162
|
-
});
|
|
4163
|
-
const title = stripped.replace(/^[\s—–-]+|[\s—–-]+$/g, "");
|
|
4164
|
-
if (!title) continue;
|
|
4165
|
-
entries.push({ title, ...week ? { week } : {}, finished, ...finishedAt ? { finishedAt } : {} });
|
|
4166
|
-
}
|
|
4167
|
-
return entries;
|
|
4168
|
-
};
|
|
4169
|
-
var ISO_WEEK = /^(\d{4})-W(\d{2})$/i;
|
|
4170
|
-
var newestFirst = (a, b) => {
|
|
4171
|
-
const ma = ISO_WEEK.exec(a);
|
|
4172
|
-
const mb = ISO_WEEK.exec(b);
|
|
4173
|
-
if (ma && mb) return Number(mb[1]) - Number(ma[1]) || Number(mb[2]) - Number(ma[2]);
|
|
4174
|
-
if (ma) return -1;
|
|
4175
|
-
if (mb) return 1;
|
|
4176
|
-
return a < b ? 1 : a > b ? -1 : 0;
|
|
4177
|
-
};
|
|
4178
|
-
var renderLine = (entry) => {
|
|
4179
|
-
const box = entry.finished ? "[x]" : "[ ]";
|
|
4180
|
-
const meta = [];
|
|
4181
|
-
if (entry.week) meta.push(`opened ${entry.week}`);
|
|
4182
|
-
if (entry.finished && entry.finishedAt) meta.push(`finished ${entry.finishedAt}`);
|
|
4183
|
-
const head = `- ${box} ${entry.title}`;
|
|
4184
|
-
if (meta.length === 0) return head;
|
|
4185
|
-
return entry.week ? `${head} \u2014 ${meta.join(", ")}` : `${head}, ${meta.join(", ")}`;
|
|
4186
|
-
};
|
|
4187
|
-
var renderBacklog = (entries) => {
|
|
4188
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
4189
|
-
for (const entry of entries) {
|
|
4190
|
-
const key = entry.week ? entry.week : null;
|
|
4191
|
-
const bucket = buckets.get(key);
|
|
4192
|
-
if (bucket) bucket.push(entry);
|
|
4193
|
-
else buckets.set(key, [entry]);
|
|
4194
|
-
}
|
|
4195
|
-
const weeks = [...buckets.keys()].filter((week) => week !== null).sort(newestFirst);
|
|
4196
|
-
let out = HEADER;
|
|
4197
|
-
for (const week of weeks) out += `
|
|
4198
|
-
|
|
4199
|
-
## ${week}
|
|
4200
|
-
|
|
4201
|
-
${buckets.get(week).map(renderLine).join("\n")}`;
|
|
4202
|
-
const unscheduled = buckets.get(null);
|
|
4203
|
-
if (unscheduled) out += `
|
|
4204
|
-
|
|
4205
|
-
## Unscheduled
|
|
4206
|
-
|
|
4207
|
-
${unscheduled.map(renderLine).join("\n")}`;
|
|
4208
|
-
return `${out}
|
|
4209
|
-
`;
|
|
4210
|
-
};
|
|
4211
|
-
var normalizeBacklog = (text, now = Date.now()) => {
|
|
4212
|
-
const entries = parseBacklog(text);
|
|
4213
|
-
const kept = [];
|
|
4214
|
-
const removed = [];
|
|
4215
|
-
for (const entry of entries) {
|
|
4216
|
-
if (entry.finished && entry.finishedAt) {
|
|
4217
|
-
const at = Date.parse(`${entry.finishedAt}T00:00:00Z`);
|
|
4218
|
-
if (Number.isFinite(at) && now - at > RETENTION_DAYS * DAY_MS) {
|
|
4219
|
-
removed.push(entry.title);
|
|
4220
|
-
continue;
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
kept.push(entry);
|
|
4224
|
-
}
|
|
4225
|
-
return { text: renderBacklog(kept), removed };
|
|
4226
|
-
};
|
|
4227
|
-
|
|
4228
4091
|
// src/lib/map-schema.ts
|
|
4229
4092
|
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4230
4093
|
import * as path from "node:path";
|
|
@@ -4274,21 +4137,11 @@ var MAP_SPECS = [
|
|
|
4274
4137
|
kind: "entries",
|
|
4275
4138
|
provisionalDays: PROVISIONAL_DAYS,
|
|
4276
4139
|
sectionRequired: true
|
|
4277
|
-
},
|
|
4278
|
-
{
|
|
4279
|
-
file: ACTIVE_BACKLOG_FILE,
|
|
4280
|
-
charter: "work in flight, grouped by the ISO week it was opened",
|
|
4281
|
-
budget: 60,
|
|
4282
|
-
minEntries: 0,
|
|
4283
|
-
kind: "backlog",
|
|
4284
|
-
provisionalDays: PROVISIONAL_DAYS,
|
|
4285
|
-
sectionRequired: false
|
|
4286
4140
|
}
|
|
4287
4141
|
];
|
|
4142
|
+
var NAVIGATION_FILES = MAP_SPECS.map((spec) => spec.file);
|
|
4288
4143
|
var MAP_SPEC_BY_FILE = new Map(MAP_SPECS.map((spec) => [spec.file, spec]));
|
|
4289
|
-
var
|
|
4290
|
-
var NAVIGATION_FILES = MAP_SPECS.filter((spec) => spec.kind === "entries").map((spec) => spec.file);
|
|
4291
|
-
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
4144
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4292
4145
|
var ENTRY_START = /^([-*+])\s+\[(binding|provisional|disputed)\]\s+(.*)$/i;
|
|
4293
4146
|
var CONTINUATION = /^\s{2,}\S/;
|
|
4294
4147
|
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
@@ -4299,7 +4152,7 @@ var VERIFIED = /\(verified\s+(\d{4}-\d{2}-\d{2})\)\s*$/i;
|
|
|
4299
4152
|
var POINTER = /(?:—|–|→|->)\s*See\s+`([^`\n]+)`(?:\s*§\s*([^()\n]+?))?(?:\s*\(verified\s+(\d{4}-\d{2}-\d{2})\))?\s*$/i;
|
|
4300
4153
|
var DECISION_RECORD = /^\s*(?:#{1,6}\s*)?(?:\*\*)?(Context|Decision|Consequences|Alternatives|Status|Trade-?offs|Rationale)(?:\*\*)?\s*(?::|$)/im;
|
|
4301
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;
|
|
4302
|
-
var
|
|
4155
|
+
var isCalendarDate = (value) => {
|
|
4303
4156
|
const at = Date.parse(`${value}T00:00:00Z`);
|
|
4304
4157
|
return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
|
|
4305
4158
|
};
|
|
@@ -4517,10 +4370,6 @@ var validateMapText = (spec, text, options) => {
|
|
|
4517
4370
|
if (nonBlank > spec.budget) {
|
|
4518
4371
|
push("budget", `${nonBlank} non-blank lines exceeds the ${spec.budget}-line budget; consolidate, do not pad`);
|
|
4519
4372
|
}
|
|
4520
|
-
if (spec.kind === "backlog") {
|
|
4521
|
-
if (!text.startsWith("# ")) push("charter", "active backlog must start with a top-level heading");
|
|
4522
|
-
return { spec, lines: nonBlank, entries: [], violations };
|
|
4523
|
-
}
|
|
4524
4373
|
const entries = parseMapEntries(text);
|
|
4525
4374
|
let inFence = false;
|
|
4526
4375
|
let tableReported = false;
|
|
@@ -4562,9 +4411,9 @@ var validateMapText = (spec, text, options) => {
|
|
|
4562
4411
|
}
|
|
4563
4412
|
if (!entry.verified) {
|
|
4564
4413
|
push("schema", "entry has no (verified YYYY-MM-DD); the lifecycle and drift checks need it", line);
|
|
4565
|
-
} else if (!
|
|
4414
|
+
} else if (!isCalendarDate(entry.verified)) {
|
|
4566
4415
|
push("schema", `verified date ${entry.verified} is not a real calendar date`, line);
|
|
4567
|
-
} else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) +
|
|
4416
|
+
} else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS) {
|
|
4568
4417
|
push("schema", `verified date ${entry.verified} is more than a day in the future`, line);
|
|
4569
4418
|
}
|
|
4570
4419
|
if (!entry.pointer) {
|
|
@@ -4614,14 +4463,6 @@ var validateMapText = (spec, text, options) => {
|
|
|
4614
4463
|
};
|
|
4615
4464
|
var normalizeMapText = (spec, text, options = {}) => {
|
|
4616
4465
|
const now = options.now ?? Date.now();
|
|
4617
|
-
if (spec.kind === "backlog") {
|
|
4618
|
-
const result = normalizeBacklog(text, now);
|
|
4619
|
-
return {
|
|
4620
|
-
text: result.text,
|
|
4621
|
-
removed: result.removed.map((title) => ({ line: 0, text: title })),
|
|
4622
|
-
kept: 0
|
|
4623
|
-
};
|
|
4624
|
-
}
|
|
4625
4466
|
const entries = parseMapEntries(text);
|
|
4626
4467
|
const drop = /* @__PURE__ */ new Set();
|
|
4627
4468
|
const removed = [];
|
|
@@ -4631,7 +4472,7 @@ var normalizeMapText = (spec, text, options = {}) => {
|
|
|
4631
4472
|
if (options.canResolve && !options.canResolve(entry.pointer, entry.section)) continue;
|
|
4632
4473
|
const at = Date.parse(`${entry.verified}T00:00:00Z`);
|
|
4633
4474
|
if (!Number.isFinite(at)) continue;
|
|
4634
|
-
if (now - at <= spec.provisionalDays *
|
|
4475
|
+
if (now - at <= spec.provisionalDays * DAY_MS) continue;
|
|
4635
4476
|
for (let i = entry.startLine; i < entry.endLine; i++) drop.add(i);
|
|
4636
4477
|
removed.push({ line: entry.startLine + 1, text: truncate(entry.rule) });
|
|
4637
4478
|
}
|
|
@@ -4661,8 +4502,6 @@ var maintainMap = (root, mapDir, now = Date.now()) => {
|
|
|
4661
4502
|
if (result.removed.length > 0) {
|
|
4662
4503
|
removedTotal += result.removed.length;
|
|
4663
4504
|
files.push({ file: spec.file, changed, removed: result.removed });
|
|
4664
|
-
} else if (changed) {
|
|
4665
|
-
files.push({ file: spec.file, changed, removed: [] });
|
|
4666
4505
|
}
|
|
4667
4506
|
}
|
|
4668
4507
|
return { files, removedTotal };
|
|
@@ -4688,21 +4527,19 @@ var auditMap = (root, mapDir, now = Date.now()) => {
|
|
|
4688
4527
|
const result = validateMapText(spec, text, { root, now });
|
|
4689
4528
|
files.push(result);
|
|
4690
4529
|
violations.push(...result.violations);
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
rulesSeen.set(key, spec.file);
|
|
4705
|
-
}
|
|
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);
|
|
4706
4543
|
}
|
|
4707
4544
|
}
|
|
4708
4545
|
}
|
|
@@ -4970,7 +4807,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
4970
4807
|
});
|
|
4971
4808
|
const root = path3.resolve(directory);
|
|
4972
4809
|
const contextAbs = path3.join(root, CONTEXT_DIR2);
|
|
4973
|
-
const activeBacklogPath = path3.join(contextAbs, ACTIVE_BACKLOG_FILE);
|
|
4974
4810
|
const config = findProjectConfig();
|
|
4975
4811
|
const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
|
|
4976
4812
|
if (writerProblem) void log("error", writerProblem);
|
|
@@ -4980,22 +4816,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
4980
4816
|
if (absolute === contextAbs || absolute.startsWith(contextAbs + path3.sep)) return false;
|
|
4981
4817
|
return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
4982
4818
|
};
|
|
4983
|
-
const normalizeActiveBacklog = async () => {
|
|
4984
|
-
try {
|
|
4985
|
-
if (!existsSync4(activeBacklogPath)) return { removed: 0 };
|
|
4986
|
-
const before = readFileSync7(activeBacklogPath, "utf8");
|
|
4987
|
-
const { text, removed } = normalizeBacklog(before);
|
|
4988
|
-
if (text !== before) writeFileSync6(activeBacklogPath, text);
|
|
4989
|
-
if (removed.length > 0) {
|
|
4990
|
-
await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
|
|
4991
|
-
removed: removed.length
|
|
4992
|
-
});
|
|
4993
|
-
}
|
|
4994
|
-
return { removed: removed.length };
|
|
4995
|
-
} catch {
|
|
4996
|
-
return { removed: 0 };
|
|
4997
|
-
}
|
|
4998
|
-
};
|
|
4999
4819
|
const applyLifecycle = async () => {
|
|
5000
4820
|
try {
|
|
5001
4821
|
const result = maintainMap(root, contextAbs);
|
|
@@ -5055,7 +4875,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5055
4875
|
"",
|
|
5056
4876
|
ENTRY_CONTRACT,
|
|
5057
4877
|
"",
|
|
5058
|
-
"Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets.
|
|
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."
|
|
5059
4879
|
].join("\n") : [
|
|
5060
4880
|
"Incremental context update. Project root: " + directory,
|
|
5061
4881
|
"",
|
|
@@ -5066,17 +4886,13 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5066
4886
|
"",
|
|
5067
4887
|
ENTRY_CONTRACT,
|
|
5068
4888
|
"",
|
|
5069
|
-
'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
|
|
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".'
|
|
5070
4890
|
].join("\n");
|
|
5071
4891
|
return {
|
|
5072
4892
|
event: async ({ event }) => {
|
|
5073
4893
|
try {
|
|
5074
4894
|
if (event.type === "file.edited") {
|
|
5075
4895
|
const file = event.properties.file;
|
|
5076
|
-
if (path3.resolve(root, file) === activeBacklogPath) {
|
|
5077
|
-
await normalizeActiveBacklog();
|
|
5078
|
-
return;
|
|
5079
|
-
}
|
|
5080
4896
|
if (isTracked(file)) pending.add(file);
|
|
5081
4897
|
return;
|
|
5082
4898
|
}
|
|
@@ -5188,7 +5004,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5188
5004
|
});
|
|
5189
5005
|
|
|
5190
5006
|
// src/lib/feedback.ts
|
|
5191
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
5007
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5192
5008
|
import { homedir as homedir5 } from "node:os";
|
|
5193
5009
|
import { dirname as dirname4, join as join9 } from "node:path";
|
|
5194
5010
|
var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
|
|
@@ -5196,7 +5012,7 @@ var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you us
|
|
|
5196
5012
|
var feedbackStatePath = () => join9(homedir5(), ".local", "share", "opencode", "lacode", "feedback.json");
|
|
5197
5013
|
function readFeedbackState(path4 = feedbackStatePath()) {
|
|
5198
5014
|
try {
|
|
5199
|
-
const parsed = JSON.parse(
|
|
5015
|
+
const parsed = JSON.parse(readFileSync7(path4, "utf8"));
|
|
5200
5016
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
5201
5017
|
return parsed;
|
|
5202
5018
|
} catch (error) {
|
|
@@ -5270,7 +5086,7 @@ async function feedbackHooks(input, options, deps = {}) {
|
|
|
5270
5086
|
|
|
5271
5087
|
// src/plugins/image-paste.ts
|
|
5272
5088
|
import { createHash, randomUUID } from "node:crypto";
|
|
5273
|
-
import { globSync, mkdirSync as mkdirSync7, readFileSync as
|
|
5089
|
+
import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5274
5090
|
import { homedir as homedir6 } from "node:os";
|
|
5275
5091
|
import { basename as basename3, join as join10 } from "node:path";
|
|
5276
5092
|
|
|
@@ -5527,7 +5343,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
5527
5343
|
}
|
|
5528
5344
|
let bytes;
|
|
5529
5345
|
try {
|
|
5530
|
-
bytes =
|
|
5346
|
+
bytes = readFileSync8(filePath);
|
|
5531
5347
|
} catch {
|
|
5532
5348
|
skipped.push(name);
|
|
5533
5349
|
continue;
|
|
@@ -6673,27 +6489,35 @@ async function selfUpdateHooks(input, options) {
|
|
|
6673
6489
|
}
|
|
6674
6490
|
|
|
6675
6491
|
// src/plugins/timeline.ts
|
|
6676
|
-
import { writeFileSync as
|
|
6492
|
+
import { writeFileSync as writeFileSync10, renameSync as renameSync7, rmSync as rmSync3 } from "node:fs";
|
|
6677
6493
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
6678
6494
|
|
|
6679
6495
|
// src/lib/timeline.ts
|
|
6680
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as
|
|
6496
|
+
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";
|
|
6681
6497
|
import { homedir as homedir7 } from "node:os";
|
|
6682
6498
|
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
6499
|
+
var TIMELINE_STATE_VERSION = 1;
|
|
6683
6500
|
function timelineDir() {
|
|
6684
6501
|
const env = process.env.LACODE_TIMELINE_DIR;
|
|
6685
6502
|
if (env) return resolve4(env);
|
|
6686
6503
|
return join11(homedir7(), ".local/share/opencode/lacode/timeline");
|
|
6687
6504
|
}
|
|
6688
|
-
function
|
|
6505
|
+
function assertValidSessionID(sessionID) {
|
|
6689
6506
|
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
|
|
6507
|
+
}
|
|
6508
|
+
function timelineFile(sessionID) {
|
|
6509
|
+
assertValidSessionID(sessionID);
|
|
6690
6510
|
return join11(timelineDir(), sessionID + ".jsonl");
|
|
6691
6511
|
}
|
|
6512
|
+
function timelineStateFile(sessionID) {
|
|
6513
|
+
assertValidSessionID(sessionID);
|
|
6514
|
+
return join11(timelineDir(), sessionID + ".state.json");
|
|
6515
|
+
}
|
|
6692
6516
|
function normalizeThrough(through) {
|
|
6693
6517
|
if (!through || typeof through !== "object") return void 0;
|
|
6694
6518
|
const out = {};
|
|
6695
|
-
if (typeof through.messageID === "string") out.messageID = through.messageID;
|
|
6696
|
-
if (typeof through.time === "number") out.time = through.time;
|
|
6519
|
+
if (typeof through.messageID === "string" && through.messageID.length > 0) out.messageID = through.messageID;
|
|
6520
|
+
if (typeof through.time === "number" && Number.isFinite(through.time)) out.time = through.time;
|
|
6697
6521
|
if (out.messageID === void 0 && out.time === void 0) return void 0;
|
|
6698
6522
|
return out;
|
|
6699
6523
|
}
|
|
@@ -6734,7 +6558,7 @@ function appendEntriesAt(file, texts, now, through) {
|
|
|
6734
6558
|
function readEntriesAt(file, limit) {
|
|
6735
6559
|
let raw = "";
|
|
6736
6560
|
try {
|
|
6737
|
-
raw =
|
|
6561
|
+
raw = readFileSync9(file, "utf8");
|
|
6738
6562
|
} catch {
|
|
6739
6563
|
return [];
|
|
6740
6564
|
}
|
|
@@ -6768,6 +6592,70 @@ function removeEntries(sessionID) {
|
|
|
6768
6592
|
} catch {
|
|
6769
6593
|
}
|
|
6770
6594
|
}
|
|
6595
|
+
function normalizePersistedState(raw) {
|
|
6596
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
6597
|
+
const cursor = normalizeThrough(obj.cursor);
|
|
6598
|
+
const failures = typeof obj.failures === "number" && Number.isFinite(obj.failures) ? Math.max(0, Math.trunc(obj.failures)) : 0;
|
|
6599
|
+
const pending = obj.pending === true;
|
|
6600
|
+
const nextAttemptAt = typeof obj.nextAttemptAt === "number" && Number.isFinite(obj.nextAttemptAt) && obj.nextAttemptAt > 0 ? obj.nextAttemptAt : void 0;
|
|
6601
|
+
const updatedAt = typeof obj.updatedAt === "number" && Number.isFinite(obj.updatedAt) ? obj.updatedAt : Date.now();
|
|
6602
|
+
const state2 = {
|
|
6603
|
+
version: TIMELINE_STATE_VERSION,
|
|
6604
|
+
failures,
|
|
6605
|
+
pending,
|
|
6606
|
+
updatedAt
|
|
6607
|
+
};
|
|
6608
|
+
if (cursor) state2.cursor = cursor;
|
|
6609
|
+
if (nextAttemptAt !== void 0) state2.nextAttemptAt = nextAttemptAt;
|
|
6610
|
+
return state2;
|
|
6611
|
+
}
|
|
6612
|
+
function readTimelineState(sessionID) {
|
|
6613
|
+
try {
|
|
6614
|
+
const raw = readFileSync9(timelineStateFile(sessionID), "utf8");
|
|
6615
|
+
return normalizePersistedState(JSON.parse(raw));
|
|
6616
|
+
} catch {
|
|
6617
|
+
return void 0;
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
function writeTimelineState(sessionID, state2) {
|
|
6621
|
+
const file = timelineStateFile(sessionID);
|
|
6622
|
+
mkdirSync8(dirname5(file), { recursive: true });
|
|
6623
|
+
const tmp = file + ".tmp." + process.pid + "." + Math.random().toString(36).slice(2);
|
|
6624
|
+
try {
|
|
6625
|
+
writeFileSync9(tmp, JSON.stringify(state2) + "\n");
|
|
6626
|
+
renameSync6(tmp, file);
|
|
6627
|
+
} catch (err) {
|
|
6628
|
+
try {
|
|
6629
|
+
rmSync2(tmp, { force: true });
|
|
6630
|
+
} catch {
|
|
6631
|
+
}
|
|
6632
|
+
throw err;
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
function removeTimelineState(sessionID) {
|
|
6636
|
+
try {
|
|
6637
|
+
rmSync2(timelineStateFile(sessionID), { force: true });
|
|
6638
|
+
} catch {
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
function listTimelineStates() {
|
|
6642
|
+
let names;
|
|
6643
|
+
try {
|
|
6644
|
+
names = readdirSync3(timelineDir());
|
|
6645
|
+
} catch {
|
|
6646
|
+
return [];
|
|
6647
|
+
}
|
|
6648
|
+
const out = [];
|
|
6649
|
+
for (const name of names) {
|
|
6650
|
+
if (!name.endsWith(".state.json")) continue;
|
|
6651
|
+
const sessionID = name.slice(0, -".state.json".length);
|
|
6652
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) continue;
|
|
6653
|
+
const state2 = readTimelineState(sessionID);
|
|
6654
|
+
if (!state2) continue;
|
|
6655
|
+
out.push({ sessionID, state: state2 });
|
|
6656
|
+
}
|
|
6657
|
+
return out;
|
|
6658
|
+
}
|
|
6771
6659
|
function collapse(s) {
|
|
6772
6660
|
return s.replace(/\s+/g, " ").trim();
|
|
6773
6661
|
}
|
|
@@ -7100,6 +6988,11 @@ function timelineHooks(input, options) {
|
|
|
7100
6988
|
const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
|
|
7101
6989
|
const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
7102
6990
|
const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
|
|
6991
|
+
const rawRetryMs = Number(config?.retryMs ?? 3e4);
|
|
6992
|
+
const retryMs = Number.isFinite(rawRetryMs) ? Math.max(1, rawRetryMs) : 3e4;
|
|
6993
|
+
const rawMaxBackoffMs = Number(config?.maxBackoffMs ?? 30 * 6e4);
|
|
6994
|
+
const maxBackoffMs = Math.max(retryMs, Number.isFinite(rawMaxBackoffMs) ? Math.max(1, rawMaxBackoffMs) : 30 * 6e4);
|
|
6995
|
+
const retryDelay = (failures) => Math.min(retryMs * 2 ** Math.min(Math.max(failures - 1, 0), 20), maxBackoffMs);
|
|
7103
6996
|
const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
|
|
7104
6997
|
sessions: /* @__PURE__ */ new Map(),
|
|
7105
6998
|
owned: /* @__PURE__ */ new Set(),
|
|
@@ -7116,21 +7009,37 @@ function timelineHooks(input, options) {
|
|
|
7116
7009
|
if (!id) return void 0;
|
|
7117
7010
|
return { providerID: provider, modelID: id };
|
|
7118
7011
|
};
|
|
7012
|
+
const persist = (id, state2) => {
|
|
7013
|
+
try {
|
|
7014
|
+
writeTimelineState(id, {
|
|
7015
|
+
version: TIMELINE_STATE_VERSION,
|
|
7016
|
+
cursor: state2.cursor,
|
|
7017
|
+
failures: state2.failures,
|
|
7018
|
+
pending: state2.pending,
|
|
7019
|
+
nextAttemptAt: state2.nextAttemptAt,
|
|
7020
|
+
updatedAt: Date.now()
|
|
7021
|
+
});
|
|
7022
|
+
} catch (error) {
|
|
7023
|
+
debug("persist failed", { sessionID: id, error });
|
|
7024
|
+
}
|
|
7025
|
+
};
|
|
7119
7026
|
const stateFor = (id) => {
|
|
7120
7027
|
let state2 = sessions.get(id);
|
|
7121
7028
|
if (!state2) {
|
|
7029
|
+
const persisted = readTimelineState(id);
|
|
7122
7030
|
state2 = {
|
|
7123
7031
|
running: false,
|
|
7124
|
-
pending: false,
|
|
7125
|
-
failures: 0,
|
|
7126
|
-
cursor: readEntries(id, 1)[0]?.through
|
|
7032
|
+
pending: persisted?.pending ?? false,
|
|
7033
|
+
failures: persisted?.failures ?? 0,
|
|
7034
|
+
cursor: persisted?.cursor ?? readEntries(id, 1)[0]?.through,
|
|
7035
|
+
nextAttemptAt: persisted?.nextAttemptAt
|
|
7127
7036
|
};
|
|
7128
7037
|
sessions.set(id, state2);
|
|
7129
7038
|
while (sessions.size > 500) {
|
|
7130
7039
|
let evicted = false;
|
|
7131
7040
|
for (const [key, st] of sessions) {
|
|
7132
7041
|
if (key === id) continue;
|
|
7133
|
-
if (!st.timer && !st.running) {
|
|
7042
|
+
if (!st.timer && !st.running && !st.pending && st.nextAttemptAt === void 0) {
|
|
7134
7043
|
sessions.delete(key);
|
|
7135
7044
|
evicted = true;
|
|
7136
7045
|
break;
|
|
@@ -7145,16 +7054,29 @@ function timelineHooks(input, options) {
|
|
|
7145
7054
|
if (disabled || disposed) return;
|
|
7146
7055
|
const state2 = stateFor(id);
|
|
7147
7056
|
if (state2.root === false) return;
|
|
7057
|
+
const wasPending = state2.pending;
|
|
7148
7058
|
state2.pending = true;
|
|
7059
|
+
if (!wasPending) persist(id, state2);
|
|
7149
7060
|
if (state2.timer || state2.running) return;
|
|
7150
7061
|
schedule(id, state2);
|
|
7151
7062
|
};
|
|
7152
7063
|
function flush(id) {
|
|
7153
7064
|
if (disabled || disposed) return;
|
|
7154
|
-
|
|
7155
|
-
if (!state2)
|
|
7065
|
+
let state2 = sessions.get(id);
|
|
7066
|
+
if (!state2) {
|
|
7067
|
+
if (readTimelineState(id)?.pending === true) state2 = stateFor(id);
|
|
7068
|
+
else return;
|
|
7069
|
+
}
|
|
7156
7070
|
if (state2.root === false) return;
|
|
7157
7071
|
if (!state2.pending || state2.running) return;
|
|
7072
|
+
if (state2.nextAttemptAt && state2.nextAttemptAt > Date.now()) {
|
|
7073
|
+
if (state2.timer) {
|
|
7074
|
+
clearTimeout(state2.timer);
|
|
7075
|
+
state2.timer = void 0;
|
|
7076
|
+
}
|
|
7077
|
+
schedule(id, state2);
|
|
7078
|
+
return;
|
|
7079
|
+
}
|
|
7158
7080
|
if (state2.timer) {
|
|
7159
7081
|
clearTimeout(state2.timer);
|
|
7160
7082
|
state2.timer = void 0;
|
|
@@ -7163,10 +7085,11 @@ function timelineHooks(input, options) {
|
|
|
7163
7085
|
}
|
|
7164
7086
|
function schedule(id, state2) {
|
|
7165
7087
|
if (state2.timer) clearTimeout(state2.timer);
|
|
7088
|
+
const delay = typeof state2.nextAttemptAt === "number" ? Math.max(0, state2.nextAttemptAt - Date.now()) : state2.failures > 0 ? retryDelay(state2.failures) : intervalMs;
|
|
7166
7089
|
state2.timer = setTimeout(() => {
|
|
7167
7090
|
state2.timer = void 0;
|
|
7168
7091
|
void tick(id, state2);
|
|
7169
|
-
},
|
|
7092
|
+
}, delay);
|
|
7170
7093
|
state2.timer.unref?.();
|
|
7171
7094
|
}
|
|
7172
7095
|
async function summarize(id, title, excerptText, prior, abort) {
|
|
@@ -7285,13 +7208,13 @@ function timelineHooks(input, options) {
|
|
|
7285
7208
|
if (reprocessing.has(id)) return;
|
|
7286
7209
|
try {
|
|
7287
7210
|
state2.running = true;
|
|
7288
|
-
state2.pending = false;
|
|
7289
7211
|
let title;
|
|
7290
7212
|
let info = {};
|
|
7291
7213
|
if (state2.root === void 0) {
|
|
7292
7214
|
info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
|
|
7293
7215
|
if (!info) {
|
|
7294
7216
|
sessions.delete(id);
|
|
7217
|
+
removeTimelineState(id);
|
|
7295
7218
|
return;
|
|
7296
7219
|
}
|
|
7297
7220
|
title = info.title;
|
|
@@ -7299,6 +7222,7 @@ function timelineHooks(input, options) {
|
|
|
7299
7222
|
state2.root = !info.parentID && !isInternalTitle(info.title);
|
|
7300
7223
|
if (!state2.root) {
|
|
7301
7224
|
sessions.delete(id);
|
|
7225
|
+
removeTimelineState(id);
|
|
7302
7226
|
return;
|
|
7303
7227
|
}
|
|
7304
7228
|
}
|
|
@@ -7306,6 +7230,10 @@ function timelineHooks(input, options) {
|
|
|
7306
7230
|
const messages = response?.data ?? [];
|
|
7307
7231
|
const excerpt = buildExcerpt(messages, state2.cursor);
|
|
7308
7232
|
if (!excerpt.text) {
|
|
7233
|
+
state2.pending = false;
|
|
7234
|
+
state2.failures = 0;
|
|
7235
|
+
state2.nextAttemptAt = void 0;
|
|
7236
|
+
persist(id, state2);
|
|
7309
7237
|
return;
|
|
7310
7238
|
}
|
|
7311
7239
|
const result = await summarize(id, state2.title ?? info?.title ?? "untitled", excerpt.text, readEntries(id, 4));
|
|
@@ -7313,8 +7241,14 @@ function timelineHooks(input, options) {
|
|
|
7313
7241
|
if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
|
|
7314
7242
|
state2.cursor = excerpt.next;
|
|
7315
7243
|
state2.failures = 0;
|
|
7244
|
+
state2.pending = false;
|
|
7245
|
+
state2.nextAttemptAt = void 0;
|
|
7246
|
+
persist(id, state2);
|
|
7316
7247
|
} else {
|
|
7317
7248
|
state2.failures++;
|
|
7249
|
+
state2.pending = true;
|
|
7250
|
+
state2.nextAttemptAt = Date.now() + retryDelay(state2.failures);
|
|
7251
|
+
persist(id, state2);
|
|
7318
7252
|
log("warn", "timeline: summarize failed", { sessionID: id, error: result.error });
|
|
7319
7253
|
}
|
|
7320
7254
|
} catch (error) {
|
|
@@ -7353,7 +7287,7 @@ function timelineHooks(input, options) {
|
|
|
7353
7287
|
if (!info) return "Session not found.";
|
|
7354
7288
|
const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
|
|
7355
7289
|
const messages = msgRes?.data ?? [];
|
|
7356
|
-
|
|
7290
|
+
writeFileSync10(temp, "");
|
|
7357
7291
|
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
7358
7292
|
if (abort?.aborted) {
|
|
7359
7293
|
try {
|
|
@@ -7393,13 +7327,21 @@ function timelineHooks(input, options) {
|
|
|
7393
7327
|
}
|
|
7394
7328
|
return "No entries generated; previous timeline kept.";
|
|
7395
7329
|
}
|
|
7396
|
-
|
|
7330
|
+
renameSync7(temp, targetFile);
|
|
7397
7331
|
const state2 = sessions.get(rootId);
|
|
7398
7332
|
if (state2) {
|
|
7399
7333
|
state2.cursor = cursor;
|
|
7400
7334
|
state2.failures = 0;
|
|
7401
7335
|
state2.pending = false;
|
|
7336
|
+
state2.nextAttemptAt = void 0;
|
|
7402
7337
|
}
|
|
7338
|
+
persist(rootId, {
|
|
7339
|
+
running: state2?.running ?? false,
|
|
7340
|
+
pending: false,
|
|
7341
|
+
failures: 0,
|
|
7342
|
+
cursor,
|
|
7343
|
+
nextAttemptAt: void 0
|
|
7344
|
+
});
|
|
7403
7345
|
return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
|
|
7404
7346
|
} catch (error) {
|
|
7405
7347
|
try {
|
|
@@ -7411,6 +7353,28 @@ function timelineHooks(input, options) {
|
|
|
7411
7353
|
reprocessing.delete(rootId);
|
|
7412
7354
|
}
|
|
7413
7355
|
}
|
|
7356
|
+
function resume() {
|
|
7357
|
+
if (disabled || disposed) return;
|
|
7358
|
+
let listed;
|
|
7359
|
+
try {
|
|
7360
|
+
listed = listTimelineStates();
|
|
7361
|
+
} catch (error) {
|
|
7362
|
+
debug("resume failed", error);
|
|
7363
|
+
return;
|
|
7364
|
+
}
|
|
7365
|
+
listed.sort((a, b) => b.state.updatedAt - a.state.updatedAt);
|
|
7366
|
+
let scheduled = 0;
|
|
7367
|
+
for (const { sessionID, state: state2 } of listed) {
|
|
7368
|
+
if (state2.pending !== true) continue;
|
|
7369
|
+
if (sessions.has(sessionID)) continue;
|
|
7370
|
+
if (scheduled >= 50) continue;
|
|
7371
|
+
const loaded = stateFor(sessionID);
|
|
7372
|
+
if (loaded.root === false) continue;
|
|
7373
|
+
schedule(sessionID, loaded);
|
|
7374
|
+
scheduled++;
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
resume();
|
|
7414
7378
|
const timelineReprocess = tool2({
|
|
7415
7379
|
description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
7416
7380
|
args: {},
|
|
@@ -7460,6 +7424,7 @@ function timelineHooks(input, options) {
|
|
|
7460
7424
|
}
|
|
7461
7425
|
sessions.delete(id);
|
|
7462
7426
|
removeEntries(id);
|
|
7427
|
+
removeTimelineState(id);
|
|
7463
7428
|
owned.delete(id);
|
|
7464
7429
|
return;
|
|
7465
7430
|
}
|