oh-my-opencode-slim 1.0.2 → 1.0.4
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/README.md +1 -2
- package/dist/cli/config-io.d.ts +1 -0
- package/dist/cli/index.js +38 -2
- package/dist/cli/install.d.ts +1 -1
- package/dist/config/constants.d.ts +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +4 -0
- package/dist/hooks/apply-patch/matching.d.ts +9 -1
- package/dist/hooks/apply-patch/rewrite.d.ts +0 -3
- package/dist/hooks/phase-reminder/index.d.ts +1 -1
- package/dist/hooks/post-file-tool-nudge/index.d.ts +3 -19
- package/dist/hooks/task-session-manager/index.d.ts +20 -4
- package/dist/hooks/todo-continuation/index.d.ts +2 -5
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +2 -8
- package/dist/index.js +909 -377
- package/dist/multiplexer/session-manager.d.ts +3 -0
- package/dist/utils/agent-variant.d.ts +2 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/session-manager.d.ts +18 -1
- package/oh-my-opencode-slim.schema.json +12 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6246
6246
|
else
|
|
6247
6247
|
return basepath.substring(0, lastslash + 1) + refpath;
|
|
6248
6248
|
}
|
|
6249
|
-
function remove_dot_segments(
|
|
6250
|
-
if (!
|
|
6251
|
-
return
|
|
6249
|
+
function remove_dot_segments(path14) {
|
|
6250
|
+
if (!path14)
|
|
6251
|
+
return path14;
|
|
6252
6252
|
var output = "";
|
|
6253
|
-
while (
|
|
6254
|
-
if (
|
|
6255
|
-
|
|
6253
|
+
while (path14.length > 0) {
|
|
6254
|
+
if (path14 === "." || path14 === "..") {
|
|
6255
|
+
path14 = "";
|
|
6256
6256
|
break;
|
|
6257
6257
|
}
|
|
6258
|
-
var twochars =
|
|
6259
|
-
var threechars =
|
|
6260
|
-
var fourchars =
|
|
6258
|
+
var twochars = path14.substring(0, 2);
|
|
6259
|
+
var threechars = path14.substring(0, 3);
|
|
6260
|
+
var fourchars = path14.substring(0, 4);
|
|
6261
6261
|
if (threechars === "../") {
|
|
6262
|
-
|
|
6262
|
+
path14 = path14.substring(3);
|
|
6263
6263
|
} else if (twochars === "./") {
|
|
6264
|
-
|
|
6264
|
+
path14 = path14.substring(2);
|
|
6265
6265
|
} else if (threechars === "/./") {
|
|
6266
|
-
|
|
6267
|
-
} else if (twochars === "/." &&
|
|
6268
|
-
|
|
6269
|
-
} else if (fourchars === "/../" || threechars === "/.." &&
|
|
6270
|
-
|
|
6266
|
+
path14 = "/" + path14.substring(3);
|
|
6267
|
+
} else if (twochars === "/." && path14.length === 2) {
|
|
6268
|
+
path14 = "/";
|
|
6269
|
+
} else if (fourchars === "/../" || threechars === "/.." && path14.length === 3) {
|
|
6270
|
+
path14 = "/" + path14.substring(4);
|
|
6271
6271
|
output = output.replace(/\/?[^\/]*$/, "");
|
|
6272
6272
|
} else {
|
|
6273
|
-
var segment =
|
|
6273
|
+
var segment = path14.match(/(\/?([^\/]*))/)[0];
|
|
6274
6274
|
output += segment;
|
|
6275
|
-
|
|
6275
|
+
path14 = path14.substring(segment.length);
|
|
6276
6276
|
}
|
|
6277
6277
|
}
|
|
6278
6278
|
return output;
|
|
@@ -18316,7 +18316,7 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
18316
18316
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
18317
18317
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
18318
18318
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Recall the workflow rules:
|
|
18319
|
-
Understand → choose the best parallelized path based on your capabilities and agents delegation rules → execute → verify.
|
|
18319
|
+
Understand → choose the best parallelized path based on your capabilities and agents delegation rules → recall session reuse rules → execute → verify.
|
|
18320
18320
|
If delegating, launch the specialist in the same turn you mention it !END!`;
|
|
18321
18321
|
var TMUX_SPAWN_DELAY_MS = 500;
|
|
18322
18322
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
@@ -18526,7 +18526,9 @@ var InterviewConfigSchema = z2.object({
|
|
|
18526
18526
|
dashboard: z2.boolean().default(false)
|
|
18527
18527
|
});
|
|
18528
18528
|
var SessionManagerConfigSchema = z2.object({
|
|
18529
|
-
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
|
|
18529
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
18530
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
18531
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
18530
18532
|
});
|
|
18531
18533
|
var TodoContinuationConfigSchema = z2.object({
|
|
18532
18534
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
@@ -18852,14 +18854,15 @@ var AGENT_DESCRIPTIONS = {
|
|
|
18852
18854
|
- **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Sequential dependencies
|
|
18853
18855
|
- **Rule of thumb:** Explaining > doing? → yourself. Test file modifications and bounded implementation work usually go to @fixer. Bigger or lots of edits, splitting makes sense, parallelized by spawning @fixers per certain scope.`,
|
|
18854
18856
|
council: `@council
|
|
18855
|
-
- Role: Multi-LLM consensus engine
|
|
18857
|
+
- Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
|
|
18856
18858
|
- Permissions: Read files
|
|
18857
18859
|
- Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
|
|
18858
|
-
- Capabilities: Runs multiple models in parallel,
|
|
18859
|
-
- **Delegate when:** Critical decisions
|
|
18860
|
-
- **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence •
|
|
18861
|
-
- **
|
|
18862
|
-
- **
|
|
18860
|
+
- Capabilities: Runs multiple models in parallel, compares their answers, resolves disagreements, and produces a final synthesized answer plus councillor details and consensus summary.
|
|
18861
|
+
- **Delegate when:** Critical decisions need multiple independent perspectives • High-stakes architectural/security/data-integrity choices • Ambiguous problems where disagreement is useful signal • You want confidence beyond a single model • The user explicitly asks for council/consensus/multiple opinions.
|
|
18862
|
+
- **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Routine implementation/debugging • A single specialist is clearly the right tool • You only need current docs/search/code review rather than multi-model consensus.
|
|
18863
|
+
- **How to call:** Send the full question/task and relevant context. Be explicit about what decision, trade-off, or answer the council should resolve. Do not ask council to do routine code edits.
|
|
18864
|
+
- **Result handling:** Council returns a structured response that may include: synthesized Council Response, individual Councillor Details, and Council Summary/confidence. Preserve that structure when the user asked for council output. Do not pretend the council only returned a final answer. If you need to act on the council result, first briefly state the council's recommendation, then proceed.
|
|
18865
|
+
- **Rule of thumb:** Need second/third opinions from different models? → @council. Need one expert agent or direct execution? → use the specialist or yourself.`,
|
|
18863
18866
|
observer: `@observer
|
|
18864
18867
|
- Role: Visual analysis specialist for images, PDFs, and diagrams
|
|
18865
18868
|
- Permissions: Read files
|
|
@@ -18951,10 +18954,10 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
|
18951
18954
|
5. Adjust if needed
|
|
18952
18955
|
|
|
18953
18956
|
### Session Reuse
|
|
18954
|
-
-
|
|
18955
|
-
-
|
|
18957
|
+
- Smartly reuse an available specialist session - constext reuse saves time and tokens
|
|
18958
|
+
- When too much unrelated, and really needed, start a fresh session with the specialist
|
|
18956
18959
|
- If multiple remembered sessions fit, prefer the most recently used matching session.
|
|
18957
|
-
-
|
|
18960
|
+
- Prefer re-uses over creating new sessions all the time
|
|
18958
18961
|
|
|
18959
18962
|
### Auto-Continue
|
|
18960
18963
|
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
@@ -19042,27 +19045,47 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
|
|
|
19042
19045
|
1. Call the \`council_session\` tool with the user's prompt
|
|
19043
19046
|
2. Optionally specify a preset (default: "default")
|
|
19044
19047
|
3. Receive the councillor responses formatted for synthesis
|
|
19045
|
-
4.
|
|
19046
|
-
5. Present the
|
|
19047
|
-
|
|
19048
|
-
**Synthesis
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
- If councillors disagree, explain the resolution and your reasoning
|
|
19056
|
-
- Acknowledge if consensus was impossible and explain why
|
|
19057
|
-
- Don't just average responses — choose the best approach and improve upon it
|
|
19058
|
-
- Present the synthesized solution with relevant code examples, concrete details, and clear explanations
|
|
19048
|
+
4. Follow the Synthesis Process below
|
|
19049
|
+
5. Present the result to the user
|
|
19050
|
+
|
|
19051
|
+
**Synthesis Process** (MANDATORY — follow in order):
|
|
19052
|
+
1. Read the original user prompt
|
|
19053
|
+
2. Review each councillor's response individually — note each councillor's key insight and unique contribution by name
|
|
19054
|
+
3. Identify agreements and contradictions between councillors
|
|
19055
|
+
4. Resolve contradictions with explicit reasoning
|
|
19056
|
+
5. Synthesize the optimal final answer
|
|
19057
|
+
6. Format output per the Required Output Format below
|
|
19059
19058
|
|
|
19060
19059
|
**Behavior**:
|
|
19061
19060
|
- Delegate requests directly to council_session
|
|
19062
19061
|
- Don't pre-analyze or filter the prompt before calling council_session
|
|
19063
|
-
-
|
|
19064
|
-
-
|
|
19065
|
-
-
|
|
19062
|
+
- Credit specific insights from individual councillors using their names
|
|
19063
|
+
- If councillors disagree, explain why you chose one approach over another
|
|
19064
|
+
- Do not omit per-councillor details from the final response
|
|
19065
|
+
- Do not collapse the output into only a final summary
|
|
19066
|
+
- Be transparent about trade-offs when different approaches have valid pros/cons
|
|
19067
|
+
- Don't just average responses — choose the best approach and improve upon it
|
|
19068
|
+
|
|
19069
|
+
**Required Output Format**:
|
|
19070
|
+
Always include these sections in your final response:
|
|
19071
|
+
|
|
19072
|
+
## Council Response
|
|
19073
|
+
Provide the best synthesized answer. Integrate the strongest points from the councillors, resolve disagreements, and give the user a clear final recommendation or answer. Include relevant code examples and concrete details.
|
|
19074
|
+
|
|
19075
|
+
## Councillor Details
|
|
19076
|
+
Include each councillor's response separately.
|
|
19077
|
+
|
|
19078
|
+
Use each councillor name exactly as provided in the tool result.
|
|
19079
|
+
|
|
19080
|
+
Format each councillor like:
|
|
19081
|
+
|
|
19082
|
+
### <councillor name>
|
|
19083
|
+
<that councillor's response>
|
|
19084
|
+
|
|
19085
|
+
If a councillor failed or timed out, include that status briefly.
|
|
19086
|
+
|
|
19087
|
+
## Council Summary
|
|
19088
|
+
Summarize where councillors agreed, where they disagreed, why you chose the final answer, and any remaining uncertainty. Include a consensus confidence rating: unanimous, majority, or split.`;
|
|
19066
19089
|
function createCouncilAgent(model, customPrompt, customAppendPrompt) {
|
|
19067
19090
|
const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
|
|
19068
19091
|
const definition = {
|
|
@@ -19135,7 +19158,7 @@ ${failedSection}`;
|
|
|
19135
19158
|
|
|
19136
19159
|
---
|
|
19137
19160
|
|
|
19138
|
-
|
|
19161
|
+
You MUST follow the Synthesis Process steps before producing output: review each councillor response individually, then produce the required output with a synthesized Council Response, per-councillor details using their exact names, and a Council Summary with consensus confidence rating (unanimous, majority, or split).`;
|
|
19139
19162
|
return prompt;
|
|
19140
19163
|
}
|
|
19141
19164
|
|
|
@@ -19767,14 +19790,37 @@ function getDisabledAgents(config) {
|
|
|
19767
19790
|
return disabled;
|
|
19768
19791
|
}
|
|
19769
19792
|
|
|
19793
|
+
// src/config/runtime-preset.ts
|
|
19794
|
+
var activeRuntimePreset = null;
|
|
19795
|
+
function setActiveRuntimePreset(name) {
|
|
19796
|
+
activeRuntimePreset = name;
|
|
19797
|
+
}
|
|
19798
|
+
function getActiveRuntimePreset() {
|
|
19799
|
+
return activeRuntimePreset;
|
|
19800
|
+
}
|
|
19801
|
+
var previousRuntimePreset = null;
|
|
19802
|
+
function getPreviousRuntimePreset() {
|
|
19803
|
+
return previousRuntimePreset;
|
|
19804
|
+
}
|
|
19805
|
+
function setActiveRuntimePresetWithPrevious(name) {
|
|
19806
|
+
previousRuntimePreset = activeRuntimePreset;
|
|
19807
|
+
activeRuntimePreset = name;
|
|
19808
|
+
}
|
|
19809
|
+
function rollbackRuntimePreset(previous) {
|
|
19810
|
+
activeRuntimePreset = previous;
|
|
19811
|
+
previousRuntimePreset = null;
|
|
19812
|
+
}
|
|
19813
|
+
|
|
19770
19814
|
// src/utils/logger.ts
|
|
19771
19815
|
import * as fs2 from "node:fs";
|
|
19816
|
+
import { appendFile } from "node:fs/promises";
|
|
19772
19817
|
import * as os from "node:os";
|
|
19773
19818
|
import * as path2 from "node:path";
|
|
19774
19819
|
var LOG_PREFIX = "oh-my-opencode-slim.";
|
|
19775
19820
|
var LOG_SUFFIX = ".log";
|
|
19776
19821
|
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19777
19822
|
var logFile = null;
|
|
19823
|
+
var writeChain = Promise.resolve();
|
|
19778
19824
|
function getLogDir() {
|
|
19779
19825
|
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
|
|
19780
19826
|
}
|
|
@@ -19817,10 +19863,14 @@ function initLogger(sessionId) {
|
|
|
19817
19863
|
fs2.mkdirSync(dir, { recursive: true });
|
|
19818
19864
|
} catch {}
|
|
19819
19865
|
logFile = path2.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
|
|
19866
|
+
try {
|
|
19867
|
+
fs2.closeSync(fs2.openSync(logFile, "a"));
|
|
19868
|
+
} catch {}
|
|
19820
19869
|
cleanupOldLogs(dir);
|
|
19821
19870
|
}
|
|
19822
19871
|
function log(message, data) {
|
|
19823
|
-
|
|
19872
|
+
const target = logFile;
|
|
19873
|
+
if (!target)
|
|
19824
19874
|
return;
|
|
19825
19875
|
try {
|
|
19826
19876
|
const timestamp = new Date().toISOString();
|
|
@@ -19834,7 +19884,7 @@ function log(message, data) {
|
|
|
19834
19884
|
}
|
|
19835
19885
|
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
19836
19886
|
`;
|
|
19837
|
-
|
|
19887
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
19838
19888
|
} catch {}
|
|
19839
19889
|
}
|
|
19840
19890
|
|
|
@@ -20020,7 +20070,7 @@ class CouncilManager {
|
|
|
20020
20070
|
}
|
|
20021
20071
|
} else {
|
|
20022
20072
|
const promises = entries.map(([name, config], index) => (async () => {
|
|
20023
|
-
if (index > 0) {
|
|
20073
|
+
if (this.tmuxEnabled && index > 0) {
|
|
20024
20074
|
await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
|
|
20025
20075
|
}
|
|
20026
20076
|
return this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
|
|
@@ -20206,6 +20256,16 @@ function unexpectedPatchLine(context, line) {
|
|
|
20206
20256
|
const rendered = line.length === 0 ? "<empty>" : line;
|
|
20207
20257
|
throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
|
|
20208
20258
|
}
|
|
20259
|
+
function parseChangeContext(line) {
|
|
20260
|
+
const context = line.slice(2);
|
|
20261
|
+
if (context.length === 0) {
|
|
20262
|
+
return;
|
|
20263
|
+
}
|
|
20264
|
+
return context.startsWith(" ") ? context.slice(1) || undefined : context;
|
|
20265
|
+
}
|
|
20266
|
+
function isPatchBoundary(line, marker) {
|
|
20267
|
+
return line.trimEnd() === marker;
|
|
20268
|
+
}
|
|
20209
20269
|
function parseChunks(lines, index, mode) {
|
|
20210
20270
|
const chunks = [];
|
|
20211
20271
|
let at = index;
|
|
@@ -20217,7 +20277,7 @@ function parseChunks(lines, index, mode) {
|
|
|
20217
20277
|
at += 1;
|
|
20218
20278
|
continue;
|
|
20219
20279
|
}
|
|
20220
|
-
const context = lines[at]
|
|
20280
|
+
const context = parseChangeContext(lines[at]);
|
|
20221
20281
|
at += 1;
|
|
20222
20282
|
const old_lines = [];
|
|
20223
20283
|
const new_lines = [];
|
|
@@ -20260,12 +20320,11 @@ function parseChunks(lines, index, mode) {
|
|
|
20260
20320
|
return { chunks, next: at };
|
|
20261
20321
|
}
|
|
20262
20322
|
function parseAdd(lines, index, mode) {
|
|
20263
|
-
|
|
20323
|
+
const contents = [];
|
|
20264
20324
|
let at = index;
|
|
20265
20325
|
while (at < lines.length && !lines[at].startsWith("***")) {
|
|
20266
20326
|
if (lines[at].startsWith("+")) {
|
|
20267
|
-
contents
|
|
20268
|
-
`;
|
|
20327
|
+
contents.push(lines[at].slice(1));
|
|
20269
20328
|
at += 1;
|
|
20270
20329
|
continue;
|
|
20271
20330
|
}
|
|
@@ -20274,18 +20333,15 @@ function parseAdd(lines, index, mode) {
|
|
|
20274
20333
|
}
|
|
20275
20334
|
at += 1;
|
|
20276
20335
|
}
|
|
20277
|
-
|
|
20278
|
-
`)
|
|
20279
|
-
contents = contents.slice(0, -1);
|
|
20280
|
-
}
|
|
20281
|
-
return { content: contents, next: at };
|
|
20336
|
+
return { content: contents.join(`
|
|
20337
|
+
`), next: at };
|
|
20282
20338
|
}
|
|
20283
20339
|
function parsePatchInternal(patchText, mode) {
|
|
20284
20340
|
const clean = normalizePatchText(patchText);
|
|
20285
20341
|
const lines = clean.split(`
|
|
20286
20342
|
`);
|
|
20287
|
-
const begin = lines.findIndex((line) => line
|
|
20288
|
-
const end = lines.findIndex((line) => line
|
|
20343
|
+
const begin = lines.findIndex((line) => isPatchBoundary(line, "*** Begin Patch"));
|
|
20344
|
+
const end = lines.findIndex((line, index2) => index2 > begin && isPatchBoundary(line, "*** End Patch"));
|
|
20289
20345
|
if (begin === -1 || end === -1 || begin >= end) {
|
|
20290
20346
|
throw new Error("Invalid patch format: missing Begin/End markers");
|
|
20291
20347
|
}
|
|
@@ -20417,12 +20473,6 @@ function formatPatch(patch) {
|
|
|
20417
20473
|
}
|
|
20418
20474
|
|
|
20419
20475
|
// src/hooks/apply-patch/matching.ts
|
|
20420
|
-
var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
|
|
20421
|
-
"exact",
|
|
20422
|
-
"unicode",
|
|
20423
|
-
"trim-end",
|
|
20424
|
-
"unicode-trim-end"
|
|
20425
|
-
]);
|
|
20426
20476
|
function equalExact(a, b) {
|
|
20427
20477
|
return a === b;
|
|
20428
20478
|
}
|
|
@@ -20441,7 +20491,7 @@ function equalTrim(a, b) {
|
|
|
20441
20491
|
function equalUnicodeTrim(a, b) {
|
|
20442
20492
|
return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
|
|
20443
20493
|
}
|
|
20444
|
-
var
|
|
20494
|
+
var autoRescueComparatorEntries = [
|
|
20445
20495
|
{ name: "exact", exact: true, same: equalExact },
|
|
20446
20496
|
{ name: "unicode", exact: false, same: equalUnicodeExact },
|
|
20447
20497
|
{ name: "trim-end", exact: false, same: equalTrimEnd },
|
|
@@ -20449,14 +20499,44 @@ var comparatorEntries = [
|
|
|
20449
20499
|
name: "unicode-trim-end",
|
|
20450
20500
|
exact: false,
|
|
20451
20501
|
same: equalUnicodeTrimEnd
|
|
20452
|
-
}
|
|
20502
|
+
}
|
|
20503
|
+
];
|
|
20504
|
+
var comparatorEntries = [
|
|
20505
|
+
...autoRescueComparatorEntries,
|
|
20453
20506
|
{ name: "trim", exact: false, same: equalTrim },
|
|
20454
20507
|
{ name: "unicode-trim", exact: false, same: equalUnicodeTrim }
|
|
20455
20508
|
];
|
|
20456
|
-
var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
|
|
20457
20509
|
var MAX_LCS_CHUNK_LINES = 48;
|
|
20458
20510
|
var MAX_LCS_CANDIDATES = 64;
|
|
20459
20511
|
var autoRescueComparators = autoRescueComparatorEntries.map((entry) => entry.same);
|
|
20512
|
+
function prepareAutoRescueTarget(target) {
|
|
20513
|
+
const trimEnd = target.trimEnd();
|
|
20514
|
+
const unicode = normalizeUnicode(target);
|
|
20515
|
+
return {
|
|
20516
|
+
exact: target,
|
|
20517
|
+
unicode,
|
|
20518
|
+
trimEnd,
|
|
20519
|
+
unicodeTrimEnd: trimEnd === target ? unicode : normalizeUnicode(trimEnd)
|
|
20520
|
+
};
|
|
20521
|
+
}
|
|
20522
|
+
function matchPreparedAutoRescueComparator(candidate, target) {
|
|
20523
|
+
if (candidate === target.exact) {
|
|
20524
|
+
return "exact";
|
|
20525
|
+
}
|
|
20526
|
+
const unicode = normalizeUnicode(candidate);
|
|
20527
|
+
if (unicode === target.unicode) {
|
|
20528
|
+
return "unicode";
|
|
20529
|
+
}
|
|
20530
|
+
const trimEnd = candidate.trimEnd();
|
|
20531
|
+
if (trimEnd === target.trimEnd) {
|
|
20532
|
+
return "trim-end";
|
|
20533
|
+
}
|
|
20534
|
+
const unicodeTrimEnd = trimEnd === candidate ? unicode : normalizeUnicode(trimEnd);
|
|
20535
|
+
if (unicodeTrimEnd === target.unicodeTrimEnd) {
|
|
20536
|
+
return "unicode-trim-end";
|
|
20537
|
+
}
|
|
20538
|
+
return;
|
|
20539
|
+
}
|
|
20460
20540
|
var permissiveComparators = comparatorEntries.map((entry) => entry.same);
|
|
20461
20541
|
function tryMatch(lines, pattern, start, comparator, eof) {
|
|
20462
20542
|
if (eof) {
|
|
@@ -20530,6 +20610,19 @@ function list(lines, pattern, start, same) {
|
|
|
20530
20610
|
}
|
|
20531
20611
|
return out;
|
|
20532
20612
|
}
|
|
20613
|
+
function lowerBound(values, target) {
|
|
20614
|
+
let low = 0;
|
|
20615
|
+
let high = values.length;
|
|
20616
|
+
while (low < high) {
|
|
20617
|
+
const middle = Math.floor((low + high) / 2);
|
|
20618
|
+
if (values[middle] < target) {
|
|
20619
|
+
low = middle + 1;
|
|
20620
|
+
continue;
|
|
20621
|
+
}
|
|
20622
|
+
high = middle;
|
|
20623
|
+
}
|
|
20624
|
+
return low;
|
|
20625
|
+
}
|
|
20533
20626
|
function sameRescueLine(a, b) {
|
|
20534
20627
|
return equalExact(a, b) || equalUnicodeExact(a, b);
|
|
20535
20628
|
}
|
|
@@ -20556,36 +20649,102 @@ function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
|
|
|
20556
20649
|
const left = old_lines.slice(0, prefixLength);
|
|
20557
20650
|
const right = old_lines.slice(old_lines.length - suffixLength);
|
|
20558
20651
|
const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
|
|
20559
|
-
|
|
20652
|
+
if (left.length === 1 && right.length === 1) {
|
|
20653
|
+
const { leftHits, rightHits } = collectOneLinePrefixSuffixHits(lines, left[0], right[0], start);
|
|
20654
|
+
return resolvePrefixSuffixHits(leftHits, rightHits, left.length, middle);
|
|
20655
|
+
}
|
|
20656
|
+
const hits = new Set;
|
|
20657
|
+
let hit;
|
|
20560
20658
|
for (const same of autoRescueComparators) {
|
|
20561
|
-
|
|
20659
|
+
const leftHits = list(lines, left, start, same);
|
|
20660
|
+
if (leftHits.length === 0) {
|
|
20661
|
+
continue;
|
|
20662
|
+
}
|
|
20663
|
+
const rightHits = list(lines, right, leftHits[0] + left.length, same);
|
|
20664
|
+
if (rightHits.length === 0) {
|
|
20665
|
+
continue;
|
|
20666
|
+
}
|
|
20667
|
+
for (const leftIndex of leftHits) {
|
|
20562
20668
|
const from = leftIndex + left.length;
|
|
20563
|
-
for (
|
|
20669
|
+
for (let index = lowerBound(rightHits, from);index < rightHits.length; index += 1) {
|
|
20670
|
+
const rightIndex = rightHits[index];
|
|
20564
20671
|
const key = `${from}:${rightIndex}`;
|
|
20565
|
-
hits.
|
|
20672
|
+
if (!hits.has(key)) {
|
|
20673
|
+
hits.add(key);
|
|
20674
|
+
hit = {
|
|
20675
|
+
start: from,
|
|
20676
|
+
del: rightIndex - from,
|
|
20677
|
+
add: [...middle]
|
|
20678
|
+
};
|
|
20679
|
+
}
|
|
20680
|
+
if (hits.size > 1) {
|
|
20681
|
+
return { kind: "ambiguous", phase: "prefix_suffix" };
|
|
20682
|
+
}
|
|
20683
|
+
}
|
|
20684
|
+
}
|
|
20685
|
+
}
|
|
20686
|
+
if (!hit) {
|
|
20687
|
+
return { kind: "miss" };
|
|
20688
|
+
}
|
|
20689
|
+
return { kind: "match", hit };
|
|
20690
|
+
}
|
|
20691
|
+
function collectOneLinePrefixSuffixHits(lines, left, right, start) {
|
|
20692
|
+
const leftTarget = prepareAutoRescueTarget(left);
|
|
20693
|
+
const rightTarget = prepareAutoRescueTarget(right);
|
|
20694
|
+
const leftHits = [];
|
|
20695
|
+
const rightHits = [];
|
|
20696
|
+
for (let index = start;index < lines.length; index += 1) {
|
|
20697
|
+
const line = prepareAutoRescueTarget(lines[index]);
|
|
20698
|
+
if (line.unicodeTrimEnd === leftTarget.unicodeTrimEnd) {
|
|
20699
|
+
leftHits.push(index);
|
|
20700
|
+
}
|
|
20701
|
+
if (index > start && line.unicodeTrimEnd === rightTarget.unicodeTrimEnd) {
|
|
20702
|
+
rightHits.push(index);
|
|
20703
|
+
}
|
|
20704
|
+
}
|
|
20705
|
+
return { leftHits, rightHits };
|
|
20706
|
+
}
|
|
20707
|
+
function resolvePrefixSuffixHits(leftHits, rightHits, leftLength, middle) {
|
|
20708
|
+
if (leftHits.length === 0 || rightHits.length === 0) {
|
|
20709
|
+
return { kind: "miss" };
|
|
20710
|
+
}
|
|
20711
|
+
const hits = new Set;
|
|
20712
|
+
let hit;
|
|
20713
|
+
for (const leftIndex of leftHits) {
|
|
20714
|
+
const from = leftIndex + leftLength;
|
|
20715
|
+
for (let index = lowerBound(rightHits, from);index < rightHits.length; index += 1) {
|
|
20716
|
+
const rightIndex = rightHits[index];
|
|
20717
|
+
const key = `${from}:${rightIndex}`;
|
|
20718
|
+
if (!hits.has(key)) {
|
|
20719
|
+
hits.add(key);
|
|
20720
|
+
hit = {
|
|
20566
20721
|
start: from,
|
|
20567
20722
|
del: rightIndex - from,
|
|
20568
20723
|
add: [...middle]
|
|
20569
|
-
}
|
|
20724
|
+
};
|
|
20725
|
+
}
|
|
20726
|
+
if (hits.size > 1) {
|
|
20727
|
+
return { kind: "ambiguous", phase: "prefix_suffix" };
|
|
20570
20728
|
}
|
|
20571
20729
|
}
|
|
20572
20730
|
}
|
|
20573
|
-
if (
|
|
20731
|
+
if (!hit) {
|
|
20574
20732
|
return { kind: "miss" };
|
|
20575
20733
|
}
|
|
20576
|
-
|
|
20577
|
-
return { kind: "ambiguous", phase: "prefix_suffix" };
|
|
20578
|
-
}
|
|
20579
|
-
return { kind: "match", hit: [...hits.values()][0] };
|
|
20734
|
+
return { kind: "match", hit };
|
|
20580
20735
|
}
|
|
20581
20736
|
function score(a, b) {
|
|
20582
|
-
const
|
|
20737
|
+
const normalizedA = a.map(normalizeLcsLine);
|
|
20738
|
+
const normalizedB = b.map(normalizeLcsLine);
|
|
20739
|
+
let previous = Array(b.length + 1).fill(0);
|
|
20583
20740
|
for (let i = 1;i <= a.length; i += 1) {
|
|
20741
|
+
const current = Array(b.length + 1).fill(0);
|
|
20584
20742
|
for (let j = 1;j <= b.length; j += 1) {
|
|
20585
|
-
|
|
20743
|
+
current[j] = normalizedA[i - 1] === normalizedB[j - 1] ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]);
|
|
20586
20744
|
}
|
|
20745
|
+
previous = current;
|
|
20587
20746
|
}
|
|
20588
|
-
return
|
|
20747
|
+
return previous[b.length];
|
|
20589
20748
|
}
|
|
20590
20749
|
function normalizeLcsLine(line) {
|
|
20591
20750
|
return normalizeUnicode(line).trim();
|
|
@@ -20612,45 +20771,23 @@ function countLcsUpperBound(a, b) {
|
|
|
20612
20771
|
}
|
|
20613
20772
|
return shared;
|
|
20614
20773
|
}
|
|
20615
|
-
function hasStableBorders(oldLines, candidate) {
|
|
20616
|
-
if (oldLines.length === 0 || candidate.length !== oldLines.length) {
|
|
20617
|
-
return false;
|
|
20618
|
-
}
|
|
20619
|
-
const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
|
|
20620
|
-
if (!same) {
|
|
20621
|
-
return false;
|
|
20622
|
-
}
|
|
20623
|
-
if (oldLines.length === 1) {
|
|
20624
|
-
return true;
|
|
20625
|
-
}
|
|
20626
|
-
return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
|
|
20627
|
-
}
|
|
20628
20774
|
function collectBorderAnchoredStarts(lines, oldLines, start) {
|
|
20629
20775
|
if (oldLines.length === 0) {
|
|
20630
20776
|
return [];
|
|
20631
20777
|
}
|
|
20632
|
-
const firstHits = new Set;
|
|
20633
|
-
const lastHits = new Set;
|
|
20634
|
-
const lastLine = oldLines[oldLines.length - 1];
|
|
20635
|
-
for (const same of autoRescueComparators) {
|
|
20636
|
-
for (const index of list(lines, [oldLines[0]], start, same)) {
|
|
20637
|
-
firstHits.add(index);
|
|
20638
|
-
}
|
|
20639
|
-
for (const index of list(lines, [lastLine], start, same)) {
|
|
20640
|
-
lastHits.add(index);
|
|
20641
|
-
}
|
|
20642
|
-
}
|
|
20643
20778
|
const candidates = [];
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20779
|
+
const firstLine = prepareAutoRescueTarget(oldLines[0]);
|
|
20780
|
+
const lastLine = prepareAutoRescueTarget(oldLines[oldLines.length - 1]);
|
|
20781
|
+
const lastOffset = oldLines.length - 1;
|
|
20782
|
+
const maxStart = lines.length - oldLines.length;
|
|
20783
|
+
for (let index = start;index <= maxStart; index += 1) {
|
|
20784
|
+
const end = index + lastOffset;
|
|
20785
|
+
if (matchPreparedAutoRescueComparator(lines[index], firstLine) === undefined) {
|
|
20647
20786
|
continue;
|
|
20648
20787
|
}
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
continue;
|
|
20788
|
+
if (oldLines.length === 1 || matchPreparedAutoRescueComparator(lines[end], lastLine) !== undefined) {
|
|
20789
|
+
candidates.push(index);
|
|
20652
20790
|
}
|
|
20653
|
-
candidates.push(index);
|
|
20654
20791
|
}
|
|
20655
20792
|
return candidates;
|
|
20656
20793
|
}
|
|
@@ -20658,11 +20795,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
|
|
|
20658
20795
|
if (old_lines.length === 0 || lines.length === 0) {
|
|
20659
20796
|
return { kind: "miss" };
|
|
20660
20797
|
}
|
|
20661
|
-
const from = start;
|
|
20662
|
-
const to = lines.length - old_lines.length;
|
|
20663
|
-
if (to < from) {
|
|
20664
|
-
return { kind: "miss" };
|
|
20665
|
-
}
|
|
20666
20798
|
if (old_lines.length > MAX_LCS_CHUNK_LINES) {
|
|
20667
20799
|
return { kind: "miss" };
|
|
20668
20800
|
}
|
|
@@ -20675,9 +20807,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
|
|
|
20675
20807
|
let bestScore = 0;
|
|
20676
20808
|
let ties = 0;
|
|
20677
20809
|
for (const index of candidates) {
|
|
20678
|
-
if (index < from || index > to) {
|
|
20679
|
-
continue;
|
|
20680
|
-
}
|
|
20681
20810
|
const window2 = lines.slice(index, index + old_lines.length);
|
|
20682
20811
|
if (countLcsUpperBound(old_lines, window2) < needed) {
|
|
20683
20812
|
continue;
|
|
@@ -20732,26 +20861,29 @@ function resolveChunkStart(lines, chunk, start) {
|
|
|
20732
20861
|
return at === -1 ? start : at + 1;
|
|
20733
20862
|
}
|
|
20734
20863
|
function resolveUniqueAnchor(lines, changeContext, start) {
|
|
20735
|
-
|
|
20736
|
-
|
|
20737
|
-
|
|
20738
|
-
|
|
20864
|
+
let matchedIndex;
|
|
20865
|
+
let matchedComparator;
|
|
20866
|
+
const anchorTarget = prepareAutoRescueTarget(changeContext);
|
|
20867
|
+
for (let index = start;index < lines.length; index += 1) {
|
|
20868
|
+
const comparator = matchPreparedAutoRescueComparator(lines[index], anchorTarget);
|
|
20869
|
+
if (!comparator) {
|
|
20870
|
+
continue;
|
|
20871
|
+
}
|
|
20872
|
+
if (matchedIndex !== undefined) {
|
|
20873
|
+
return { kind: "ambiguous" };
|
|
20739
20874
|
}
|
|
20875
|
+
matchedIndex = index;
|
|
20876
|
+
matchedComparator = comparator;
|
|
20740
20877
|
}
|
|
20741
|
-
if (
|
|
20878
|
+
if (matchedIndex === undefined) {
|
|
20742
20879
|
return { kind: "missing" };
|
|
20743
20880
|
}
|
|
20744
|
-
|
|
20745
|
-
return { kind: "ambiguous" };
|
|
20746
|
-
}
|
|
20747
|
-
const index = [...hits][0];
|
|
20748
|
-
const canonicalLine = lines[index];
|
|
20749
|
-
const comparator = seekMatch(lines, [changeContext], index)?.comparator;
|
|
20881
|
+
const canonicalLine = lines[matchedIndex];
|
|
20750
20882
|
return {
|
|
20751
20883
|
kind: "match",
|
|
20752
|
-
index,
|
|
20884
|
+
index: matchedIndex,
|
|
20753
20885
|
exact: canonicalLine === changeContext,
|
|
20754
|
-
comparator:
|
|
20886
|
+
comparator: matchedComparator ?? "exact",
|
|
20755
20887
|
canonicalLine
|
|
20756
20888
|
};
|
|
20757
20889
|
}
|
|
@@ -20902,10 +21034,11 @@ ${chunk.change_context}`);
|
|
|
20902
21034
|
old_lines: [],
|
|
20903
21035
|
canonical_old_lines: [anchor],
|
|
20904
21036
|
canonical_new_lines: [...chunk.new_lines, anchor],
|
|
21037
|
+
canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
|
|
20905
21038
|
resolved_is_end_of_file: insertAt + 1 === lines.length,
|
|
20906
21039
|
rewritten: true,
|
|
20907
21040
|
strategy,
|
|
20908
|
-
matchComparator:
|
|
21041
|
+
matchComparator: anchorMatch.comparator
|
|
20909
21042
|
});
|
|
20910
21043
|
start = insertAt;
|
|
20911
21044
|
continue;
|
|
@@ -21279,15 +21412,6 @@ function renderRewriteDependencyGroup(group, cfg) {
|
|
|
21279
21412
|
}
|
|
21280
21413
|
return group.group.chunks ? createUpdateHunk(group.group.sourcePath, group.group.chunks, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined) : createCollapsedUpdateHunk(group.group.sourcePath, group.group.sourceFilePath, group.group.baseText, group.group.finalText, cfg, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined);
|
|
21281
21414
|
}
|
|
21282
|
-
function rewriteModeForDependentUpdate(group) {
|
|
21283
|
-
if (group.kind === "add") {
|
|
21284
|
-
return "collapse:add-followed-by-update";
|
|
21285
|
-
}
|
|
21286
|
-
if (group.group.outputPath !== group.group.sourcePath) {
|
|
21287
|
-
return "collapse:move-followed-by-update";
|
|
21288
|
-
}
|
|
21289
|
-
return "merge:same-file-updates";
|
|
21290
|
-
}
|
|
21291
21415
|
function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
|
|
21292
21416
|
if (group.kind === "add") {
|
|
21293
21417
|
return {
|
|
@@ -21327,12 +21451,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21327
21451
|
const normalizedPatchText = normalizePatchText(patchText);
|
|
21328
21452
|
const rewritten = [];
|
|
21329
21453
|
let changed = false;
|
|
21330
|
-
let rewrittenChunks = 0;
|
|
21331
|
-
const rewriteModes = new Set;
|
|
21332
|
-
const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
|
|
21333
|
-
if (pathsNormalized) {
|
|
21334
|
-
rewriteModes.add("normalize:patch-paths");
|
|
21335
|
-
}
|
|
21336
21454
|
const dependencyGroups = new Map;
|
|
21337
21455
|
for (const hunk of hunks) {
|
|
21338
21456
|
if (hunk.type === "add") {
|
|
@@ -21387,14 +21505,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21387
21505
|
continue;
|
|
21388
21506
|
}
|
|
21389
21507
|
changed = true;
|
|
21390
|
-
rewrittenChunks += 1;
|
|
21391
|
-
if (chunk.strategy) {
|
|
21392
|
-
rewriteModes.add(chunk.strategy);
|
|
21393
|
-
continue;
|
|
21394
|
-
}
|
|
21395
|
-
if (chunk.matchComparator && chunk.matchComparator !== "exact") {
|
|
21396
|
-
rewriteModes.add(`match:${chunk.matchComparator}`);
|
|
21397
|
-
}
|
|
21398
21508
|
}
|
|
21399
21509
|
const nextOutputPath = hunk.move_path ?? hunk.path;
|
|
21400
21510
|
const nextOutputFilePath = movePath ?? filePath;
|
|
@@ -21402,7 +21512,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21402
21512
|
const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
|
|
21403
21513
|
rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
|
|
21404
21514
|
changed = true;
|
|
21405
|
-
rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
|
|
21406
21515
|
clearDependencyGroup(filePath);
|
|
21407
21516
|
if (movePath && movePath !== filePath) {
|
|
21408
21517
|
clearDependencyGroup(movePath);
|
|
@@ -21449,35 +21558,23 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21449
21558
|
if (pathsNormalized) {
|
|
21450
21559
|
return {
|
|
21451
21560
|
patchText: formatPatch({ hunks }),
|
|
21452
|
-
changed: true
|
|
21453
|
-
rewrittenChunks: 0,
|
|
21454
|
-
totalChunks,
|
|
21455
|
-
rewriteModes: [...rewriteModes].sort()
|
|
21561
|
+
changed: true
|
|
21456
21562
|
};
|
|
21457
21563
|
}
|
|
21458
21564
|
if (normalizedPatchText !== patchText) {
|
|
21459
21565
|
return {
|
|
21460
21566
|
patchText: normalizedPatchText,
|
|
21461
|
-
changed: true
|
|
21462
|
-
rewrittenChunks: 0,
|
|
21463
|
-
totalChunks,
|
|
21464
|
-
rewriteModes: ["normalize:patch-text"]
|
|
21567
|
+
changed: true
|
|
21465
21568
|
};
|
|
21466
21569
|
}
|
|
21467
21570
|
return {
|
|
21468
21571
|
patchText,
|
|
21469
|
-
changed: false
|
|
21470
|
-
rewrittenChunks: 0,
|
|
21471
|
-
totalChunks,
|
|
21472
|
-
rewriteModes: []
|
|
21572
|
+
changed: false
|
|
21473
21573
|
};
|
|
21474
21574
|
}
|
|
21475
21575
|
return {
|
|
21476
21576
|
patchText: formatPatch({ hunks: rewritten }),
|
|
21477
|
-
changed: true
|
|
21478
|
-
rewrittenChunks,
|
|
21479
|
-
totalChunks,
|
|
21480
|
-
rewriteModes: [...rewriteModes].sort()
|
|
21577
|
+
changed: true
|
|
21481
21578
|
};
|
|
21482
21579
|
} catch (error) {
|
|
21483
21580
|
throw ensureApplyPatchError(error, "Unexpected rewrite failure");
|
|
@@ -21497,30 +21594,36 @@ function createApplyPatchHook(ctx) {
|
|
|
21497
21594
|
if (input.tool !== "apply_patch") {
|
|
21498
21595
|
return;
|
|
21499
21596
|
}
|
|
21500
|
-
|
|
21597
|
+
const args = output.args;
|
|
21598
|
+
if (!args || typeof args.patchText !== "string") {
|
|
21501
21599
|
return;
|
|
21502
21600
|
}
|
|
21601
|
+
const patchText = args.patchText;
|
|
21503
21602
|
const root = input.directory || ctx.directory || process.cwd();
|
|
21504
21603
|
const worktree = ctx.worktree || root;
|
|
21505
21604
|
try {
|
|
21506
|
-
const result = await rewritePatch(root,
|
|
21605
|
+
const result = await rewritePatch(root, patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
|
|
21507
21606
|
if (result.changed) {
|
|
21508
|
-
|
|
21509
|
-
logHookStatus("rewrite"
|
|
21510
|
-
rewrittenChunks: result.rewrittenChunks,
|
|
21511
|
-
totalChunks: result.totalChunks,
|
|
21512
|
-
strategies: result.rewriteModes
|
|
21513
|
-
});
|
|
21607
|
+
args.patchText = result.patchText;
|
|
21608
|
+
logHookStatus("rewrite");
|
|
21514
21609
|
return;
|
|
21515
21610
|
}
|
|
21516
|
-
logHookStatus("unchanged"
|
|
21517
|
-
rewrittenChunks: 0,
|
|
21518
|
-
totalChunks: result.totalChunks
|
|
21519
|
-
});
|
|
21611
|
+
logHookStatus("unchanged");
|
|
21520
21612
|
return;
|
|
21521
21613
|
} catch (error) {
|
|
21522
21614
|
const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
21523
21615
|
const details = getApplyPatchErrorDetails(normalizedError);
|
|
21616
|
+
if (normalizedError.kind === "blocked" && details?.code === "outside_workspace") {
|
|
21617
|
+
logHookStatus("skipped", {
|
|
21618
|
+
kind: details.kind,
|
|
21619
|
+
code: details.code,
|
|
21620
|
+
reason: normalizedError.message,
|
|
21621
|
+
failOpen: true,
|
|
21622
|
+
rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
|
|
21623
|
+
rewriteStage: "before-native"
|
|
21624
|
+
});
|
|
21625
|
+
return;
|
|
21626
|
+
}
|
|
21524
21627
|
logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
|
|
21525
21628
|
kind: details?.kind ?? "internal",
|
|
21526
21629
|
code: details?.code ?? "internal_unexpected",
|
|
@@ -22036,11 +22139,8 @@ function resolveRuntimeAgentName(config, agentName) {
|
|
|
22036
22139
|
function escapeRegExp2(value) {
|
|
22037
22140
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22038
22141
|
}
|
|
22039
|
-
function
|
|
22040
|
-
|
|
22041
|
-
return text;
|
|
22042
|
-
}
|
|
22043
|
-
let rewritten = text;
|
|
22142
|
+
function createDisplayNameMentionRewriter(config) {
|
|
22143
|
+
const replacements = [];
|
|
22044
22144
|
for (const internalName of getRuntimeAgentNames(config)) {
|
|
22045
22145
|
const displayName = getAgentOverride(config, internalName)?.displayName;
|
|
22046
22146
|
if (!displayName) {
|
|
@@ -22050,9 +22150,24 @@ function rewriteDisplayNameMentions(config, text) {
|
|
|
22050
22150
|
if (!normalizedDisplayName || normalizedDisplayName === internalName) {
|
|
22051
22151
|
continue;
|
|
22052
22152
|
}
|
|
22053
|
-
|
|
22153
|
+
replacements.push({
|
|
22154
|
+
regex: new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"),
|
|
22155
|
+
internalName
|
|
22156
|
+
});
|
|
22054
22157
|
}
|
|
22055
|
-
|
|
22158
|
+
if (replacements.length === 0) {
|
|
22159
|
+
return (text) => text;
|
|
22160
|
+
}
|
|
22161
|
+
return (text) => {
|
|
22162
|
+
if (!text.includes("@")) {
|
|
22163
|
+
return text;
|
|
22164
|
+
}
|
|
22165
|
+
let rewritten = text;
|
|
22166
|
+
for (const replacement of replacements) {
|
|
22167
|
+
rewritten = rewritten.replace(replacement.regex, `$1@${replacement.internalName}`);
|
|
22168
|
+
}
|
|
22169
|
+
return rewritten;
|
|
22170
|
+
};
|
|
22056
22171
|
}
|
|
22057
22172
|
// src/utils/internal-initiator.ts
|
|
22058
22173
|
var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
|
|
@@ -22076,6 +22191,8 @@ function hasInternalInitiatorMarker(part) {
|
|
|
22076
22191
|
return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
|
|
22077
22192
|
}
|
|
22078
22193
|
// src/utils/session-manager.ts
|
|
22194
|
+
var MIN_CONTEXT_FILE_LINES = 10;
|
|
22195
|
+
var MAX_CONTEXT_FILES_PER_SESSION = 8;
|
|
22079
22196
|
function aliasPrefix(agentType) {
|
|
22080
22197
|
switch (agentType) {
|
|
22081
22198
|
case "explorer":
|
|
@@ -22115,11 +22232,15 @@ function deriveTaskSessionLabel(input) {
|
|
|
22115
22232
|
|
|
22116
22233
|
class SessionManager {
|
|
22117
22234
|
maxSessionsPerAgent;
|
|
22235
|
+
readContextMinLines;
|
|
22236
|
+
readContextMaxFiles;
|
|
22118
22237
|
sessionsByParent = new Map;
|
|
22119
22238
|
nextAliasIndexByParent = new Map;
|
|
22120
22239
|
orderCounter = 0;
|
|
22121
|
-
constructor(maxSessionsPerAgent) {
|
|
22240
|
+
constructor(maxSessionsPerAgent, options = {}) {
|
|
22122
22241
|
this.maxSessionsPerAgent = maxSessionsPerAgent;
|
|
22242
|
+
this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
|
|
22243
|
+
this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
|
|
22123
22244
|
}
|
|
22124
22245
|
remember(input) {
|
|
22125
22246
|
const now = this.nextOrder();
|
|
@@ -22138,6 +22259,7 @@ class SessionManager {
|
|
|
22138
22259
|
taskId: input.taskId,
|
|
22139
22260
|
agentType: input.agentType,
|
|
22140
22261
|
label: input.label,
|
|
22262
|
+
contextFiles: [],
|
|
22141
22263
|
createdAt: now,
|
|
22142
22264
|
lastUsedAt: now
|
|
22143
22265
|
};
|
|
@@ -22171,6 +22293,39 @@ class SessionManager {
|
|
|
22171
22293
|
}
|
|
22172
22294
|
}
|
|
22173
22295
|
}
|
|
22296
|
+
taskIds() {
|
|
22297
|
+
const ids = new Set;
|
|
22298
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22299
|
+
for (const group of groups.values()) {
|
|
22300
|
+
for (const entry of group) {
|
|
22301
|
+
ids.add(entry.taskId);
|
|
22302
|
+
}
|
|
22303
|
+
}
|
|
22304
|
+
}
|
|
22305
|
+
return ids;
|
|
22306
|
+
}
|
|
22307
|
+
addContext(taskId, files) {
|
|
22308
|
+
if (files.length === 0)
|
|
22309
|
+
return;
|
|
22310
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22311
|
+
for (const group of groups.values()) {
|
|
22312
|
+
const match = group.find((entry) => entry.taskId === taskId);
|
|
22313
|
+
if (!match)
|
|
22314
|
+
continue;
|
|
22315
|
+
const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
|
|
22316
|
+
for (const file of files) {
|
|
22317
|
+
const previous = existing.get(file.path);
|
|
22318
|
+
if (previous) {
|
|
22319
|
+
previous.lineCount = Math.max(previous.lineCount, file.lineCount);
|
|
22320
|
+
previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
|
|
22321
|
+
continue;
|
|
22322
|
+
}
|
|
22323
|
+
match.contextFiles.push({ ...file });
|
|
22324
|
+
}
|
|
22325
|
+
this.trimContextFiles(match);
|
|
22326
|
+
}
|
|
22327
|
+
}
|
|
22328
|
+
}
|
|
22174
22329
|
clearParent(parentSessionId) {
|
|
22175
22330
|
this.sessionsByParent.delete(parentSessionId);
|
|
22176
22331
|
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
@@ -22182,7 +22337,17 @@ class SessionManager {
|
|
|
22182
22337
|
const lines = [...groups.entries()].map(([agentType, entries]) => [
|
|
22183
22338
|
agentType,
|
|
22184
22339
|
[...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
22185
|
-
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) =>
|
|
22340
|
+
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
|
|
22341
|
+
`- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
|
|
22342
|
+
...entries.map((entry) => [
|
|
22343
|
+
entry,
|
|
22344
|
+
formatContextFiles(entry.contextFiles, {
|
|
22345
|
+
minLines: this.readContextMinLines,
|
|
22346
|
+
maxFiles: this.readContextMaxFiles
|
|
22347
|
+
})
|
|
22348
|
+
]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
|
|
22349
|
+
].join(`
|
|
22350
|
+
`));
|
|
22186
22351
|
if (lines.length === 0)
|
|
22187
22352
|
return;
|
|
22188
22353
|
return [
|
|
@@ -22236,11 +22401,25 @@ class SessionManager {
|
|
|
22236
22401
|
group.length = this.maxSessionsPerAgent;
|
|
22237
22402
|
}
|
|
22238
22403
|
}
|
|
22404
|
+
trimContextFiles(entry) {
|
|
22405
|
+
if (this.readContextMaxFiles === 0) {
|
|
22406
|
+
entry.contextFiles = [];
|
|
22407
|
+
return;
|
|
22408
|
+
}
|
|
22409
|
+
entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
|
|
22410
|
+
}
|
|
22239
22411
|
nextOrder() {
|
|
22240
22412
|
this.orderCounter += 1;
|
|
22241
22413
|
return this.orderCounter;
|
|
22242
22414
|
}
|
|
22243
22415
|
}
|
|
22416
|
+
function formatContextFiles(files, options) {
|
|
22417
|
+
const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
|
|
22418
|
+
const shown = eligible.slice(0, options.maxFiles);
|
|
22419
|
+
const rest = eligible.length - shown.length;
|
|
22420
|
+
const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
|
|
22421
|
+
return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
|
|
22422
|
+
}
|
|
22244
22423
|
// src/utils/task.ts
|
|
22245
22424
|
function parseTaskIdFromTaskOutput(output) {
|
|
22246
22425
|
const lines = output.split(/\r?\n/);
|
|
@@ -22542,6 +22721,17 @@ ${allowedEntries.map((entry) => entry.block).join(`
|
|
|
22542
22721
|
});
|
|
22543
22722
|
}
|
|
22544
22723
|
function createFilterAvailableSkillsHook(_ctx, config) {
|
|
22724
|
+
const permissionRulesByAgent = new Map;
|
|
22725
|
+
const getPermissionRules = (agentName) => {
|
|
22726
|
+
const cached = permissionRulesByAgent.get(agentName);
|
|
22727
|
+
if (cached) {
|
|
22728
|
+
return cached;
|
|
22729
|
+
}
|
|
22730
|
+
const configuredSkills = getAgentOverride(config, agentName)?.skills;
|
|
22731
|
+
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22732
|
+
permissionRulesByAgent.set(agentName, permissionRules);
|
|
22733
|
+
return permissionRules;
|
|
22734
|
+
};
|
|
22545
22735
|
return {
|
|
22546
22736
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
22547
22737
|
const { messages } = output;
|
|
@@ -22549,8 +22739,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
22549
22739
|
return;
|
|
22550
22740
|
}
|
|
22551
22741
|
const agentName = getCurrentAgent(messages);
|
|
22552
|
-
const
|
|
22553
|
-
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22742
|
+
const permissionRules = getPermissionRules(agentName);
|
|
22554
22743
|
for (const message of messages) {
|
|
22555
22744
|
for (const part of message.parts) {
|
|
22556
22745
|
if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
|
|
@@ -22903,7 +23092,21 @@ function processImageAttachments(args) {
|
|
|
22903
23092
|
const observerEnabled = !disabledAgents.has("observer");
|
|
22904
23093
|
if (!observerEnabled)
|
|
22905
23094
|
return;
|
|
23095
|
+
const messagesWithImages = [];
|
|
23096
|
+
for (const msg of messages) {
|
|
23097
|
+
if (msg.info.role !== "user")
|
|
23098
|
+
continue;
|
|
23099
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
23100
|
+
if (imageParts.length > 0) {
|
|
23101
|
+
messagesWithImages.push({ msg, imageParts });
|
|
23102
|
+
}
|
|
23103
|
+
}
|
|
22906
23104
|
const saveDir = join7(workDir, ".opencode", "images");
|
|
23105
|
+
if (messagesWithImages.length === 0) {
|
|
23106
|
+
if (existsSync4(saveDir))
|
|
23107
|
+
cleanupAllSessions(saveDir);
|
|
23108
|
+
return;
|
|
23109
|
+
}
|
|
22907
23110
|
const gitignorePath = join7(workDir, ".opencode", ".gitignore");
|
|
22908
23111
|
try {
|
|
22909
23112
|
mkdirSync2(saveDir, { recursive: true });
|
|
@@ -22914,12 +23117,7 @@ function processImageAttachments(args) {
|
|
|
22914
23117
|
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
22915
23118
|
}
|
|
22916
23119
|
cleanupAllSessions(saveDir);
|
|
22917
|
-
for (const msg of
|
|
22918
|
-
if (msg.info.role !== "user")
|
|
22919
|
-
continue;
|
|
22920
|
-
const imageParts = msg.parts.filter(isImagePart);
|
|
22921
|
-
if (imageParts.length === 0)
|
|
22922
|
-
continue;
|
|
23120
|
+
for (const { msg, imageParts } of messagesWithImages) {
|
|
22923
23121
|
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
22924
23122
|
const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
|
|
22925
23123
|
try {
|
|
@@ -23007,7 +23205,7 @@ ${JSON_ERROR_REMINDER}`;
|
|
|
23007
23205
|
};
|
|
23008
23206
|
}
|
|
23009
23207
|
// src/hooks/phase-reminder/index.ts
|
|
23010
|
-
var PHASE_REMINDER = `<
|
|
23208
|
+
var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
|
|
23011
23209
|
function createPhaseReminderHook() {
|
|
23012
23210
|
return {
|
|
23013
23211
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
@@ -23038,11 +23236,14 @@ function createPhaseReminderHook() {
|
|
|
23038
23236
|
if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
|
|
23039
23237
|
return;
|
|
23040
23238
|
}
|
|
23041
|
-
|
|
23239
|
+
if (originalText.includes(PHASE_REMINDER)) {
|
|
23240
|
+
return;
|
|
23241
|
+
}
|
|
23242
|
+
lastUserMessage.parts[textPartIndex].text = `${originalText}
|
|
23042
23243
|
|
|
23043
23244
|
---
|
|
23044
23245
|
|
|
23045
|
-
${
|
|
23246
|
+
${PHASE_REMINDER}`;
|
|
23046
23247
|
}
|
|
23047
23248
|
};
|
|
23048
23249
|
}
|
|
@@ -23050,36 +23251,36 @@ ${originalText}`;
|
|
|
23050
23251
|
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
23051
23252
|
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
23052
23253
|
function createPostFileToolNudgeHook(options = {}) {
|
|
23053
|
-
|
|
23254
|
+
function appendReminder(output) {
|
|
23255
|
+
if (typeof output.output !== "string") {
|
|
23256
|
+
return;
|
|
23257
|
+
}
|
|
23258
|
+
if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
|
|
23259
|
+
return;
|
|
23260
|
+
}
|
|
23261
|
+
output.output = [
|
|
23262
|
+
output.output,
|
|
23263
|
+
"",
|
|
23264
|
+
"<internal_reminder>",
|
|
23265
|
+
POST_FILE_TOOL_NUDGE,
|
|
23266
|
+
"</internal_reminder>"
|
|
23267
|
+
].join(`
|
|
23268
|
+
`);
|
|
23269
|
+
}
|
|
23054
23270
|
return {
|
|
23055
|
-
"tool.execute.after": async (input,
|
|
23271
|
+
"tool.execute.after": async (input, output) => {
|
|
23056
23272
|
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
23057
23273
|
return;
|
|
23058
23274
|
}
|
|
23059
|
-
pendingSessionIds.add(input.sessionID);
|
|
23060
|
-
},
|
|
23061
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
23062
|
-
if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
|
|
23063
|
-
return;
|
|
23064
|
-
}
|
|
23065
23275
|
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
23066
23276
|
return;
|
|
23067
23277
|
}
|
|
23068
|
-
output
|
|
23069
|
-
},
|
|
23070
|
-
event: async (input) => {
|
|
23071
|
-
if (input.event.type !== "session.deleted") {
|
|
23072
|
-
return;
|
|
23073
|
-
}
|
|
23074
|
-
const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
|
|
23075
|
-
if (!sessionID) {
|
|
23076
|
-
return;
|
|
23077
|
-
}
|
|
23078
|
-
pendingSessionIds.delete(sessionID);
|
|
23278
|
+
appendReminder(output);
|
|
23079
23279
|
}
|
|
23080
23280
|
};
|
|
23081
23281
|
}
|
|
23082
23282
|
// src/hooks/task-session-manager/index.ts
|
|
23283
|
+
import path8 from "node:path";
|
|
23083
23284
|
var AGENT_NAME_SET = new Set([
|
|
23084
23285
|
"orchestrator",
|
|
23085
23286
|
"oracle",
|
|
@@ -23092,16 +23293,97 @@ var AGENT_NAME_SET = new Set([
|
|
|
23092
23293
|
"councillor"
|
|
23093
23294
|
]);
|
|
23094
23295
|
var MAX_PENDING_TASK_CALLS = 100;
|
|
23296
|
+
var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
|
|
23297
|
+
var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
|
|
23095
23298
|
function isAgentName(value) {
|
|
23096
23299
|
return typeof value === "string" && AGENT_NAME_SET.has(value);
|
|
23097
23300
|
}
|
|
23098
23301
|
function isObjectRecord(value) {
|
|
23099
23302
|
return typeof value === "object" && value !== null;
|
|
23100
23303
|
}
|
|
23304
|
+
function extractPath(output) {
|
|
23305
|
+
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
23306
|
+
}
|
|
23307
|
+
function normalizePath(root, file) {
|
|
23308
|
+
const relative = path8.relative(root, file);
|
|
23309
|
+
if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
23310
|
+
return file;
|
|
23311
|
+
}
|
|
23312
|
+
return relative;
|
|
23313
|
+
}
|
|
23314
|
+
function extractReadFiles(root, output) {
|
|
23315
|
+
if (typeof output.output !== "string")
|
|
23316
|
+
return [];
|
|
23317
|
+
const file = extractPath(output.output);
|
|
23318
|
+
if (!file)
|
|
23319
|
+
return [];
|
|
23320
|
+
return [
|
|
23321
|
+
{
|
|
23322
|
+
path: normalizePath(root, file),
|
|
23323
|
+
lineCount: countReadLines(output.output).length,
|
|
23324
|
+
lineNumbers: countReadLines(output.output),
|
|
23325
|
+
lastReadAt: Date.now()
|
|
23326
|
+
}
|
|
23327
|
+
];
|
|
23328
|
+
}
|
|
23329
|
+
function countReadLines(output) {
|
|
23330
|
+
const lines = new Set;
|
|
23331
|
+
for (const match of output.matchAll(/^([0-9]+):/gm)) {
|
|
23332
|
+
lines.add(Number(match[1]));
|
|
23333
|
+
}
|
|
23334
|
+
return [...lines];
|
|
23335
|
+
}
|
|
23101
23336
|
function createTaskSessionManagerHook(_ctx, options) {
|
|
23102
|
-
const sessionManager = new SessionManager(options.maxSessionsPerAgent
|
|
23337
|
+
const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
|
|
23338
|
+
readContextMinLines: options.readContextMinLines,
|
|
23339
|
+
readContextMaxFiles: options.readContextMaxFiles
|
|
23340
|
+
});
|
|
23103
23341
|
const pendingCalls = new Map;
|
|
23104
23342
|
const pendingCallOrder = [];
|
|
23343
|
+
const contextByTask = new Map;
|
|
23344
|
+
const pendingManagedTaskIds = new Set;
|
|
23345
|
+
function addTaskContext(taskId, files) {
|
|
23346
|
+
if (files.length === 0)
|
|
23347
|
+
return;
|
|
23348
|
+
let context = contextByTask.get(taskId);
|
|
23349
|
+
if (!context) {
|
|
23350
|
+
context = new Map;
|
|
23351
|
+
contextByTask.set(taskId, context);
|
|
23352
|
+
}
|
|
23353
|
+
for (const file of files) {
|
|
23354
|
+
const pending = context.get(file.path) ?? {
|
|
23355
|
+
path: file.path,
|
|
23356
|
+
lines: new Set,
|
|
23357
|
+
lastReadAt: file.lastReadAt
|
|
23358
|
+
};
|
|
23359
|
+
for (const line of file.lineNumbers ?? []) {
|
|
23360
|
+
pending.lines.add(line);
|
|
23361
|
+
}
|
|
23362
|
+
pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
|
|
23363
|
+
context.set(file.path, pending);
|
|
23364
|
+
}
|
|
23365
|
+
sessionManager.addContext(taskId, contextFilesForPrompt(context));
|
|
23366
|
+
}
|
|
23367
|
+
function contextFilesForPrompt(context) {
|
|
23368
|
+
if (!context)
|
|
23369
|
+
return [];
|
|
23370
|
+
return [...context.values()].map((file) => ({
|
|
23371
|
+
path: file.path,
|
|
23372
|
+
lineCount: file.lines.size,
|
|
23373
|
+
lastReadAt: file.lastReadAt
|
|
23374
|
+
}));
|
|
23375
|
+
}
|
|
23376
|
+
function canTrackTaskContext(taskId) {
|
|
23377
|
+
return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
|
|
23378
|
+
}
|
|
23379
|
+
function pruneContext() {
|
|
23380
|
+
const remembered = sessionManager.taskIds();
|
|
23381
|
+
for (const taskId of contextByTask.keys()) {
|
|
23382
|
+
if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
|
|
23383
|
+
contextByTask.delete(taskId);
|
|
23384
|
+
}
|
|
23385
|
+
}
|
|
23386
|
+
}
|
|
23105
23387
|
function isMissingRememberedSessionError(output) {
|
|
23106
23388
|
const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
|
|
23107
23389
|
return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
|
|
@@ -23167,6 +23449,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23167
23449
|
return;
|
|
23168
23450
|
}
|
|
23169
23451
|
args.task_id = remembered.taskId;
|
|
23452
|
+
pendingManagedTaskIds.add(remembered.taskId);
|
|
23170
23453
|
sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
|
|
23171
23454
|
if (input.callID) {
|
|
23172
23455
|
rememberPendingCall({
|
|
@@ -23179,6 +23462,12 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23179
23462
|
}
|
|
23180
23463
|
},
|
|
23181
23464
|
"tool.execute.after": async (input, output) => {
|
|
23465
|
+
if (input.tool.toLowerCase() === "read") {
|
|
23466
|
+
if (input.sessionID && canTrackTaskContext(input.sessionID)) {
|
|
23467
|
+
addTaskContext(input.sessionID, extractReadFiles(_ctx.directory, output));
|
|
23468
|
+
}
|
|
23469
|
+
return;
|
|
23470
|
+
}
|
|
23182
23471
|
if (input.tool.toLowerCase() !== "task")
|
|
23183
23472
|
return;
|
|
23184
23473
|
const pending = takePendingCall(input.callID);
|
|
@@ -23200,17 +23489,50 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23200
23489
|
agentType: pending.agentType,
|
|
23201
23490
|
label: pending.label
|
|
23202
23491
|
});
|
|
23492
|
+
pendingManagedTaskIds.delete(taskId);
|
|
23493
|
+
const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
|
|
23494
|
+
sessionManager.addContext(taskId, contextFiles);
|
|
23495
|
+
pruneContext();
|
|
23203
23496
|
},
|
|
23204
|
-
"experimental.chat.
|
|
23205
|
-
|
|
23497
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
23498
|
+
for (let i = output.messages.length - 1;i >= 0; i -= 1) {
|
|
23499
|
+
const message = output.messages[i];
|
|
23500
|
+
if (message.info.role !== "user")
|
|
23501
|
+
continue;
|
|
23502
|
+
if (message.info.agent && message.info.agent !== "orchestrator")
|
|
23503
|
+
return;
|
|
23504
|
+
if (!message.info.sessionID || !options.shouldManageSession(message.info.sessionID)) {
|
|
23505
|
+
return;
|
|
23506
|
+
}
|
|
23507
|
+
const reminder = sessionManager.formatForPrompt(message.info.sessionID);
|
|
23508
|
+
if (!reminder)
|
|
23509
|
+
return;
|
|
23510
|
+
const textPart = message.parts.find((part) => part.type === "text" && typeof part.text === "string");
|
|
23511
|
+
if (!textPart)
|
|
23512
|
+
return;
|
|
23513
|
+
if (textPart.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER))
|
|
23514
|
+
return;
|
|
23515
|
+
if (textPart.text?.includes(RESUMABLE_SESSIONS_START))
|
|
23516
|
+
return;
|
|
23517
|
+
textPart.text = [
|
|
23518
|
+
textPart.text ?? "",
|
|
23519
|
+
"",
|
|
23520
|
+
RESUMABLE_SESSIONS_START,
|
|
23521
|
+
reminder,
|
|
23522
|
+
RESUMABLE_SESSIONS_END
|
|
23523
|
+
].join(`
|
|
23524
|
+
`);
|
|
23206
23525
|
return;
|
|
23207
23526
|
}
|
|
23208
|
-
const reminder = sessionManager.formatForPrompt(input.sessionID);
|
|
23209
|
-
if (!reminder)
|
|
23210
|
-
return;
|
|
23211
|
-
output.system.push(reminder);
|
|
23212
23527
|
},
|
|
23213
23528
|
event: async (input) => {
|
|
23529
|
+
if (input.event.type === "session.created") {
|
|
23530
|
+
const info = input.event.properties?.info;
|
|
23531
|
+
if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
|
|
23532
|
+
pendingManagedTaskIds.add(info.id);
|
|
23533
|
+
}
|
|
23534
|
+
return;
|
|
23535
|
+
}
|
|
23214
23536
|
if (input.event.type !== "session.deleted")
|
|
23215
23537
|
return;
|
|
23216
23538
|
const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
@@ -23218,6 +23540,9 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23218
23540
|
return;
|
|
23219
23541
|
sessionManager.clearParent(sessionId);
|
|
23220
23542
|
sessionManager.dropTask(sessionId);
|
|
23543
|
+
contextByTask.delete(sessionId);
|
|
23544
|
+
pendingManagedTaskIds.delete(sessionId);
|
|
23545
|
+
pruneContext();
|
|
23221
23546
|
for (const [callId, pending] of pendingCalls.entries()) {
|
|
23222
23547
|
if (pending.parentSessionId !== sessionId) {
|
|
23223
23548
|
continue;
|
|
@@ -23263,7 +23588,7 @@ function createTodoHygiene(options) {
|
|
|
23263
23588
|
handleRequestStart(input) {
|
|
23264
23589
|
clear(input.sessionID);
|
|
23265
23590
|
},
|
|
23266
|
-
async handleToolExecuteAfter(input) {
|
|
23591
|
+
async handleToolExecuteAfter(input, _output) {
|
|
23267
23592
|
if (!input.sessionID) {
|
|
23268
23593
|
return;
|
|
23269
23594
|
}
|
|
@@ -23273,6 +23598,10 @@ function createTodoHygiene(options) {
|
|
|
23273
23598
|
}
|
|
23274
23599
|
try {
|
|
23275
23600
|
if (RESET.has(tool)) {
|
|
23601
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
23602
|
+
clear(input.sessionID);
|
|
23603
|
+
return;
|
|
23604
|
+
}
|
|
23276
23605
|
active.add(input.sessionID);
|
|
23277
23606
|
clearCycle(input.sessionID);
|
|
23278
23607
|
const state2 = await options.getTodoState(input.sessionID);
|
|
@@ -23331,39 +23660,22 @@ function createTodoHygiene(options) {
|
|
|
23331
23660
|
});
|
|
23332
23661
|
}
|
|
23333
23662
|
},
|
|
23334
|
-
|
|
23335
|
-
|
|
23336
|
-
return;
|
|
23337
|
-
}
|
|
23338
|
-
const reasons = pending.get(input.sessionID);
|
|
23663
|
+
getPendingReminder(sessionID) {
|
|
23664
|
+
const reasons = pending.get(sessionID);
|
|
23339
23665
|
if (!reasons || reasons.size === 0) {
|
|
23340
|
-
return;
|
|
23341
|
-
}
|
|
23342
|
-
const reminder = pick(reasons);
|
|
23343
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
23344
|
-
clear(input.sessionID);
|
|
23345
|
-
return;
|
|
23666
|
+
return null;
|
|
23346
23667
|
}
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
clear(input.sessionID);
|
|
23351
|
-
return;
|
|
23352
|
-
}
|
|
23353
|
-
pending.delete(input.sessionID);
|
|
23354
|
-
output.system.push(reminder);
|
|
23355
|
-
options.log?.("Injected todo hygiene reminder", {
|
|
23356
|
-
sessionID: input.sessionID,
|
|
23357
|
-
reminder,
|
|
23358
|
-
reasons: Array.from(reasons)
|
|
23359
|
-
});
|
|
23360
|
-
} catch (error) {
|
|
23361
|
-
pending.delete(input.sessionID);
|
|
23362
|
-
options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
|
|
23363
|
-
sessionID: input.sessionID,
|
|
23364
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23365
|
-
});
|
|
23668
|
+
if (options.shouldInject && !options.shouldInject(sessionID)) {
|
|
23669
|
+
clear(sessionID);
|
|
23670
|
+
return null;
|
|
23366
23671
|
}
|
|
23672
|
+
const reminder = pick(reasons);
|
|
23673
|
+
options.log?.("Read todo hygiene reminder", {
|
|
23674
|
+
sessionID,
|
|
23675
|
+
reminder,
|
|
23676
|
+
reasons: Array.from(reasons)
|
|
23677
|
+
});
|
|
23678
|
+
return reminder;
|
|
23367
23679
|
},
|
|
23368
23680
|
handleEvent(event) {
|
|
23369
23681
|
if (event.type !== "session.deleted") {
|
|
@@ -23382,6 +23694,8 @@ function createTodoHygiene(options) {
|
|
|
23382
23694
|
var HOOK_NAME = "todo-continuation";
|
|
23383
23695
|
var COMMAND_NAME = "auto-continue";
|
|
23384
23696
|
var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
|
|
23697
|
+
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
23698
|
+
var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
|
|
23385
23699
|
var SUPPRESS_AFTER_ABORT_MS = 5000;
|
|
23386
23700
|
var NOTIFICATION_BUSY_GRACE_MS = 250;
|
|
23387
23701
|
var QUESTION_PHRASES = [
|
|
@@ -23419,6 +23733,35 @@ function resetState(state) {
|
|
|
23419
23733
|
state.notifyingSessionIds.clear();
|
|
23420
23734
|
state.notificationBusyUntilBySession.clear();
|
|
23421
23735
|
}
|
|
23736
|
+
function stripTodoHygieneInstruction(text) {
|
|
23737
|
+
const trimmed = text.trimEnd();
|
|
23738
|
+
if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
|
|
23739
|
+
return trimmed;
|
|
23740
|
+
}
|
|
23741
|
+
const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
|
|
23742
|
+
if (start === -1) {
|
|
23743
|
+
return trimmed;
|
|
23744
|
+
}
|
|
23745
|
+
return trimmed.slice(0, start).trimEnd();
|
|
23746
|
+
}
|
|
23747
|
+
function appendTodoHygieneInstruction(message, reminder) {
|
|
23748
|
+
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
23749
|
+
if (!textPart)
|
|
23750
|
+
return;
|
|
23751
|
+
const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
23752
|
+
const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
|
|
23753
|
+
${reminder}
|
|
23754
|
+
${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
|
|
23755
|
+
textPart.text = baseText ? `${baseText}
|
|
23756
|
+
|
|
23757
|
+
${instruction}` : instruction;
|
|
23758
|
+
}
|
|
23759
|
+
function stripTodoHygieneInstructionFromMessage(message) {
|
|
23760
|
+
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
23761
|
+
if (!textPart)
|
|
23762
|
+
return;
|
|
23763
|
+
textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
23764
|
+
}
|
|
23422
23765
|
function createTodoContinuationHook(ctx, config) {
|
|
23423
23766
|
const maxContinuations = config?.maxContinuations ?? 5;
|
|
23424
23767
|
const cooldownMs = config?.cooldownMs ?? 3000;
|
|
@@ -23494,7 +23837,8 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23494
23837
|
const sessionID = inferSessionID(messages, i);
|
|
23495
23838
|
const partSignature = message.parts.map((part) => {
|
|
23496
23839
|
if (part.type === "text" && typeof part.text === "string") {
|
|
23497
|
-
|
|
23840
|
+
const text = stripTodoHygieneInstruction(part.text);
|
|
23841
|
+
return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
|
|
23498
23842
|
}
|
|
23499
23843
|
return part.type ?? "unknown";
|
|
23500
23844
|
}).join("|");
|
|
@@ -23502,6 +23846,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23502
23846
|
return {
|
|
23503
23847
|
sessionID,
|
|
23504
23848
|
agent: message.info.agent,
|
|
23849
|
+
message,
|
|
23505
23850
|
signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
|
|
23506
23851
|
};
|
|
23507
23852
|
}
|
|
@@ -23529,9 +23874,16 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23529
23874
|
return;
|
|
23530
23875
|
}
|
|
23531
23876
|
if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
|
|
23877
|
+
const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
|
|
23878
|
+
if (reminder) {
|
|
23879
|
+
appendTodoHygieneInstruction(lastUserMessage.message, reminder);
|
|
23880
|
+
} else {
|
|
23881
|
+
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
23882
|
+
}
|
|
23532
23883
|
return;
|
|
23533
23884
|
}
|
|
23534
23885
|
requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
|
|
23886
|
+
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
23535
23887
|
hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
|
|
23536
23888
|
}
|
|
23537
23889
|
function markNotificationStarted(sessionID) {
|
|
@@ -23881,7 +24233,6 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23881
24233
|
return {
|
|
23882
24234
|
tool: { auto_continue: autoContinue },
|
|
23883
24235
|
handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
|
|
23884
|
-
handleChatSystemTransform: hygiene.handleChatSystemTransform,
|
|
23885
24236
|
handleMessagesTransform,
|
|
23886
24237
|
handleEvent,
|
|
23887
24238
|
handleChatMessage,
|
|
@@ -23889,7 +24240,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23889
24240
|
};
|
|
23890
24241
|
}
|
|
23891
24242
|
// src/interview/manager.ts
|
|
23892
|
-
import
|
|
24243
|
+
import path12 from "node:path";
|
|
23893
24244
|
|
|
23894
24245
|
// src/interview/dashboard.ts
|
|
23895
24246
|
import crypto from "node:crypto";
|
|
@@ -23899,27 +24250,27 @@ import {
|
|
|
23899
24250
|
createServer
|
|
23900
24251
|
} from "node:http";
|
|
23901
24252
|
import os3 from "node:os";
|
|
23902
|
-
import
|
|
24253
|
+
import path10 from "node:path";
|
|
23903
24254
|
import { URL as URL2 } from "node:url";
|
|
23904
24255
|
|
|
23905
24256
|
// src/interview/document.ts
|
|
23906
24257
|
import * as fsSync from "node:fs";
|
|
23907
24258
|
import * as fs6 from "node:fs/promises";
|
|
23908
|
-
import * as
|
|
24259
|
+
import * as path9 from "node:path";
|
|
23909
24260
|
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23910
24261
|
function normalizeOutputFolder(outputFolder) {
|
|
23911
24262
|
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23912
24263
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23913
24264
|
}
|
|
23914
24265
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23915
|
-
return
|
|
24266
|
+
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23916
24267
|
}
|
|
23917
24268
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23918
24269
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23919
|
-
return
|
|
24270
|
+
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23920
24271
|
}
|
|
23921
24272
|
function relativeInterviewPath(directory, filePath) {
|
|
23922
|
-
return
|
|
24273
|
+
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23923
24274
|
}
|
|
23924
24275
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23925
24276
|
const trimmed = value.trim();
|
|
@@ -23928,22 +24279,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
23928
24279
|
}
|
|
23929
24280
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23930
24281
|
const candidates = new Set;
|
|
23931
|
-
const resolvedRoot =
|
|
23932
|
-
if (
|
|
24282
|
+
const resolvedRoot = path9.resolve(directory);
|
|
24283
|
+
if (path9.isAbsolute(trimmed)) {
|
|
23933
24284
|
candidates.add(trimmed);
|
|
23934
24285
|
} else {
|
|
23935
|
-
candidates.add(
|
|
23936
|
-
candidates.add(
|
|
24286
|
+
candidates.add(path9.resolve(directory, trimmed));
|
|
24287
|
+
candidates.add(path9.join(outputDir, trimmed));
|
|
23937
24288
|
if (!trimmed.endsWith(".md")) {
|
|
23938
|
-
candidates.add(
|
|
24289
|
+
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23939
24290
|
}
|
|
23940
24291
|
}
|
|
23941
24292
|
for (const candidate of candidates) {
|
|
23942
|
-
if (
|
|
24293
|
+
if (path9.extname(candidate) !== ".md") {
|
|
23943
24294
|
continue;
|
|
23944
24295
|
}
|
|
23945
|
-
const resolved =
|
|
23946
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
24296
|
+
const resolved = path9.resolve(candidate);
|
|
24297
|
+
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23947
24298
|
continue;
|
|
23948
24299
|
}
|
|
23949
24300
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -24023,7 +24374,7 @@ function parseFrontmatter(content) {
|
|
|
24023
24374
|
return result;
|
|
24024
24375
|
}
|
|
24025
24376
|
async function ensureInterviewFile(record) {
|
|
24026
|
-
await fs6.mkdir(
|
|
24377
|
+
await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
|
|
24027
24378
|
try {
|
|
24028
24379
|
await fs6.access(record.markdownPath);
|
|
24029
24380
|
} catch {
|
|
@@ -25693,12 +26044,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25693
26044
|
|
|
25694
26045
|
// src/interview/dashboard.ts
|
|
25695
26046
|
function getAuthFilePath(port) {
|
|
25696
|
-
const dataHome = process.env.XDG_DATA_HOME ||
|
|
25697
|
-
return
|
|
26047
|
+
const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
|
|
26048
|
+
return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
|
|
25698
26049
|
}
|
|
25699
26050
|
function writeAuthFile(port, token) {
|
|
25700
26051
|
const filePath = getAuthFilePath(port);
|
|
25701
|
-
const dir =
|
|
26052
|
+
const dir = path10.dirname(filePath);
|
|
25702
26053
|
try {
|
|
25703
26054
|
fsSync2.mkdirSync(dir, { recursive: true });
|
|
25704
26055
|
} catch {}
|
|
@@ -25835,7 +26186,7 @@ function createDashboardServer(config) {
|
|
|
25835
26186
|
const directories = getKnownDirectories();
|
|
25836
26187
|
const items = [];
|
|
25837
26188
|
for (const dir of directories) {
|
|
25838
|
-
const interviewDir =
|
|
26189
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25839
26190
|
let entries;
|
|
25840
26191
|
try {
|
|
25841
26192
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25847,7 +26198,7 @@ function createDashboardServer(config) {
|
|
|
25847
26198
|
continue;
|
|
25848
26199
|
let content;
|
|
25849
26200
|
try {
|
|
25850
|
-
content = await fs7.readFile(
|
|
26201
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25851
26202
|
} catch {
|
|
25852
26203
|
continue;
|
|
25853
26204
|
}
|
|
@@ -25873,7 +26224,7 @@ function createDashboardServer(config) {
|
|
|
25873
26224
|
const directories = getKnownDirectories();
|
|
25874
26225
|
let rebuilt = 0;
|
|
25875
26226
|
for (const dir of directories) {
|
|
25876
|
-
const interviewDir =
|
|
26227
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25877
26228
|
let entries;
|
|
25878
26229
|
try {
|
|
25879
26230
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25885,7 +26236,7 @@ function createDashboardServer(config) {
|
|
|
25885
26236
|
continue;
|
|
25886
26237
|
let content;
|
|
25887
26238
|
try {
|
|
25888
|
-
content = await fs7.readFile(
|
|
26239
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25889
26240
|
} catch {
|
|
25890
26241
|
continue;
|
|
25891
26242
|
}
|
|
@@ -25911,7 +26262,7 @@ function createDashboardServer(config) {
|
|
|
25911
26262
|
questions: [],
|
|
25912
26263
|
pendingAnswers: null,
|
|
25913
26264
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
25914
|
-
filePath:
|
|
26265
|
+
filePath: path10.join(interviewDir, entry),
|
|
25915
26266
|
nudgeAction: null
|
|
25916
26267
|
});
|
|
25917
26268
|
if (!sessions.has(fm.sessionID)) {
|
|
@@ -26175,7 +26526,7 @@ function createDashboardServer(config) {
|
|
|
26175
26526
|
const dirs = getKnownDirectories();
|
|
26176
26527
|
for (const dir of dirs) {
|
|
26177
26528
|
const slug = extractResumeSlug(interviewId);
|
|
26178
|
-
const candidate =
|
|
26529
|
+
const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
|
|
26179
26530
|
try {
|
|
26180
26531
|
document = await fs7.readFile(candidate, "utf8");
|
|
26181
26532
|
markdownPath = candidate;
|
|
@@ -26703,7 +27054,7 @@ function createInterviewServer(deps) {
|
|
|
26703
27054
|
// src/interview/service.ts
|
|
26704
27055
|
import { spawn } from "node:child_process";
|
|
26705
27056
|
import * as fs8 from "node:fs/promises";
|
|
26706
|
-
import * as
|
|
27057
|
+
import * as path11 from "node:path";
|
|
26707
27058
|
|
|
26708
27059
|
// src/interview/types.ts
|
|
26709
27060
|
import { z as z3 } from "zod";
|
|
@@ -26970,12 +27321,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26970
27321
|
if (!newSlug) {
|
|
26971
27322
|
return;
|
|
26972
27323
|
}
|
|
26973
|
-
const currentFileName =
|
|
27324
|
+
const currentFileName = path11.basename(interview.markdownPath, ".md");
|
|
26974
27325
|
if (currentFileName === newSlug) {
|
|
26975
27326
|
return;
|
|
26976
27327
|
}
|
|
26977
|
-
const dir =
|
|
26978
|
-
const newPath =
|
|
27328
|
+
const dir = path11.dirname(interview.markdownPath);
|
|
27329
|
+
const newPath = path11.join(dir, `${newSlug}.md`);
|
|
26979
27330
|
try {
|
|
26980
27331
|
await fs8.access(newPath);
|
|
26981
27332
|
return;
|
|
@@ -27051,9 +27402,9 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27051
27402
|
const messages = await loadMessages(sessionID);
|
|
27052
27403
|
const title = extractTitle(document);
|
|
27053
27404
|
const record = {
|
|
27054
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
27405
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
|
|
27055
27406
|
sessionID,
|
|
27056
|
-
idea: title ||
|
|
27407
|
+
idea: title || path11.basename(markdownPath, ".md"),
|
|
27057
27408
|
markdownPath,
|
|
27058
27409
|
createdAt: nowIso(),
|
|
27059
27410
|
status: "active",
|
|
@@ -27280,7 +27631,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27280
27631
|
return fileCache.items;
|
|
27281
27632
|
}
|
|
27282
27633
|
const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
|
|
27283
|
-
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) =>
|
|
27634
|
+
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
|
|
27284
27635
|
let entries;
|
|
27285
27636
|
try {
|
|
27286
27637
|
entries = await fs8.readdir(outputDir);
|
|
@@ -27291,8 +27642,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27291
27642
|
for (const entry of entries) {
|
|
27292
27643
|
if (!entry.endsWith(".md"))
|
|
27293
27644
|
continue;
|
|
27294
|
-
const fullPath =
|
|
27295
|
-
if (activePaths.has(
|
|
27645
|
+
const fullPath = path11.join(outputDir, entry);
|
|
27646
|
+
if (activePaths.has(path11.resolve(fullPath)))
|
|
27296
27647
|
continue;
|
|
27297
27648
|
let content;
|
|
27298
27649
|
try {
|
|
@@ -27391,7 +27742,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27391
27742
|
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
27392
27743
|
if (!dashboardEnabled) {
|
|
27393
27744
|
const service2 = createInterviewService(ctx, interviewConfig);
|
|
27394
|
-
const resolvedOutputPath =
|
|
27745
|
+
const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
|
|
27395
27746
|
const server = createInterviewServer({
|
|
27396
27747
|
getState: async (interviewId) => service2.getInterviewState(interviewId),
|
|
27397
27748
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
@@ -27496,7 +27847,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27496
27847
|
listInterviews: () => service.listInterviews(),
|
|
27497
27848
|
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
27498
27849
|
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
27499
|
-
outputFolder:
|
|
27850
|
+
outputFolder: path12.join(ctx.directory, outputFolder),
|
|
27500
27851
|
port: 0
|
|
27501
27852
|
});
|
|
27502
27853
|
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
@@ -27936,23 +28287,23 @@ class TmuxMultiplexer {
|
|
|
27936
28287
|
return null;
|
|
27937
28288
|
}
|
|
27938
28289
|
const stdout = await proc.stdout();
|
|
27939
|
-
const
|
|
28290
|
+
const path13 = stdout.trim().split(`
|
|
27940
28291
|
`)[0];
|
|
27941
|
-
if (!
|
|
28292
|
+
if (!path13) {
|
|
27942
28293
|
log("[tmux] findBinary: no path in output");
|
|
27943
28294
|
return null;
|
|
27944
28295
|
}
|
|
27945
|
-
const verifyProc = crossSpawn([
|
|
28296
|
+
const verifyProc = crossSpawn([path13, "-V"], {
|
|
27946
28297
|
stdout: "pipe",
|
|
27947
28298
|
stderr: "pipe"
|
|
27948
28299
|
});
|
|
27949
28300
|
const verifyExit = await verifyProc.exited;
|
|
27950
28301
|
if (verifyExit !== 0) {
|
|
27951
|
-
log("[tmux] findBinary: tmux -V failed", { path:
|
|
28302
|
+
log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
|
|
27952
28303
|
return null;
|
|
27953
28304
|
}
|
|
27954
|
-
log("[tmux] findBinary: found", { path:
|
|
27955
|
-
return
|
|
28305
|
+
log("[tmux] findBinary: found", { path: path13 });
|
|
28306
|
+
return path13;
|
|
27956
28307
|
} catch (err) {
|
|
27957
28308
|
log("[tmux] findBinary: exception", { error: String(err) });
|
|
27958
28309
|
return null;
|
|
@@ -28333,6 +28684,7 @@ class MultiplexerSessionManager {
|
|
|
28333
28684
|
sessions = new Map;
|
|
28334
28685
|
knownSessions = new Map;
|
|
28335
28686
|
spawningSessions = new Set;
|
|
28687
|
+
closingSessions = new Map;
|
|
28336
28688
|
pollInterval;
|
|
28337
28689
|
enabled = false;
|
|
28338
28690
|
constructor(ctx, config) {
|
|
@@ -28361,17 +28713,22 @@ class MultiplexerSessionManager {
|
|
|
28361
28713
|
const parentId = info.parentID;
|
|
28362
28714
|
const title = info.title ?? "Subagent";
|
|
28363
28715
|
const directory = info.directory ?? this.directory;
|
|
28364
|
-
this.knownSessions.set(sessionId, {
|
|
28365
|
-
parentId,
|
|
28366
|
-
title,
|
|
28367
|
-
directory
|
|
28368
|
-
});
|
|
28369
28716
|
if (this.isTrackedOrSpawning(sessionId)) {
|
|
28370
28717
|
log("[multiplexer-session-manager] session already tracked or spawning", {
|
|
28371
28718
|
sessionId
|
|
28372
28719
|
});
|
|
28373
28720
|
return;
|
|
28374
28721
|
}
|
|
28722
|
+
const closing = this.closingSessions.get(sessionId);
|
|
28723
|
+
if (closing)
|
|
28724
|
+
await closing;
|
|
28725
|
+
if (this.isTrackedOrSpawning(sessionId))
|
|
28726
|
+
return;
|
|
28727
|
+
this.knownSessions.set(sessionId, {
|
|
28728
|
+
parentId,
|
|
28729
|
+
title,
|
|
28730
|
+
directory
|
|
28731
|
+
});
|
|
28375
28732
|
this.spawningSessions.add(sessionId);
|
|
28376
28733
|
try {
|
|
28377
28734
|
const serverRunning = await isServerRunning(this.serverUrl);
|
|
@@ -28381,7 +28738,7 @@ class MultiplexerSessionManager {
|
|
|
28381
28738
|
});
|
|
28382
28739
|
return;
|
|
28383
28740
|
}
|
|
28384
|
-
if (this.sessions.has(sessionId)) {
|
|
28741
|
+
if (this.closingSessions.has(sessionId) || this.sessions.has(sessionId)) {
|
|
28385
28742
|
return;
|
|
28386
28743
|
}
|
|
28387
28744
|
log("[multiplexer-session-manager] child session created, spawning pane", {
|
|
@@ -28395,23 +28752,31 @@ class MultiplexerSessionManager {
|
|
|
28395
28752
|
});
|
|
28396
28753
|
return { success: false, paneId: undefined };
|
|
28397
28754
|
});
|
|
28398
|
-
if (paneResult.success
|
|
28399
|
-
|
|
28400
|
-
|
|
28755
|
+
if (!paneResult.success || !paneResult.paneId)
|
|
28756
|
+
return;
|
|
28757
|
+
if (!this.knownSessions.has(sessionId) || this.closingSessions.has(sessionId)) {
|
|
28758
|
+
await this.multiplexer.closePane(paneResult.paneId).catch((err) => log("[multiplexer-session-manager] closing stale spawned pane failed", {
|
|
28401
28759
|
sessionId,
|
|
28402
28760
|
paneId: paneResult.paneId,
|
|
28403
|
-
|
|
28404
|
-
|
|
28405
|
-
|
|
28406
|
-
createdAt: now,
|
|
28407
|
-
lastSeenAt: now
|
|
28408
|
-
});
|
|
28409
|
-
log("[multiplexer-session-manager] pane spawned", {
|
|
28410
|
-
sessionId,
|
|
28411
|
-
paneId: paneResult.paneId
|
|
28412
|
-
});
|
|
28413
|
-
this.startPolling();
|
|
28761
|
+
error: String(err)
|
|
28762
|
+
}));
|
|
28763
|
+
return;
|
|
28414
28764
|
}
|
|
28765
|
+
const now = Date.now();
|
|
28766
|
+
this.sessions.set(sessionId, {
|
|
28767
|
+
sessionId,
|
|
28768
|
+
paneId: paneResult.paneId,
|
|
28769
|
+
parentId,
|
|
28770
|
+
title,
|
|
28771
|
+
directory,
|
|
28772
|
+
createdAt: now,
|
|
28773
|
+
lastSeenAt: now
|
|
28774
|
+
});
|
|
28775
|
+
log("[multiplexer-session-manager] pane spawned", {
|
|
28776
|
+
sessionId,
|
|
28777
|
+
paneId: paneResult.paneId
|
|
28778
|
+
});
|
|
28779
|
+
this.startPolling();
|
|
28415
28780
|
} finally {
|
|
28416
28781
|
this.spawningSessions.delete(sessionId);
|
|
28417
28782
|
}
|
|
@@ -28425,7 +28790,7 @@ class MultiplexerSessionManager {
|
|
|
28425
28790
|
if (!sessionId)
|
|
28426
28791
|
return;
|
|
28427
28792
|
if (event.properties?.status?.type === "idle") {
|
|
28428
|
-
await this.closeSession(sessionId);
|
|
28793
|
+
await this.closeSession(sessionId, "idle");
|
|
28429
28794
|
return;
|
|
28430
28795
|
}
|
|
28431
28796
|
if (event.properties?.status?.type === "busy") {
|
|
@@ -28437,14 +28802,13 @@ class MultiplexerSessionManager {
|
|
|
28437
28802
|
return;
|
|
28438
28803
|
if (event.type !== "session.deleted")
|
|
28439
28804
|
return;
|
|
28440
|
-
const sessionId = event
|
|
28805
|
+
const sessionId = this.getSessionId(event);
|
|
28441
28806
|
if (!sessionId)
|
|
28442
28807
|
return;
|
|
28443
28808
|
log("[multiplexer-session-manager] session deleted, closing pane", {
|
|
28444
28809
|
sessionId
|
|
28445
28810
|
});
|
|
28446
|
-
await this.closeSession(sessionId);
|
|
28447
|
-
this.knownSessions.delete(sessionId);
|
|
28811
|
+
await this.closeSession(sessionId, "deleted");
|
|
28448
28812
|
}
|
|
28449
28813
|
startPolling() {
|
|
28450
28814
|
if (this.pollInterval)
|
|
@@ -28481,35 +28845,58 @@ class MultiplexerSessionManager {
|
|
|
28481
28845
|
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
28482
28846
|
const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
|
|
28483
28847
|
if (isIdle || missingTooLong || isTimedOut) {
|
|
28484
|
-
sessionsToClose.push(
|
|
28848
|
+
sessionsToClose.push({
|
|
28849
|
+
sessionId,
|
|
28850
|
+
reason: isIdle ? "idle" : isTimedOut ? "timeout" : "missing"
|
|
28851
|
+
});
|
|
28485
28852
|
}
|
|
28486
28853
|
}
|
|
28487
|
-
for (const sessionId of sessionsToClose) {
|
|
28488
|
-
await this.closeSession(sessionId);
|
|
28854
|
+
for (const { sessionId, reason } of sessionsToClose) {
|
|
28855
|
+
await this.closeSession(sessionId, reason);
|
|
28489
28856
|
}
|
|
28490
28857
|
} catch (err) {
|
|
28491
28858
|
log("[multiplexer-session-manager] poll error", { error: String(err) });
|
|
28492
28859
|
}
|
|
28493
28860
|
}
|
|
28494
|
-
async closeSession(sessionId) {
|
|
28861
|
+
async closeSession(sessionId, reason) {
|
|
28862
|
+
if (reason === "deleted") {
|
|
28863
|
+
this.knownSessions.delete(sessionId);
|
|
28864
|
+
}
|
|
28865
|
+
const existingClose = this.closingSessions.get(sessionId);
|
|
28866
|
+
if (existingClose)
|
|
28867
|
+
return existingClose;
|
|
28495
28868
|
const tracked = this.sessions.get(sessionId);
|
|
28496
28869
|
if (!tracked || !this.multiplexer)
|
|
28497
28870
|
return;
|
|
28871
|
+
this.sessions.delete(sessionId);
|
|
28498
28872
|
log("[multiplexer-session-manager] closing session pane", {
|
|
28499
28873
|
sessionId,
|
|
28500
|
-
paneId: tracked.paneId
|
|
28874
|
+
paneId: tracked.paneId,
|
|
28875
|
+
reason
|
|
28501
28876
|
});
|
|
28502
|
-
|
|
28503
|
-
|
|
28504
|
-
|
|
28505
|
-
|
|
28506
|
-
|
|
28877
|
+
const closePromise = this.multiplexer.closePane(tracked.paneId).then(() => {
|
|
28878
|
+
return;
|
|
28879
|
+
}).catch((err) => log("[multiplexer-session-manager] failed to close session pane", {
|
|
28880
|
+
sessionId,
|
|
28881
|
+
paneId: tracked.paneId,
|
|
28882
|
+
reason,
|
|
28883
|
+
error: String(err)
|
|
28884
|
+
})).finally(() => {
|
|
28885
|
+
this.closingSessions.delete(sessionId);
|
|
28886
|
+
this.updatePolling();
|
|
28887
|
+
});
|
|
28888
|
+
this.closingSessions.set(sessionId, closePromise);
|
|
28889
|
+
await closePromise;
|
|
28507
28890
|
}
|
|
28508
28891
|
async respawnIfKnown(sessionId) {
|
|
28509
28892
|
if (!this.enabled || !this.multiplexer)
|
|
28510
28893
|
return;
|
|
28511
|
-
|
|
28894
|
+
const closing = this.closingSessions.get(sessionId);
|
|
28895
|
+
if (closing)
|
|
28896
|
+
await closing;
|
|
28897
|
+
if (this.isTrackedOrSpawning(sessionId)) {
|
|
28512
28898
|
return;
|
|
28899
|
+
}
|
|
28513
28900
|
const known = this.knownSessions.get(sessionId);
|
|
28514
28901
|
if (!known)
|
|
28515
28902
|
return;
|
|
@@ -28523,8 +28910,9 @@ class MultiplexerSessionManager {
|
|
|
28523
28910
|
});
|
|
28524
28911
|
return;
|
|
28525
28912
|
}
|
|
28526
|
-
if (this.sessions.has(sessionId))
|
|
28913
|
+
if (this.sessions.has(sessionId) || this.closingSessions.has(sessionId)) {
|
|
28527
28914
|
return;
|
|
28915
|
+
}
|
|
28528
28916
|
log("[multiplexer-session-manager] child session busy again, respawning pane", {
|
|
28529
28917
|
sessionId,
|
|
28530
28918
|
parentId: known.parentId,
|
|
@@ -28538,6 +28926,14 @@ class MultiplexerSessionManager {
|
|
|
28538
28926
|
});
|
|
28539
28927
|
if (!paneResult.success || !paneResult.paneId)
|
|
28540
28928
|
return;
|
|
28929
|
+
if (!this.knownSessions.has(sessionId) || this.closingSessions.has(sessionId)) {
|
|
28930
|
+
await this.multiplexer.closePane(paneResult.paneId).catch((err) => log("[multiplexer-session-manager] closing stale respawned pane failed", {
|
|
28931
|
+
sessionId,
|
|
28932
|
+
paneId: paneResult.paneId,
|
|
28933
|
+
error: String(err)
|
|
28934
|
+
}));
|
|
28935
|
+
return;
|
|
28936
|
+
}
|
|
28541
28937
|
const now = Date.now();
|
|
28542
28938
|
this.sessions.set(sessionId, {
|
|
28543
28939
|
sessionId,
|
|
@@ -28560,8 +28956,21 @@ class MultiplexerSessionManager {
|
|
|
28560
28956
|
isTrackedOrSpawning(sessionId) {
|
|
28561
28957
|
return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
|
|
28562
28958
|
}
|
|
28959
|
+
updatePolling() {
|
|
28960
|
+
if (this.sessions.size > 0 || this.closingSessions.size > 0) {
|
|
28961
|
+
this.startPolling();
|
|
28962
|
+
} else {
|
|
28963
|
+
this.stopPolling();
|
|
28964
|
+
}
|
|
28965
|
+
}
|
|
28966
|
+
getSessionId(event) {
|
|
28967
|
+
return event.properties?.info?.id ?? event.properties?.sessionID;
|
|
28968
|
+
}
|
|
28563
28969
|
async cleanup() {
|
|
28564
28970
|
this.stopPolling();
|
|
28971
|
+
if (this.closingSessions.size > 0) {
|
|
28972
|
+
await Promise.all(this.closingSessions.values());
|
|
28973
|
+
}
|
|
28565
28974
|
if (this.sessions.size > 0 && this.multiplexer) {
|
|
28566
28975
|
log("[multiplexer-session-manager] closing all panes", {
|
|
28567
28976
|
count: this.sessions.size
|
|
@@ -28576,6 +28985,7 @@ class MultiplexerSessionManager {
|
|
|
28576
28985
|
}
|
|
28577
28986
|
this.knownSessions.clear();
|
|
28578
28987
|
this.spawningSessions.clear();
|
|
28988
|
+
this.closingSessions.clear();
|
|
28579
28989
|
log("[multiplexer-session-manager] cleanup complete");
|
|
28580
28990
|
}
|
|
28581
28991
|
}
|
|
@@ -28787,9 +29197,9 @@ function findSgCliPathSync() {
|
|
|
28787
29197
|
}
|
|
28788
29198
|
if (process.platform === "darwin") {
|
|
28789
29199
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
28790
|
-
for (const
|
|
28791
|
-
if (existsSync7(
|
|
28792
|
-
return
|
|
29200
|
+
for (const path13 of homebrewPaths) {
|
|
29201
|
+
if (existsSync7(path13) && isValidBinary(path13)) {
|
|
29202
|
+
return path13;
|
|
28793
29203
|
}
|
|
28794
29204
|
}
|
|
28795
29205
|
}
|
|
@@ -28806,8 +29216,8 @@ function getSgCliPath() {
|
|
|
28806
29216
|
}
|
|
28807
29217
|
return "sg";
|
|
28808
29218
|
}
|
|
28809
|
-
function setSgCliPath(
|
|
28810
|
-
resolvedCliPath =
|
|
29219
|
+
function setSgCliPath(path13) {
|
|
29220
|
+
resolvedCliPath = path13;
|
|
28811
29221
|
}
|
|
28812
29222
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
28813
29223
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -29215,7 +29625,7 @@ Returns the councillor responses with a summary footer.`,
|
|
|
29215
29625
|
// src/tools/preset-manager.ts
|
|
29216
29626
|
var COMMAND_NAME3 = "preset";
|
|
29217
29627
|
function createPresetManager(ctx, config) {
|
|
29218
|
-
let activePreset = config.preset ?? null;
|
|
29628
|
+
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
29219
29629
|
async function handleCommandExecuteBefore(input, output) {
|
|
29220
29630
|
if (input.command !== COMMAND_NAME3) {
|
|
29221
29631
|
return;
|
|
@@ -29256,21 +29666,41 @@ function createPresetManager(ctx, config) {
|
|
|
29256
29666
|
}
|
|
29257
29667
|
const agentUpdates = {};
|
|
29258
29668
|
for (const [agentName, override] of Object.entries(preset)) {
|
|
29669
|
+
const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
|
|
29259
29670
|
const agentConfig = mapOverrideToAgentConfig(override);
|
|
29260
29671
|
if (Object.keys(agentConfig).length > 0) {
|
|
29261
|
-
agentUpdates[
|
|
29672
|
+
agentUpdates[resolvedName] = agentConfig;
|
|
29262
29673
|
}
|
|
29263
29674
|
}
|
|
29264
|
-
|
|
29675
|
+
const currentRuntimePreset = getActiveRuntimePreset();
|
|
29676
|
+
const resetUpdates = {};
|
|
29677
|
+
if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
|
|
29678
|
+
const oldPreset = config.presets[currentRuntimePreset];
|
|
29679
|
+
for (const rawName of Object.keys(oldPreset)) {
|
|
29680
|
+
const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
|
|
29681
|
+
if (resolvedOld in agentUpdates)
|
|
29682
|
+
continue;
|
|
29683
|
+
const baseline = config.agents?.[resolvedOld];
|
|
29684
|
+
if (baseline) {
|
|
29685
|
+
resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
|
|
29686
|
+
}
|
|
29687
|
+
}
|
|
29688
|
+
}
|
|
29689
|
+
const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
|
|
29690
|
+
const allUpdates = { ...resetUpdates, ...agentUpdates };
|
|
29691
|
+
if (!hasAgentUpdates) {
|
|
29265
29692
|
output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
|
|
29266
29693
|
return;
|
|
29267
29694
|
}
|
|
29695
|
+
const previousPreset = activePreset;
|
|
29696
|
+
setActiveRuntimePresetWithPrevious(presetName);
|
|
29268
29697
|
try {
|
|
29269
29698
|
await ctx.client.config.update({
|
|
29270
|
-
body: { agent:
|
|
29699
|
+
body: { agent: allUpdates }
|
|
29271
29700
|
});
|
|
29272
29701
|
activePreset = presetName;
|
|
29273
|
-
const
|
|
29702
|
+
const summaryParts = [];
|
|
29703
|
+
for (const [name, cfg] of Object.entries(agentUpdates)) {
|
|
29274
29704
|
const parts = [name];
|
|
29275
29705
|
if (cfg.model)
|
|
29276
29706
|
parts.push(`model: ${cfg.model}`);
|
|
@@ -29280,12 +29710,16 @@ function createPresetManager(ctx, config) {
|
|
|
29280
29710
|
parts.push(`temp: ${cfg.temperature}`);
|
|
29281
29711
|
if (cfg.options)
|
|
29282
29712
|
parts.push("options: yes");
|
|
29283
|
-
|
|
29284
|
-
}
|
|
29285
|
-
|
|
29713
|
+
summaryParts.push(parts.join(" → "));
|
|
29714
|
+
}
|
|
29715
|
+
if (Object.keys(resetUpdates).length > 0) {
|
|
29716
|
+
summaryParts.push(`Reset to baseline: ${Object.keys(resetUpdates).join(", ")}`);
|
|
29717
|
+
}
|
|
29286
29718
|
output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
|
|
29287
|
-
${
|
|
29719
|
+
${summaryParts.join(`
|
|
29720
|
+
`)}`));
|
|
29288
29721
|
} catch (err) {
|
|
29722
|
+
rollbackRuntimePreset(previousPreset);
|
|
29289
29723
|
output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
|
|
29290
29724
|
}
|
|
29291
29725
|
}
|
|
@@ -29375,14 +29809,14 @@ var BINARY_PREFIXES = [
|
|
|
29375
29809
|
var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
|
|
29376
29810
|
// src/tools/smartfetch/tool.ts
|
|
29377
29811
|
import os4 from "node:os";
|
|
29378
|
-
import
|
|
29812
|
+
import path16 from "node:path";
|
|
29379
29813
|
import {
|
|
29380
29814
|
tool as tool4
|
|
29381
29815
|
} from "@opencode-ai/plugin";
|
|
29382
29816
|
|
|
29383
29817
|
// src/tools/smartfetch/binary.ts
|
|
29384
29818
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
29385
|
-
import
|
|
29819
|
+
import path13 from "node:path";
|
|
29386
29820
|
function extensionForMime(contentType) {
|
|
29387
29821
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
29388
29822
|
const map = {
|
|
@@ -29403,10 +29837,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
29403
29837
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
29404
29838
|
await mkdir2(binaryDir, { recursive: true });
|
|
29405
29839
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
29406
|
-
const parsed =
|
|
29840
|
+
const parsed = path13.parse(initialName);
|
|
29407
29841
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
29408
29842
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
29409
|
-
const file =
|
|
29843
|
+
const file = path13.join(binaryDir, candidateName);
|
|
29410
29844
|
try {
|
|
29411
29845
|
await writeFile2(file, data, { flag: "wx" });
|
|
29412
29846
|
return file;
|
|
@@ -30060,7 +30494,7 @@ var L = class u2 {
|
|
|
30060
30494
|
};
|
|
30061
30495
|
|
|
30062
30496
|
// src/tools/smartfetch/network.ts
|
|
30063
|
-
import
|
|
30497
|
+
import path14 from "node:path";
|
|
30064
30498
|
|
|
30065
30499
|
// src/tools/smartfetch/utils.ts
|
|
30066
30500
|
var import_readability = __toESM(require_readability(), 1);
|
|
@@ -30785,7 +31219,7 @@ function inferFilenameFromUrl(url) {
|
|
|
30785
31219
|
function truncateFilename(name, maxLength = 180) {
|
|
30786
31220
|
if (name.length <= maxLength)
|
|
30787
31221
|
return name;
|
|
30788
|
-
const parsed =
|
|
31222
|
+
const parsed = path14.parse(name);
|
|
30789
31223
|
const ext = parsed.ext || "";
|
|
30790
31224
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
30791
31225
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -30957,7 +31391,7 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
30957
31391
|
// src/tools/smartfetch/secondary-model.ts
|
|
30958
31392
|
import { existsSync as existsSync9 } from "node:fs";
|
|
30959
31393
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
30960
|
-
import
|
|
31394
|
+
import path15 from "node:path";
|
|
30961
31395
|
function parseModelRef(value) {
|
|
30962
31396
|
if (!value)
|
|
30963
31397
|
return;
|
|
@@ -30983,7 +31417,7 @@ function pickAgentModelRef(value) {
|
|
|
30983
31417
|
}
|
|
30984
31418
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
30985
31419
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
30986
|
-
const fullPath =
|
|
31420
|
+
const fullPath = path15.join(baseDir, file);
|
|
30987
31421
|
if (existsSync9(fullPath))
|
|
30988
31422
|
return fullPath;
|
|
30989
31423
|
}
|
|
@@ -31000,7 +31434,7 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
31000
31434
|
}
|
|
31001
31435
|
}
|
|
31002
31436
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
31003
|
-
const projectDir =
|
|
31437
|
+
const projectDir = path15.join(directory, ".opencode");
|
|
31004
31438
|
const userDirs = getConfigSearchDirs();
|
|
31005
31439
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
31006
31440
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -31161,7 +31595,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
31161
31595
|
// src/tools/smartfetch/tool.ts
|
|
31162
31596
|
var z5 = tool4.schema;
|
|
31163
31597
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
31164
|
-
const binaryDir = options.binaryDir ||
|
|
31598
|
+
const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
|
|
31165
31599
|
return tool4({
|
|
31166
31600
|
description: WEBFETCH_DESCRIPTION,
|
|
31167
31601
|
args: {
|
|
@@ -31685,6 +32119,16 @@ class SubagentDepthTracker {
|
|
|
31685
32119
|
|
|
31686
32120
|
// src/utils/system-collapse.ts
|
|
31687
32121
|
function collapseSystemInPlace(system2) {
|
|
32122
|
+
if (system2.length === 0) {
|
|
32123
|
+
return;
|
|
32124
|
+
}
|
|
32125
|
+
if (system2.length === 1) {
|
|
32126
|
+
if (system2[0]) {
|
|
32127
|
+
return;
|
|
32128
|
+
}
|
|
32129
|
+
system2.length = 0;
|
|
32130
|
+
return;
|
|
32131
|
+
}
|
|
31688
32132
|
const joined = system2.join(`
|
|
31689
32133
|
|
|
31690
32134
|
`);
|
|
@@ -31723,6 +32167,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31723
32167
|
const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
31724
32168
|
initLogger(sessionId);
|
|
31725
32169
|
let config;
|
|
32170
|
+
let disabledAgents;
|
|
31726
32171
|
let agentDefs;
|
|
31727
32172
|
let agents;
|
|
31728
32173
|
let mcps;
|
|
@@ -31748,9 +32193,20 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31748
32193
|
let presetManager;
|
|
31749
32194
|
let councilTools;
|
|
31750
32195
|
let webfetch;
|
|
32196
|
+
let rewriteDisplayNameMentions;
|
|
31751
32197
|
let toolCount = 0;
|
|
31752
32198
|
try {
|
|
31753
32199
|
config = loadPluginConfig(ctx.directory);
|
|
32200
|
+
const runtimePreset = getActiveRuntimePreset();
|
|
32201
|
+
if (runtimePreset && config.presets?.[runtimePreset]) {
|
|
32202
|
+
config.preset = runtimePreset;
|
|
32203
|
+
const presetAgents = config.presets[runtimePreset];
|
|
32204
|
+
config.agents = deepMerge(config.agents, presetAgents);
|
|
32205
|
+
} else if (runtimePreset) {
|
|
32206
|
+
setActiveRuntimePreset(null);
|
|
32207
|
+
}
|
|
32208
|
+
disabledAgents = getDisabledAgents(config);
|
|
32209
|
+
rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
|
|
31754
32210
|
agentDefs = createAgents(config);
|
|
31755
32211
|
agents = getAgentConfigs(config);
|
|
31756
32212
|
modelArrayMap = {};
|
|
@@ -31787,7 +32243,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31787
32243
|
main_pane_size: config.multiplexer?.main_pane_size ?? 60
|
|
31788
32244
|
};
|
|
31789
32245
|
const multiplexer = getMultiplexer(multiplexerConfig);
|
|
31790
|
-
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
|
|
32246
|
+
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null && multiplexer.isInsideSession();
|
|
31791
32247
|
log("[plugin] initialized with multiplexer config", {
|
|
31792
32248
|
multiplexerConfig,
|
|
31793
32249
|
enabled: multiplexerEnabled,
|
|
@@ -31824,6 +32280,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31824
32280
|
});
|
|
31825
32281
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
31826
32282
|
maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
|
|
32283
|
+
readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
|
|
32284
|
+
readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
|
|
31827
32285
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
31828
32286
|
});
|
|
31829
32287
|
interviewManager = createInterviewManager(ctx, config);
|
|
@@ -31943,6 +32401,83 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31943
32401
|
});
|
|
31944
32402
|
}
|
|
31945
32403
|
}
|
|
32404
|
+
const runtimePresetName = getActiveRuntimePreset();
|
|
32405
|
+
if (runtimePresetName && config.presets?.[runtimePresetName]) {
|
|
32406
|
+
const runtimePreset = config.presets[runtimePresetName];
|
|
32407
|
+
for (const [agentName, override] of Object.entries(runtimePreset)) {
|
|
32408
|
+
const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
|
|
32409
|
+
const entry = configAgent[resolvedName];
|
|
32410
|
+
if (!entry)
|
|
32411
|
+
continue;
|
|
32412
|
+
if (typeof override.model === "string") {
|
|
32413
|
+
entry.model = override.model;
|
|
32414
|
+
} else if (Array.isArray(override.model) && override.model.length > 0) {
|
|
32415
|
+
const first = override.model[0];
|
|
32416
|
+
entry.model = typeof first === "string" ? first : first.id;
|
|
32417
|
+
if (typeof first !== "string" && first.variant) {
|
|
32418
|
+
entry.variant = first.variant;
|
|
32419
|
+
}
|
|
32420
|
+
}
|
|
32421
|
+
if (typeof override.variant === "string") {
|
|
32422
|
+
entry.variant = override.variant;
|
|
32423
|
+
} else if ("variant" in override) {
|
|
32424
|
+
delete entry.variant;
|
|
32425
|
+
}
|
|
32426
|
+
if (typeof override.temperature === "number") {
|
|
32427
|
+
entry.temperature = override.temperature;
|
|
32428
|
+
} else if ("temperature" in override) {
|
|
32429
|
+
delete entry.temperature;
|
|
32430
|
+
}
|
|
32431
|
+
if (override.options && typeof override.options === "object" && !Array.isArray(override.options)) {
|
|
32432
|
+
entry.options = override.options;
|
|
32433
|
+
} else if ("options" in override) {
|
|
32434
|
+
delete entry.options;
|
|
32435
|
+
}
|
|
32436
|
+
log("[plugin] runtime preset override", {
|
|
32437
|
+
preset: runtimePresetName,
|
|
32438
|
+
agent: agentName,
|
|
32439
|
+
model: entry.model
|
|
32440
|
+
});
|
|
32441
|
+
}
|
|
32442
|
+
const prevPresetName = getPreviousRuntimePreset();
|
|
32443
|
+
if (prevPresetName && config.presets?.[prevPresetName]) {
|
|
32444
|
+
const prevPreset = config.presets[prevPresetName];
|
|
32445
|
+
const newPresetResolved = new Set(Object.keys(runtimePreset).map((k) => AGENT_ALIASES[k] ?? k));
|
|
32446
|
+
for (const agentName of Object.keys(prevPreset)) {
|
|
32447
|
+
const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
|
|
32448
|
+
if (newPresetResolved.has(resolvedName))
|
|
32449
|
+
continue;
|
|
32450
|
+
const entry = configAgent[resolvedName];
|
|
32451
|
+
if (!entry)
|
|
32452
|
+
continue;
|
|
32453
|
+
const baseline = config.agents?.[resolvedName];
|
|
32454
|
+
const prevOverride = prevPreset[agentName];
|
|
32455
|
+
if (typeof baseline?.model === "string") {
|
|
32456
|
+
entry.model = baseline.model;
|
|
32457
|
+
}
|
|
32458
|
+
if (typeof baseline?.variant === "string") {
|
|
32459
|
+
entry.variant = baseline.variant;
|
|
32460
|
+
} else if (prevOverride && "variant" in prevOverride) {
|
|
32461
|
+
delete entry.variant;
|
|
32462
|
+
}
|
|
32463
|
+
if (typeof baseline?.temperature === "number") {
|
|
32464
|
+
entry.temperature = baseline.temperature;
|
|
32465
|
+
} else if (prevOverride && "temperature" in prevOverride) {
|
|
32466
|
+
delete entry.temperature;
|
|
32467
|
+
}
|
|
32468
|
+
if (baseline?.options && typeof baseline.options === "object" && !Array.isArray(baseline.options)) {
|
|
32469
|
+
entry.options = baseline.options;
|
|
32470
|
+
} else if (prevOverride && "options" in prevOverride) {
|
|
32471
|
+
delete entry.options;
|
|
32472
|
+
}
|
|
32473
|
+
log("[plugin] runtime preset reset from previous", {
|
|
32474
|
+
previousPreset: prevPresetName,
|
|
32475
|
+
agent: resolvedName,
|
|
32476
|
+
model: entry.model
|
|
32477
|
+
});
|
|
32478
|
+
}
|
|
32479
|
+
}
|
|
32480
|
+
}
|
|
31946
32481
|
const configMcp = opencodeConfig.mcp;
|
|
31947
32482
|
if (!configMcp) {
|
|
31948
32483
|
opencodeConfig.mcp = { ...mcps };
|
|
@@ -32000,7 +32535,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32000
32535
|
await multiplexerSessionManager.onSessionStatus(event);
|
|
32001
32536
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
32002
32537
|
await interviewManager.handleEvent(input);
|
|
32003
|
-
await postFileToolNudgeHook.event(input);
|
|
32004
32538
|
await taskSessionManagerHook.event(input);
|
|
32005
32539
|
if (input.event.type === "session.deleted") {
|
|
32006
32540
|
const props = input.event.properties;
|
|
@@ -32043,15 +32577,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32043
32577
|
const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
|
|
32044
32578
|
if (!alreadyInjected) {
|
|
32045
32579
|
const orchestratorDef = agentDefs.find((a) => a.name === "orchestrator");
|
|
32046
|
-
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(
|
|
32580
|
+
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
|
|
32047
32581
|
output.system[0] = orchestratorPrompt + (output.system[0] ? `
|
|
32048
32582
|
|
|
32049
32583
|
${output.system[0]}` : "");
|
|
32050
32584
|
}
|
|
32051
32585
|
}
|
|
32052
|
-
await todoContinuationHook.handleChatSystemTransform(input, output);
|
|
32053
|
-
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
32054
|
-
await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
|
|
32055
32586
|
collapseSystemInPlace(output.system);
|
|
32056
32587
|
},
|
|
32057
32588
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
@@ -32064,25 +32595,26 @@ ${output.system[0]}` : "");
|
|
|
32064
32595
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
32065
32596
|
continue;
|
|
32066
32597
|
}
|
|
32067
|
-
part.text = rewriteDisplayNameMentions(
|
|
32598
|
+
part.text = rewriteDisplayNameMentions(part.text);
|
|
32068
32599
|
}
|
|
32069
32600
|
}
|
|
32070
32601
|
processImageAttachments({
|
|
32071
32602
|
messages: typedOutput.messages,
|
|
32072
32603
|
workDir: ctx.directory,
|
|
32073
|
-
disabledAgents
|
|
32604
|
+
disabledAgents,
|
|
32074
32605
|
log
|
|
32075
32606
|
});
|
|
32076
32607
|
await todoContinuationHook.handleMessagesTransform({
|
|
32077
32608
|
messages: typedOutput.messages
|
|
32078
32609
|
});
|
|
32610
|
+
await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
32079
32611
|
await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
32080
32612
|
await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
32081
32613
|
},
|
|
32082
32614
|
"tool.execute.after": async (input, output) => {
|
|
32083
32615
|
await delegateTaskRetryHook["tool.execute.after"](input, output);
|
|
32084
32616
|
await jsonErrorRecoveryHook["tool.execute.after"](input, output);
|
|
32085
|
-
await todoContinuationHook.handleToolExecuteAfter(input);
|
|
32617
|
+
await todoContinuationHook.handleToolExecuteAfter(input, output);
|
|
32086
32618
|
await postFileToolNudgeHook["tool.execute.after"](input, output);
|
|
32087
32619
|
await taskSessionManagerHook["tool.execute.after"](input, output);
|
|
32088
32620
|
}
|