@andyqiu/codeforge 0.8.17 → 0.8.19
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/bin/codeforge.mjs +16 -1
- package/dist/index.js +2159 -1457
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26679,6 +26679,111 @@ function withTimeout2(p, ms, label, signal, hbOpts) {
|
|
|
26679
26679
|
});
|
|
26680
26680
|
}
|
|
26681
26681
|
|
|
26682
|
+
// lib/merge-timeline.ts
|
|
26683
|
+
var STATE_LABEL = {
|
|
26684
|
+
pre_check: "\uD83D\uDD0D 前置检查",
|
|
26685
|
+
approval_pre_check: "\uD83D\uDD0D approval 预检",
|
|
26686
|
+
dispatch_review: "\uD83D\uDC40 reviewer 审查",
|
|
26687
|
+
collect_decision: "\uD83D\uDCCB 收集决策",
|
|
26688
|
+
dispatch_coder: "\uD83D\uDEE0 coder 修改",
|
|
26689
|
+
wait_coder: "⏳ 等待 coder",
|
|
26690
|
+
block_pause: "⛔ 阻塞",
|
|
26691
|
+
do_merge: "\uD83D\uDD00 squash merge"
|
|
26692
|
+
};
|
|
26693
|
+
var ROUND_STATES = new Set([
|
|
26694
|
+
"dispatch_review",
|
|
26695
|
+
"collect_decision",
|
|
26696
|
+
"dispatch_coder",
|
|
26697
|
+
"wait_coder"
|
|
26698
|
+
]);
|
|
26699
|
+
function formatDuration(ms) {
|
|
26700
|
+
let v = ms;
|
|
26701
|
+
if (!Number.isFinite(v) || v < 0)
|
|
26702
|
+
v = 0;
|
|
26703
|
+
const totalSec = Math.round(v / 1000);
|
|
26704
|
+
if (totalSec < 60) {
|
|
26705
|
+
return `${(v / 1000).toFixed(1)}s`;
|
|
26706
|
+
}
|
|
26707
|
+
const m = Math.floor(totalSec / 60);
|
|
26708
|
+
const s = totalSec % 60;
|
|
26709
|
+
return `${m}m${String(s).padStart(2, "0")}s`;
|
|
26710
|
+
}
|
|
26711
|
+
function createMergeTimeline(opts = {}) {
|
|
26712
|
+
const now = opts.now ?? Date.now;
|
|
26713
|
+
const segs = [];
|
|
26714
|
+
let currentRound = 0;
|
|
26715
|
+
function liveTitle(seg, atMs) {
|
|
26716
|
+
const label = STATE_LABEL[seg.state] ?? seg.state;
|
|
26717
|
+
const elapsed = formatDuration(atMs - seg.startMs);
|
|
26718
|
+
const roundSuffix = seg.round !== undefined ? ` · 第 ${seg.round} 轮` : "";
|
|
26719
|
+
return `${label}…(${elapsed})${roundSuffix}`;
|
|
26720
|
+
}
|
|
26721
|
+
return {
|
|
26722
|
+
record(state, detail) {
|
|
26723
|
+
try {
|
|
26724
|
+
const t = now();
|
|
26725
|
+
const open = segs.length > 0 ? segs[segs.length - 1] : undefined;
|
|
26726
|
+
if (open && open.endMs === undefined && open.state === state) {
|
|
26727
|
+
open.detail = detail;
|
|
26728
|
+
return liveTitle(open, t);
|
|
26729
|
+
}
|
|
26730
|
+
if (open && open.endMs === undefined) {
|
|
26731
|
+
open.endMs = t;
|
|
26732
|
+
}
|
|
26733
|
+
if (state === "dispatch_review") {
|
|
26734
|
+
currentRound += 1;
|
|
26735
|
+
}
|
|
26736
|
+
const seg = {
|
|
26737
|
+
state,
|
|
26738
|
+
detail,
|
|
26739
|
+
startMs: t,
|
|
26740
|
+
...ROUND_STATES.has(state) && currentRound > 0 ? { round: currentRound } : {}
|
|
26741
|
+
};
|
|
26742
|
+
segs.push(seg);
|
|
26743
|
+
return liveTitle(seg, t);
|
|
26744
|
+
} catch {
|
|
26745
|
+
return "";
|
|
26746
|
+
}
|
|
26747
|
+
},
|
|
26748
|
+
finish() {
|
|
26749
|
+
const t = now();
|
|
26750
|
+
const open = segs.length > 0 ? segs[segs.length - 1] : undefined;
|
|
26751
|
+
if (open && open.endMs === undefined) {
|
|
26752
|
+
open.endMs = t;
|
|
26753
|
+
}
|
|
26754
|
+
},
|
|
26755
|
+
totalMs() {
|
|
26756
|
+
let sum = 0;
|
|
26757
|
+
for (const s of segs) {
|
|
26758
|
+
if (s.endMs !== undefined)
|
|
26759
|
+
sum += s.endMs - s.startMs;
|
|
26760
|
+
}
|
|
26761
|
+
return sum;
|
|
26762
|
+
},
|
|
26763
|
+
segments() {
|
|
26764
|
+
return segs.map((s) => ({ ...s }));
|
|
26765
|
+
},
|
|
26766
|
+
renderSummary() {
|
|
26767
|
+
if (segs.length === 0) {
|
|
26768
|
+
return "⏱ 耗时明细:(无阶段记录)";
|
|
26769
|
+
}
|
|
26770
|
+
const rows = [];
|
|
26771
|
+
for (const s of segs) {
|
|
26772
|
+
const label = STATE_LABEL[s.state] ?? s.state;
|
|
26773
|
+
const roundSuffix = s.round !== undefined ? ` · 第 ${s.round} 轮` : "";
|
|
26774
|
+
const durMs = s.endMs !== undefined ? s.endMs - s.startMs : 0;
|
|
26775
|
+
rows.push({ label: `${label}${roundSuffix}`, dur: formatDuration(durMs) });
|
|
26776
|
+
}
|
|
26777
|
+
const labelWidth = Math.max(...rows.map((r) => r.label.length), 4);
|
|
26778
|
+
const lines = rows.map((r) => ` ${r.label.padEnd(labelWidth)} ${r.dur}`);
|
|
26779
|
+
const total = formatDuration(this.totalMs());
|
|
26780
|
+
const sep3 = " " + "─".repeat(labelWidth + 8);
|
|
26781
|
+
return ["⏱ 耗时明细:", ...lines, sep3, ` ${"总计".padEnd(labelWidth)} ${total}`].join(`
|
|
26782
|
+
`);
|
|
26783
|
+
}
|
|
26784
|
+
};
|
|
26785
|
+
}
|
|
26786
|
+
|
|
26682
26787
|
// tools/session-merge.ts
|
|
26683
26788
|
var description12 = [
|
|
26684
26789
|
"合并当前 session 绑定的 worktree 改动到主工作区(squash + review-fix-review 闭环)。",
|
|
@@ -26939,6 +27044,8 @@ async function execute12(input) {
|
|
|
26939
27044
|
}
|
|
26940
27045
|
const mergeArgs = args;
|
|
26941
27046
|
const sendProgress = _ctx.sendProgress;
|
|
27047
|
+
const setTitle = _ctx.setTitle;
|
|
27048
|
+
const timeline = createMergeTimeline();
|
|
26942
27049
|
const result = await runMergeLoop({
|
|
26943
27050
|
sessionId: mergeSessionId,
|
|
26944
27051
|
mainRoot,
|
|
@@ -26947,14 +27054,26 @@ async function execute12(input) {
|
|
|
26947
27054
|
...mergeArgs.summary ? { summary: mergeArgs.summary } : {},
|
|
26948
27055
|
..._ctx.planStore ? { planStore: _ctx.planStore } : {},
|
|
26949
27056
|
spawner: _ctx.spawner,
|
|
26950
|
-
|
|
26951
|
-
|
|
27057
|
+
onProgress: (state, detail) => {
|
|
27058
|
+
const title = timeline.record(state, detail);
|
|
27059
|
+
if (setTitle && title) {
|
|
27060
|
+
try {
|
|
27061
|
+
setTitle(title);
|
|
27062
|
+
} catch {}
|
|
27063
|
+
}
|
|
27064
|
+
if (sendProgress) {
|
|
26952
27065
|
Promise.resolve(sendProgress(state, detail)).catch(() => {});
|
|
26953
27066
|
}
|
|
26954
|
-
}
|
|
27067
|
+
},
|
|
26955
27068
|
..._ctx.__testHooks ? { __testHooks: _ctx.__testHooks } : {}
|
|
26956
27069
|
});
|
|
26957
|
-
|
|
27070
|
+
timeline.finish();
|
|
27071
|
+
return {
|
|
27072
|
+
ok: true,
|
|
27073
|
+
action: "merge",
|
|
27074
|
+
data: result,
|
|
27075
|
+
timeline: { text: timeline.renderSummary(), totalMs: timeline.totalMs() }
|
|
27076
|
+
};
|
|
26958
27077
|
} catch (err) {
|
|
26959
27078
|
return {
|
|
26960
27079
|
ok: false,
|
|
@@ -27910,6 +28029,10 @@ function classifyError(err) {
|
|
|
27910
28029
|
}
|
|
27911
28030
|
if (/\b(5\d{2}|529|429)\b/.test(msg))
|
|
27912
28031
|
return "retryable";
|
|
28032
|
+
if (/model.*(not.*(found|supported)|is.*not.*available)/i.test(msg))
|
|
28033
|
+
return "switch_model";
|
|
28034
|
+
if (/api.?key.*(not.*configured|missing)/i.test(msg))
|
|
28035
|
+
return "switch_model";
|
|
27913
28036
|
if (errName === "ProviderAuthError" || errName === "ContextOverflowError")
|
|
27914
28037
|
return "fatal";
|
|
27915
28038
|
if (/\b4[0-9]{2}\b/.test(msg) && !/\b429\b/.test(msg))
|
|
@@ -27970,6 +28093,8 @@ async function withLlmRetry(fn, opts = {}) {
|
|
|
27970
28093
|
const startedAt = Date.now();
|
|
27971
28094
|
let lastError;
|
|
27972
28095
|
let attempt = 0;
|
|
28096
|
+
let pendingModel;
|
|
28097
|
+
let activeModel = opts.currentModel ?? "";
|
|
27973
28098
|
while (attempt < maxAttempts) {
|
|
27974
28099
|
if (attempt > 0 && opts.signal?.aborted) {
|
|
27975
28100
|
const e = new Error("withLlmRetry: aborted by signal");
|
|
@@ -27988,11 +28113,24 @@ async function withLlmRetry(fn, opts = {}) {
|
|
|
27988
28113
|
}
|
|
27989
28114
|
attempt++;
|
|
27990
28115
|
try {
|
|
27991
|
-
const result = await fn();
|
|
28116
|
+
const result = await fn({ model: pendingModel });
|
|
27992
28117
|
if (opts.isRetryableResult) {
|
|
27993
28118
|
const failErr = opts.isRetryableResult(result);
|
|
27994
28119
|
if (failErr) {
|
|
27995
28120
|
const cls = classifyError(failErr);
|
|
28121
|
+
if (cls === "switch_model") {
|
|
28122
|
+
const nextModel = opts.resolveModelForAttempt?.(activeModel);
|
|
28123
|
+
if (nextModel != null && attempt < maxAttempts) {
|
|
28124
|
+
activeModel = nextModel;
|
|
28125
|
+
pendingModel = nextModel;
|
|
28126
|
+
lastError = failErr;
|
|
28127
|
+
const delayMs = await handleRetryDelay(failErr, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
|
|
28128
|
+
if (delayMs < 0)
|
|
28129
|
+
throw lastError;
|
|
28130
|
+
continue;
|
|
28131
|
+
}
|
|
28132
|
+
throw failErr;
|
|
28133
|
+
}
|
|
27996
28134
|
if (cls === "retryable" && attempt < maxAttempts) {
|
|
27997
28135
|
lastError = failErr;
|
|
27998
28136
|
const delayMs = await handleRetryDelay(failErr, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
|
|
@@ -28008,6 +28146,22 @@ async function withLlmRetry(fn, opts = {}) {
|
|
|
28008
28146
|
if (err.name === "AbortError")
|
|
28009
28147
|
throw err;
|
|
28010
28148
|
const cls = classifyError(err);
|
|
28149
|
+
if (cls === "switch_model") {
|
|
28150
|
+
const nextModel = opts.resolveModelForAttempt?.(activeModel);
|
|
28151
|
+
if (nextModel != null && attempt < maxAttempts) {
|
|
28152
|
+
activeModel = nextModel;
|
|
28153
|
+
pendingModel = nextModel;
|
|
28154
|
+
lastError = err;
|
|
28155
|
+
const delayMs2 = await handleRetryDelay(err, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
|
|
28156
|
+
if (delayMs2 < 0) {
|
|
28157
|
+
const e = new Error("withLlmRetry: aborted during retry sleep");
|
|
28158
|
+
e.name = "AbortError";
|
|
28159
|
+
throw e;
|
|
28160
|
+
}
|
|
28161
|
+
continue;
|
|
28162
|
+
}
|
|
28163
|
+
throw err;
|
|
28164
|
+
}
|
|
28011
28165
|
if (cls === "fatal" || attempt >= maxAttempts) {
|
|
28012
28166
|
throw err;
|
|
28013
28167
|
}
|
|
@@ -29096,13 +29250,23 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
29096
29250
|
if (!v.ok)
|
|
29097
29251
|
return wrap(JSON.parse(v.output));
|
|
29098
29252
|
const sid = input.sessionID;
|
|
29253
|
+
const tctx = input;
|
|
29099
29254
|
__setContext({
|
|
29100
29255
|
mainRoot: ctx.directory ?? process.cwd(),
|
|
29101
29256
|
spawner,
|
|
29102
29257
|
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? "",
|
|
29103
29258
|
...sid ? { currentSessionId: sid } : {},
|
|
29259
|
+
...typeof tctx.metadata === "function" ? {
|
|
29260
|
+
setTitle: (title) => {
|
|
29261
|
+
try {
|
|
29262
|
+
tctx.metadata({ title });
|
|
29263
|
+
} catch {}
|
|
29264
|
+
}
|
|
29265
|
+
} : {},
|
|
29104
29266
|
...sid ? {
|
|
29105
29267
|
sendProgress: async (state, detail) => {
|
|
29268
|
+
if (state !== "block_pause")
|
|
29269
|
+
return;
|
|
29106
29270
|
const client = ctx.client;
|
|
29107
29271
|
if (typeof client?.tui?.showToast === "function") {
|
|
29108
29272
|
await client.tui.showToast({
|
|
@@ -29117,6 +29281,10 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
29117
29281
|
title: `session_merge: ${args.action}`,
|
|
29118
29282
|
action: args.action
|
|
29119
29283
|
};
|
|
29284
|
+
const rWithTimeline = result;
|
|
29285
|
+
if (args.action === "merge" && rWithTimeline.timeline?.text) {
|
|
29286
|
+
meta["description"] = rWithTimeline.timeline.text;
|
|
29287
|
+
}
|
|
29120
29288
|
return wrap(result, meta);
|
|
29121
29289
|
});
|
|
29122
29290
|
}
|
|
@@ -30119,1607 +30287,2099 @@ var memoriesContextServer = async (ctx) => {
|
|
|
30119
30287
|
};
|
|
30120
30288
|
var handler9 = memoriesContextServer;
|
|
30121
30289
|
|
|
30122
|
-
//
|
|
30123
|
-
|
|
30124
|
-
|
|
30125
|
-
|
|
30126
|
-
|
|
30127
|
-
|
|
30128
|
-
|
|
30129
|
-
|
|
30130
|
-
|
|
30131
|
-
|
|
30132
|
-
|
|
30133
|
-
|
|
30134
|
-
|
|
30135
|
-
|
|
30136
|
-
|
|
30137
|
-
|
|
30138
|
-
|
|
30139
|
-
|
|
30140
|
-
|
|
30141
|
-
|
|
30142
|
-
|
|
30143
|
-
|
|
30144
|
-
|
|
30290
|
+
// lib/fallback-state.ts
|
|
30291
|
+
function createFallbackState(opts) {
|
|
30292
|
+
const now = opts.now ?? Date.now();
|
|
30293
|
+
return {
|
|
30294
|
+
sessionID: opts.sessionID,
|
|
30295
|
+
phase: "pending_output",
|
|
30296
|
+
originalModel: opts.model,
|
|
30297
|
+
currentModel: opts.model,
|
|
30298
|
+
pendingModel: null,
|
|
30299
|
+
fallbackIndex: 0,
|
|
30300
|
+
failedModels: new Map,
|
|
30301
|
+
attemptCount: 0,
|
|
30302
|
+
lastActiveAt: now,
|
|
30303
|
+
lastReplayAt: 0,
|
|
30304
|
+
inflightReplay: false,
|
|
30305
|
+
agent: opts.agent,
|
|
30306
|
+
directory: opts.directory ?? null,
|
|
30307
|
+
awaitingFirstAssistantOutput: true,
|
|
30308
|
+
lastDispatchedAt: now
|
|
30309
|
+
};
|
|
30310
|
+
}
|
|
30311
|
+
function markDispatched(map2, opts) {
|
|
30312
|
+
const now = opts.now ?? Date.now();
|
|
30313
|
+
const existing = map2.get(opts.sessionID);
|
|
30314
|
+
if (!existing) {
|
|
30315
|
+
const s = createFallbackState({ ...opts, now });
|
|
30316
|
+
map2.set(opts.sessionID, s);
|
|
30317
|
+
return s;
|
|
30145
30318
|
}
|
|
30146
|
-
|
|
30147
|
-
|
|
30148
|
-
|
|
30149
|
-
|
|
30150
|
-
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
|
|
30154
|
-
|
|
30155
|
-
|
|
30156
|
-
|
|
30157
|
-
|
|
30158
|
-
|
|
30159
|
-
|
|
30160
|
-
|
|
30161
|
-
}
|
|
30162
|
-
function buildFallbackMeta(cfg, agent, currentModel) {
|
|
30163
|
-
const r = resolveAgentModel(cfg, agent);
|
|
30164
|
-
if (!r)
|
|
30319
|
+
existing.agent = opts.agent;
|
|
30320
|
+
existing.directory = opts.directory ?? existing.directory ?? null;
|
|
30321
|
+
existing.phase = "pending_output";
|
|
30322
|
+
existing.awaitingFirstAssistantOutput = true;
|
|
30323
|
+
existing.lastDispatchedAt = now;
|
|
30324
|
+
existing.lastActiveAt = now;
|
|
30325
|
+
return existing;
|
|
30326
|
+
}
|
|
30327
|
+
function markFirstOutput(state, now) {
|
|
30328
|
+
state.awaitingFirstAssistantOutput = false;
|
|
30329
|
+
state.lastActiveAt = now ?? Date.now();
|
|
30330
|
+
}
|
|
30331
|
+
var CONTEXT_OVERFLOW_RE = /context.*length|context.*window|too.*many.*token|maximum.*context/i;
|
|
30332
|
+
function planFallback(state, chain, opts) {
|
|
30333
|
+
if (state.attemptCount >= opts.maxAttempts)
|
|
30165
30334
|
return null;
|
|
30166
|
-
|
|
30167
|
-
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
|
|
30171
|
-
|
|
30172
|
-
|
|
30173
|
-
|
|
30174
|
-
|
|
30175
|
-
|
|
30335
|
+
if (!Array.isArray(chain) || chain.length === 0)
|
|
30336
|
+
return null;
|
|
30337
|
+
const idx = chain.indexOf(state.currentModel);
|
|
30338
|
+
const startFrom = idx >= 0 ? idx + 1 : 0;
|
|
30339
|
+
let nextModel = null;
|
|
30340
|
+
for (let i = startFrom;i < chain.length; i++) {
|
|
30341
|
+
const cand = chain[i];
|
|
30342
|
+
if (!cand)
|
|
30343
|
+
continue;
|
|
30344
|
+
if (cand === state.currentModel)
|
|
30345
|
+
continue;
|
|
30346
|
+
if (state.failedModels.has(cand))
|
|
30347
|
+
continue;
|
|
30348
|
+
nextModel = cand;
|
|
30349
|
+
break;
|
|
30350
|
+
}
|
|
30351
|
+
if (!nextModel)
|
|
30352
|
+
return null;
|
|
30353
|
+
const isOverflow = opts.errorMessage ? CONTEXT_OVERFLOW_RE.test(opts.errorMessage) : false;
|
|
30354
|
+
let tier = 1;
|
|
30355
|
+
if (isOverflow) {
|
|
30356
|
+
tier = Math.min(3, state.attemptCount + 1);
|
|
30357
|
+
}
|
|
30358
|
+
const reason = isOverflow ? "context_overflow" : "model_error";
|
|
30359
|
+
return { nextModel, tier, reason };
|
|
30360
|
+
}
|
|
30361
|
+
function beginPlannedFallback(state, plan, now) {
|
|
30362
|
+
const t = now ?? Date.now();
|
|
30363
|
+
state.phase = "planned_fallback";
|
|
30364
|
+
state.pendingModel = plan.nextModel;
|
|
30365
|
+
state.awaitingFirstAssistantOutput = false;
|
|
30366
|
+
state.attemptCount += 1;
|
|
30367
|
+
state.lastReplayAt = t;
|
|
30368
|
+
state.lastActiveAt = t;
|
|
30369
|
+
}
|
|
30370
|
+
function beginReplaying(state, now) {
|
|
30371
|
+
state.phase = "replaying";
|
|
30372
|
+
state.inflightReplay = true;
|
|
30373
|
+
state.lastActiveAt = now ?? Date.now();
|
|
30374
|
+
}
|
|
30375
|
+
function commitFallback(state, plan, now) {
|
|
30376
|
+
const t = now ?? Date.now();
|
|
30377
|
+
if (state.pendingModel === null)
|
|
30378
|
+
return false;
|
|
30379
|
+
const target = state.pendingModel;
|
|
30380
|
+
state.failedModels.set(state.currentModel, t);
|
|
30381
|
+
state.currentModel = target;
|
|
30382
|
+
state.pendingModel = null;
|
|
30383
|
+
state.inflightReplay = false;
|
|
30384
|
+
state.phase = "committed";
|
|
30385
|
+
state.fallbackIndex += 1;
|
|
30386
|
+
state.lastActiveAt = t;
|
|
30387
|
+
return true;
|
|
30176
30388
|
}
|
|
30177
|
-
function
|
|
30178
|
-
const
|
|
30179
|
-
|
|
30180
|
-
|
|
30181
|
-
return `${head}${reason}
|
|
30182
|
-
fallback 链已用尽:${meta.chain.join(" → ")}`;
|
|
30389
|
+
function rollbackFallback(state, plan, opts) {
|
|
30390
|
+
const t = opts.now ?? Date.now();
|
|
30391
|
+
if (!opts.abortFailed) {
|
|
30392
|
+
state.failedModels.set(plan.nextModel, t);
|
|
30183
30393
|
}
|
|
30184
|
-
|
|
30185
|
-
|
|
30186
|
-
|
|
30394
|
+
state.pendingModel = null;
|
|
30395
|
+
state.currentModel = state.originalModel;
|
|
30396
|
+
state.inflightReplay = false;
|
|
30397
|
+
state.phase = "rolled_back";
|
|
30398
|
+
state.lastActiveAt = t;
|
|
30187
30399
|
}
|
|
30188
|
-
|
|
30189
|
-
const
|
|
30190
|
-
|
|
30191
|
-
|
|
30192
|
-
|
|
30193
|
-
|
|
30194
|
-
|
|
30195
|
-
|
|
30196
|
-
|
|
30197
|
-
|
|
30198
|
-
|
|
30199
|
-
|
|
30200
|
-
|
|
30201
|
-
|
|
30202
|
-
|
|
30203
|
-
|
|
30204
|
-
|
|
30205
|
-
|
|
30206
|
-
|
|
30207
|
-
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
if (!providerID || !modelID)
|
|
30211
|
-
return;
|
|
30212
|
-
const currentModel = `${providerID}/${modelID}`;
|
|
30213
|
-
const meta = buildFallbackMeta(state.config, agent, currentModel);
|
|
30214
|
-
if (!meta)
|
|
30215
|
-
return;
|
|
30216
|
-
output.options = output.options ?? {};
|
|
30217
|
-
output.options.codeforge_fallback = {
|
|
30218
|
-
chain: meta.chain,
|
|
30219
|
-
next: meta.next_fallback,
|
|
30220
|
-
source: meta.source
|
|
30221
|
-
};
|
|
30222
|
-
safeWriteLog(PLUGIN_NAME10, {
|
|
30223
|
-
hook: "chat.params",
|
|
30224
|
-
agent,
|
|
30225
|
-
model: currentModel,
|
|
30226
|
-
chain: meta.chain,
|
|
30227
|
-
next: meta.next_fallback
|
|
30228
|
-
});
|
|
30229
|
-
});
|
|
30230
|
-
},
|
|
30231
|
-
event: async ({ event }) => {
|
|
30232
|
-
await safeAsync(PLUGIN_NAME10, "event", async () => {
|
|
30233
|
-
if (!state.config)
|
|
30234
|
-
return;
|
|
30235
|
-
const e = event;
|
|
30236
|
-
if (!e || typeof e.type !== "string")
|
|
30237
|
-
return;
|
|
30238
|
-
if (!/error|failed|unavailable/i.test(e.type))
|
|
30239
|
-
return;
|
|
30240
|
-
const props = e.properties ?? {};
|
|
30241
|
-
const message = typeof props.error === "string" && props.error || typeof props.message === "string" && props.message || typeof props.error?.message === "string" && props.error.message || "";
|
|
30242
|
-
if (!looksLikeModelError(String(message)))
|
|
30243
|
-
return;
|
|
30244
|
-
const agent = props.agent ?? "unknown";
|
|
30245
|
-
const model = props.model ?? "unknown/unknown";
|
|
30246
|
-
const meta = buildFallbackMeta(state.config, agent, model);
|
|
30247
|
-
const suggestion = meta ? buildSuggestion(meta, String(message)) : `⚠️ ${agent}/${model} 失败:${message}`;
|
|
30248
|
-
log7.warn(`[${PLUGIN_NAME10}] ${suggestion}`);
|
|
30249
|
-
safeWriteLog(PLUGIN_NAME10, {
|
|
30250
|
-
hook: "event.error",
|
|
30251
|
-
eventType: e.type,
|
|
30252
|
-
agent,
|
|
30253
|
-
model,
|
|
30254
|
-
suggestion,
|
|
30255
|
-
chain: meta?.chain
|
|
30256
|
-
});
|
|
30257
|
-
});
|
|
30400
|
+
function recoverToOriginal(state, cooldownSec, now) {
|
|
30401
|
+
const t = now ?? Date.now();
|
|
30402
|
+
if (state.currentModel === state.originalModel)
|
|
30403
|
+
return false;
|
|
30404
|
+
const failedAt = state.failedModels.get(state.originalModel);
|
|
30405
|
+
if (failedAt === undefined)
|
|
30406
|
+
return false;
|
|
30407
|
+
if (t - failedAt < cooldownSec * 1000)
|
|
30408
|
+
return false;
|
|
30409
|
+
state.currentModel = state.originalModel;
|
|
30410
|
+
state.failedModels.delete(state.originalModel);
|
|
30411
|
+
state.phase = "committed";
|
|
30412
|
+
state.lastActiveAt = t;
|
|
30413
|
+
return true;
|
|
30414
|
+
}
|
|
30415
|
+
function sweepStale(map2, ttlMs, now) {
|
|
30416
|
+
const t = now ?? Date.now();
|
|
30417
|
+
let removed = 0;
|
|
30418
|
+
for (const [sid, s] of map2) {
|
|
30419
|
+
if (t - s.lastActiveAt > ttlMs) {
|
|
30420
|
+
map2.delete(sid);
|
|
30421
|
+
removed++;
|
|
30258
30422
|
}
|
|
30259
|
-
}
|
|
30260
|
-
|
|
30261
|
-
|
|
30423
|
+
}
|
|
30424
|
+
return removed;
|
|
30425
|
+
}
|
|
30262
30426
|
|
|
30263
|
-
//
|
|
30264
|
-
|
|
30265
|
-
|
|
30266
|
-
|
|
30267
|
-
|
|
30268
|
-
|
|
30269
|
-
|
|
30270
|
-
|
|
30271
|
-
|
|
30272
|
-
|
|
30273
|
-
|
|
30274
|
-
|
|
30275
|
-
|
|
30276
|
-
|
|
30277
|
-
|
|
30278
|
-
|
|
30427
|
+
// lib/fallback-replay.ts
|
|
30428
|
+
function degradeParts(parts, tier) {
|
|
30429
|
+
if (!Array.isArray(parts))
|
|
30430
|
+
return { parts: [], dropped: [] };
|
|
30431
|
+
if (tier === 1)
|
|
30432
|
+
return { parts: [...parts], dropped: [] };
|
|
30433
|
+
const keep = [];
|
|
30434
|
+
const dropped = [];
|
|
30435
|
+
for (const p of parts) {
|
|
30436
|
+
const type = p && typeof p === "object" ? p.type : undefined;
|
|
30437
|
+
const isKept = type === "text";
|
|
30438
|
+
if (isKept)
|
|
30439
|
+
keep.push(p);
|
|
30440
|
+
else
|
|
30441
|
+
dropped.push(p);
|
|
30442
|
+
}
|
|
30443
|
+
return { parts: keep, dropped };
|
|
30279
30444
|
}
|
|
30280
|
-
function
|
|
30281
|
-
|
|
30445
|
+
function roleOf(m) {
|
|
30446
|
+
if (typeof m.role === "string")
|
|
30447
|
+
return m.role;
|
|
30448
|
+
if (m.info && typeof m.info.role === "string")
|
|
30449
|
+
return m.info.role;
|
|
30450
|
+
return;
|
|
30282
30451
|
}
|
|
30283
|
-
function
|
|
30284
|
-
|
|
30285
|
-
return
|
|
30286
|
-
|
|
30287
|
-
|
|
30452
|
+
function partsOf(m) {
|
|
30453
|
+
if (Array.isArray(m.parts))
|
|
30454
|
+
return m.parts;
|
|
30455
|
+
return [];
|
|
30456
|
+
}
|
|
30457
|
+
function pickLastUserParts(messages) {
|
|
30458
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
30459
|
+
return null;
|
|
30460
|
+
let lastAssistantIdx = -1;
|
|
30461
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
30462
|
+
const m = messages[i];
|
|
30463
|
+
if (m && roleOf(m) === "assistant") {
|
|
30464
|
+
lastAssistantIdx = i;
|
|
30465
|
+
break;
|
|
30466
|
+
}
|
|
30467
|
+
}
|
|
30468
|
+
if (lastAssistantIdx >= 0) {
|
|
30469
|
+
for (let i = lastAssistantIdx - 1;i >= 0; i--) {
|
|
30470
|
+
const m = messages[i];
|
|
30471
|
+
if (m && roleOf(m) === "user") {
|
|
30472
|
+
const parts = partsOf(m);
|
|
30473
|
+
return parts.length > 0 ? parts : null;
|
|
30474
|
+
}
|
|
30475
|
+
}
|
|
30476
|
+
}
|
|
30477
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
30478
|
+
const m = messages[i];
|
|
30479
|
+
if (m && roleOf(m) === "user") {
|
|
30480
|
+
const parts = partsOf(m);
|
|
30481
|
+
return parts.length > 0 ? parts : null;
|
|
30482
|
+
}
|
|
30288
30483
|
}
|
|
30484
|
+
return null;
|
|
30289
30485
|
}
|
|
30290
|
-
function
|
|
30291
|
-
const
|
|
30292
|
-
if (
|
|
30486
|
+
async function fetchLastUserParts(client, sessionID, directory) {
|
|
30487
|
+
const fn = client?.session?.messages;
|
|
30488
|
+
if (typeof fn !== "function")
|
|
30293
30489
|
return null;
|
|
30294
|
-
let parsed;
|
|
30295
30490
|
try {
|
|
30296
|
-
|
|
30491
|
+
const res = await fn({
|
|
30492
|
+
path: { id: sessionID },
|
|
30493
|
+
query: directory ? { directory } : undefined
|
|
30494
|
+
});
|
|
30495
|
+
if (res && typeof res === "object" && "error" in res && res.error)
|
|
30496
|
+
return null;
|
|
30497
|
+
const data = res && typeof res === "object" ? res.data : undefined;
|
|
30498
|
+
const arr = Array.isArray(data) ? data : null;
|
|
30499
|
+
if (!arr)
|
|
30500
|
+
return null;
|
|
30501
|
+
return pickLastUserParts(arr);
|
|
30297
30502
|
} catch {
|
|
30298
30503
|
return null;
|
|
30299
30504
|
}
|
|
30300
|
-
|
|
30301
|
-
|
|
30302
|
-
const
|
|
30303
|
-
|
|
30304
|
-
const tools = obj["allowed_tools"];
|
|
30305
|
-
if (!name || !Array.isArray(tools))
|
|
30505
|
+
}
|
|
30506
|
+
function splitModel(model) {
|
|
30507
|
+
const i = model.indexOf("/");
|
|
30508
|
+
if (i <= 0 || i >= model.length - 1)
|
|
30306
30509
|
return null;
|
|
30307
|
-
|
|
30308
|
-
for (const t of tools) {
|
|
30309
|
-
if (typeof t === "string")
|
|
30310
|
-
allowedTools.push(t);
|
|
30311
|
-
}
|
|
30312
|
-
return { name, allowedTools };
|
|
30510
|
+
return { providerID: model.slice(0, i), modelID: model.slice(i + 1) };
|
|
30313
30511
|
}
|
|
30314
|
-
function
|
|
30315
|
-
const
|
|
30316
|
-
const
|
|
30317
|
-
|
|
30318
|
-
|
|
30319
|
-
const candidateDirs = [
|
|
30320
|
-
join21(rootDir, ".codeforge", "agents"),
|
|
30321
|
-
join21(rootDir, "agents"),
|
|
30322
|
-
homeAgentsDir
|
|
30323
|
-
];
|
|
30324
|
-
const result = new Map;
|
|
30325
|
-
const safeSet = new Set(PARALLEL_SAFE_TOOLS);
|
|
30326
|
-
const unionTools = new Set;
|
|
30327
|
-
const log7 = makePluginLogger(PLUGIN_NAME11);
|
|
30328
|
-
for (const dir of candidateDirs) {
|
|
30329
|
-
if (!dirExists(dir))
|
|
30330
|
-
continue;
|
|
30331
|
-
let entries;
|
|
30332
|
-
try {
|
|
30333
|
-
entries = dirReader(dir);
|
|
30334
|
-
} catch (err) {
|
|
30335
|
-
log7.warn(`agents 目录读取失败(已跳过)`, {
|
|
30336
|
-
dir,
|
|
30337
|
-
error: err instanceof Error ? err.message : String(err)
|
|
30338
|
-
});
|
|
30339
|
-
continue;
|
|
30340
|
-
}
|
|
30341
|
-
for (const entry of entries) {
|
|
30342
|
-
if (!entry.endsWith(".md"))
|
|
30343
|
-
continue;
|
|
30344
|
-
const path23 = join21(dir, entry);
|
|
30345
|
-
let content;
|
|
30346
|
-
try {
|
|
30347
|
-
content = reader(path23);
|
|
30348
|
-
} catch (err) {
|
|
30349
|
-
log7.warn(`agent.md 读取失败(已跳过)`, {
|
|
30350
|
-
path: path23,
|
|
30351
|
-
error: err instanceof Error ? err.message : String(err)
|
|
30352
|
-
});
|
|
30353
|
-
continue;
|
|
30354
|
-
}
|
|
30355
|
-
const parsed = parseAgentFrontmatter(content);
|
|
30356
|
-
if (!parsed) {
|
|
30357
|
-
log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path23 });
|
|
30358
|
-
continue;
|
|
30359
|
-
}
|
|
30360
|
-
if (result.has(parsed.name))
|
|
30361
|
-
continue;
|
|
30362
|
-
const intersect = [];
|
|
30363
|
-
const seen = new Set;
|
|
30364
|
-
for (const t of parsed.allowedTools) {
|
|
30365
|
-
if (safeSet.has(t) && !seen.has(t)) {
|
|
30366
|
-
intersect.push(t);
|
|
30367
|
-
seen.add(t);
|
|
30368
|
-
unionTools.add(t);
|
|
30369
|
-
}
|
|
30370
|
-
}
|
|
30371
|
-
result.set(parsed.name, intersect);
|
|
30512
|
+
function toTextParts(parts) {
|
|
30513
|
+
const out = [];
|
|
30514
|
+
for (const p of parts) {
|
|
30515
|
+
if (p && typeof p === "object" && p.type === "text" && typeof p.text === "string") {
|
|
30516
|
+
out.push({ type: "text", text: p.text, id: makePartId() });
|
|
30372
30517
|
}
|
|
30373
30518
|
}
|
|
30374
|
-
|
|
30375
|
-
return result;
|
|
30519
|
+
return out;
|
|
30376
30520
|
}
|
|
30377
|
-
|
|
30378
|
-
|
|
30379
|
-
|
|
30380
|
-
|
|
30381
|
-
|
|
30382
|
-
const oldestKey = sessionAgentMap2.keys().next().value;
|
|
30383
|
-
if (oldestKey === undefined)
|
|
30384
|
-
break;
|
|
30385
|
-
sessionAgentMap2.delete(oldestKey);
|
|
30521
|
+
async function executeReplay(client, opts) {
|
|
30522
|
+
const abortFn = client?.session?.abort;
|
|
30523
|
+
const promptFn = client?.session?.promptAsync;
|
|
30524
|
+
if (typeof abortFn !== "function") {
|
|
30525
|
+
return { aborted: false, prompted: false, error: "abort_unavailable" };
|
|
30386
30526
|
}
|
|
30387
|
-
|
|
30388
|
-
|
|
30389
|
-
|
|
30390
|
-
}
|
|
30391
|
-
function resolveAgent(sessionID, nowFn = Date.now) {
|
|
30392
|
-
if (!sessionID)
|
|
30393
|
-
return "unknown";
|
|
30394
|
-
const entry = sessionAgentMap2.get(sessionID);
|
|
30395
|
-
if (!entry)
|
|
30396
|
-
return "unknown";
|
|
30397
|
-
const now = nowFn();
|
|
30398
|
-
if (isExpired3(entry, now)) {
|
|
30399
|
-
sessionAgentMap2.delete(sessionID);
|
|
30400
|
-
return "unknown";
|
|
30527
|
+
const modelObj = splitModel(opts.model);
|
|
30528
|
+
if (!modelObj) {
|
|
30529
|
+
return { aborted: false, prompted: false, error: "invalid_model" };
|
|
30401
30530
|
}
|
|
30402
|
-
sessionAgentMap2.delete(sessionID);
|
|
30403
|
-
sessionAgentMap2.set(sessionID, entry);
|
|
30404
|
-
return entry.agent;
|
|
30405
|
-
}
|
|
30406
|
-
function renderNudge(agent, tools) {
|
|
30407
|
-
const isUnknown = agent === "unknown";
|
|
30408
|
-
const agentLine = isUnknown ? `You are running as a CodeForge agent (specific agent unknown for this turn).` : `You are the \`${agent}\` agent.`;
|
|
30409
|
-
const toolsBulleted = tools.length > 0 ? tools.map((t) => ` - \`${t}\``).join(`
|
|
30410
|
-
`) : ` - (no parallel-safe tools registered for this agent)`;
|
|
30411
|
-
const body = `────────────────────────────────────────
|
|
30412
|
-
PARALLEL TOOL CALL DIRECTIVE (auto-injected by parallel-tool-nudge plugin)
|
|
30413
|
-
────────────────────────────────────────
|
|
30414
|
-
${agentLine} The opencode runtime executes tool calls in your single
|
|
30415
|
-
response **truly in parallel** (verified: up to 22 tools in 72ms).
|
|
30416
|
-
You MUST batch-emit independent read-only tool calls in one response
|
|
30417
|
-
whenever possible.
|
|
30418
|
-
|
|
30419
|
-
Parallel-safe tools available to you:
|
|
30420
|
-
${toolsBulleted}
|
|
30421
|
-
|
|
30422
|
-
Heuristics:
|
|
30423
|
-
- Need to read 3 files? → emit 3 \`read\` calls in ONE response, not 3 sequential turns
|
|
30424
|
-
- Need to read + map? → emit \`read\` + \`repo_map\` in ONE response
|
|
30425
|
-
- DO NOT batch write tools (edit / write / ast_edit / bash)
|
|
30426
|
-
- DO NOT chain reads where each read's path depends on previous read's output
|
|
30427
|
-
|
|
30428
|
-
This directive is re-injected every turn — it is not optional advice.
|
|
30429
|
-
────────────────────────────────────────`;
|
|
30430
|
-
if (body.length <= NUDGE_MAX_LEN2)
|
|
30431
|
-
return body;
|
|
30432
|
-
return body.slice(0, NUDGE_MAX_LEN2 - 4) + `
|
|
30433
|
-
…`;
|
|
30434
|
-
}
|
|
30435
|
-
var log7 = makePluginLogger(PLUGIN_NAME11);
|
|
30436
|
-
var parallelToolNudgeServer = async (ctx) => {
|
|
30437
30531
|
try {
|
|
30438
|
-
const
|
|
30439
|
-
|
|
30440
|
-
|
|
30441
|
-
agentToolsMap.set(k, v);
|
|
30442
|
-
} catch (err) {
|
|
30443
|
-
log7.warn(`loadAgentToolsMap 失败(plugin 将 no-op)`, {
|
|
30444
|
-
error: err instanceof Error ? err.message : String(err)
|
|
30532
|
+
const res = await abortFn({
|
|
30533
|
+
path: { id: opts.sessionID },
|
|
30534
|
+
query: opts.directory ? { directory: opts.directory } : undefined
|
|
30445
30535
|
});
|
|
30536
|
+
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
30537
|
+
return { aborted: false, prompted: false, error: "abort_returned_error" };
|
|
30538
|
+
}
|
|
30539
|
+
} catch (e) {
|
|
30540
|
+
return { aborted: false, prompted: false, error: `abort_threw: ${e.message}` };
|
|
30446
30541
|
}
|
|
30447
|
-
|
|
30448
|
-
|
|
30449
|
-
agentToolsMap.clear();
|
|
30450
|
-
log7.warn(`0 real agents loaded; plugin will be no-op for this session`, {
|
|
30451
|
-
directory: ctx.directory
|
|
30452
|
-
});
|
|
30542
|
+
if (typeof promptFn !== "function") {
|
|
30543
|
+
return { aborted: true, prompted: false, error: "promptAsync_unavailable" };
|
|
30453
30544
|
}
|
|
30454
|
-
const
|
|
30455
|
-
|
|
30456
|
-
|
|
30457
|
-
|
|
30458
|
-
|
|
30459
|
-
|
|
30460
|
-
|
|
30461
|
-
|
|
30462
|
-
|
|
30463
|
-
|
|
30464
|
-
|
|
30465
|
-
|
|
30466
|
-
|
|
30467
|
-
|
|
30468
|
-
|
|
30469
|
-
|
|
30470
|
-
const agent = input?.agent;
|
|
30471
|
-
if (!sid || !agent)
|
|
30472
|
-
return;
|
|
30473
|
-
sessionAgentMap2.set(sid, { agent, ts: Date.now() });
|
|
30474
|
-
pruneIfOversize3();
|
|
30475
|
-
});
|
|
30476
|
-
},
|
|
30477
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
30478
|
-
await safeAsync(PLUGIN_NAME11, "experimental.chat.system.transform", async () => {
|
|
30479
|
-
if (agentToolsMap.size === 0)
|
|
30480
|
-
return;
|
|
30481
|
-
if (!output || !Array.isArray(output.system))
|
|
30482
|
-
return;
|
|
30483
|
-
const sid = input?.sessionID;
|
|
30484
|
-
const agent = resolveAgent(sid);
|
|
30485
|
-
const tools = agent !== "unknown" ? agentToolsMap.get(agent) ?? agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [] : agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
|
|
30486
|
-
const nudge = renderNudge(agent, tools);
|
|
30487
|
-
output.system.push(nudge);
|
|
30488
|
-
safeWriteLog(PLUGIN_NAME11, {
|
|
30489
|
-
hook: "experimental.chat.system.transform",
|
|
30490
|
-
sessionID: sid,
|
|
30491
|
-
agent,
|
|
30492
|
-
injected_tools: tools,
|
|
30493
|
-
injected_chars: nudge.length,
|
|
30494
|
-
fallback: agent === "unknown"
|
|
30495
|
-
});
|
|
30496
|
-
});
|
|
30545
|
+
const textParts = toTextParts(opts.parts);
|
|
30546
|
+
if (textParts.length === 0) {
|
|
30547
|
+
return { aborted: true, prompted: false, error: "no_text_parts" };
|
|
30548
|
+
}
|
|
30549
|
+
try {
|
|
30550
|
+
const res = await promptFn({
|
|
30551
|
+
path: { id: opts.sessionID },
|
|
30552
|
+
query: opts.directory ? { directory: opts.directory } : undefined,
|
|
30553
|
+
body: {
|
|
30554
|
+
model: { providerID: modelObj.providerID, modelID: modelObj.modelID },
|
|
30555
|
+
agent: opts.agent,
|
|
30556
|
+
parts: textParts
|
|
30557
|
+
}
|
|
30558
|
+
});
|
|
30559
|
+
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
30560
|
+
return { aborted: true, prompted: false, error: "prompt_returned_error" };
|
|
30497
30561
|
}
|
|
30498
|
-
|
|
30499
|
-
}
|
|
30500
|
-
|
|
30501
|
-
|
|
30502
|
-
// plugins/pwsh-utf8.ts
|
|
30503
|
-
var PLUGIN_NAME12 = "pwsh-utf8";
|
|
30504
|
-
logLifecycle(PLUGIN_NAME12, "import", {});
|
|
30505
|
-
var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
|
|
30506
|
-
function prependUtf8Prelude(command) {
|
|
30507
|
-
if (typeof command !== "string")
|
|
30508
|
-
return;
|
|
30509
|
-
if (command.length === 0)
|
|
30510
|
-
return command;
|
|
30511
|
-
if (/chcp\s+65001/i.test(command))
|
|
30512
|
-
return command;
|
|
30513
|
-
if (/\[Console\]::OutputEncoding\s*=/i.test(command))
|
|
30514
|
-
return command;
|
|
30515
|
-
return PRELUDE + command;
|
|
30562
|
+
return { aborted: true, prompted: true };
|
|
30563
|
+
} catch (e) {
|
|
30564
|
+
return { aborted: true, prompted: false, error: `prompt_threw: ${e.message}` };
|
|
30565
|
+
}
|
|
30516
30566
|
}
|
|
30517
|
-
var handler12 = async (_ctx4) => {
|
|
30518
|
-
const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
|
|
30519
|
-
const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
|
|
30520
|
-
logLifecycle(PLUGIN_NAME12, "activate", { enabled, platform: process.platform, reason });
|
|
30521
|
-
if (!enabled)
|
|
30522
|
-
return {};
|
|
30523
|
-
return {
|
|
30524
|
-
"tool.execute.before": async (input, output) => {
|
|
30525
|
-
await safeAsync(PLUGIN_NAME12, "tool.execute.before", async () => {
|
|
30526
|
-
if (input.tool !== "bash")
|
|
30527
|
-
return;
|
|
30528
|
-
const args = output.args ?? {};
|
|
30529
|
-
const original = args["command"];
|
|
30530
|
-
const next = prependUtf8Prelude(original);
|
|
30531
|
-
if (next !== undefined && next !== original) {
|
|
30532
|
-
args["command"] = next;
|
|
30533
|
-
output.args = args;
|
|
30534
|
-
safeWriteLog(PLUGIN_NAME12, {
|
|
30535
|
-
hook: "tool.execute.before",
|
|
30536
|
-
tool: input.tool,
|
|
30537
|
-
callID: input.callID,
|
|
30538
|
-
sessionID: input.sessionID,
|
|
30539
|
-
injected: true
|
|
30540
|
-
});
|
|
30541
|
-
}
|
|
30542
|
-
});
|
|
30543
|
-
}
|
|
30544
|
-
};
|
|
30545
|
-
};
|
|
30546
30567
|
|
|
30547
|
-
//
|
|
30548
|
-
import { promises as
|
|
30568
|
+
// plugins/subtask-heartbeat.ts
|
|
30569
|
+
import { promises as fsPromises } from "node:fs";
|
|
30549
30570
|
import * as path23 from "node:path";
|
|
30550
|
-
|
|
30551
|
-
|
|
30552
|
-
|
|
30553
|
-
|
|
30554
|
-
|
|
30555
|
-
|
|
30556
|
-
|
|
30557
|
-
|
|
30571
|
+
var recordSessionParent2 = recordSessionParent;
|
|
30572
|
+
var lookupParentSessionId2 = lookupParentSessionId;
|
|
30573
|
+
var deleteSessionParent2 = deleteSessionParent;
|
|
30574
|
+
var sweepExpiredSessionParents2 = sweepExpiredSessionParents;
|
|
30575
|
+
var _bulkInjectSessionParentMap2 = _bulkInjectSessionParentMap;
|
|
30576
|
+
var _capSessionParentMap2 = _capSessionParentMap;
|
|
30577
|
+
var _setPersistRootForTests2 = _setPersistRootForTests;
|
|
30578
|
+
var PLUGIN_NAME10 = "subtask-heartbeat";
|
|
30579
|
+
logLifecycle(PLUGIN_NAME10, "import", {});
|
|
30580
|
+
var HEARTBEAT_INTERVAL_MS2 = 30000;
|
|
30581
|
+
var HEARTBEAT_DEBOUNCE_MS = 25000;
|
|
30582
|
+
var TOAST_DURATION_MS2 = 5000;
|
|
30583
|
+
var START_TOAST_DURATION_MS = 2000;
|
|
30584
|
+
var PENDING_TASK_TTL_MS = 60000;
|
|
30585
|
+
var PENDING_TASK_MAX_PARENTS = 64;
|
|
30586
|
+
var PENDING_TASK_MAX_PER_PARENT = 16;
|
|
30587
|
+
var DESCRIPTION_MAX_LEN = 60;
|
|
30588
|
+
var PARENT_PARSE_FAIL_MAX_LOG = 10;
|
|
30589
|
+
var inflight2 = new Map;
|
|
30590
|
+
var pendingTask = new Map;
|
|
30591
|
+
var _parentParseFailLogged = 0;
|
|
30592
|
+
function detectUnparsedParentID(event) {
|
|
30593
|
+
if (!event || typeof event !== "object")
|
|
30594
|
+
return false;
|
|
30595
|
+
const e = event;
|
|
30596
|
+
if (e.type !== "session.created")
|
|
30597
|
+
return false;
|
|
30598
|
+
if (extractCreatedChild(event))
|
|
30599
|
+
return false;
|
|
30558
30600
|
try {
|
|
30559
|
-
|
|
30560
|
-
|
|
30561
|
-
|
|
30562
|
-
|
|
30563
|
-
throw err;
|
|
30601
|
+
const json2 = JSON.stringify(event);
|
|
30602
|
+
return /\bparent[_-]?[iI][dD]\b/.test(json2);
|
|
30603
|
+
} catch {
|
|
30604
|
+
return false;
|
|
30564
30605
|
}
|
|
30565
|
-
const out = [];
|
|
30566
|
-
for (const e of entries) {
|
|
30567
|
-
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
30568
|
-
continue;
|
|
30569
|
-
const file2 = path23.join(dir, e.name);
|
|
30570
|
-
const id = e.name.replace(/\.jsonl$/, "");
|
|
30571
|
-
try {
|
|
30572
|
-
const stat = await fs18.stat(file2);
|
|
30573
|
-
const headerLine = await readFirstLine(file2);
|
|
30574
|
-
let started_at = stat.birthtimeMs;
|
|
30575
|
-
if (headerLine) {
|
|
30576
|
-
try {
|
|
30577
|
-
const h = JSON.parse(headerLine);
|
|
30578
|
-
if (h.__header && typeof h.started_at === "number") {
|
|
30579
|
-
started_at = h.started_at;
|
|
30580
|
-
}
|
|
30581
|
-
} catch {}
|
|
30582
|
-
}
|
|
30583
|
-
out.push({
|
|
30584
|
-
id,
|
|
30585
|
-
file: file2,
|
|
30586
|
-
started_at,
|
|
30587
|
-
size: stat.size,
|
|
30588
|
-
mtime_ms: stat.mtimeMs
|
|
30589
|
-
});
|
|
30590
|
-
} catch {}
|
|
30591
|
-
}
|
|
30592
|
-
out.sort((a, b) => b.started_at - a.started_at);
|
|
30593
|
-
return out;
|
|
30594
|
-
}
|
|
30595
|
-
function resolveDir(opts = {}) {
|
|
30596
|
-
const root = path23.resolve(opts.root ?? process.cwd());
|
|
30597
|
-
return opts.sessions_dir ? path23.resolve(root, opts.sessions_dir) : path23.join(runtimeDir(root), "sessions");
|
|
30598
30606
|
}
|
|
30599
|
-
function
|
|
30600
|
-
|
|
30607
|
+
function extractCreatedChild(event) {
|
|
30608
|
+
if (!event || typeof event !== "object")
|
|
30609
|
+
return null;
|
|
30610
|
+
const e = event;
|
|
30611
|
+
if (e.type !== "session.created")
|
|
30612
|
+
return null;
|
|
30613
|
+
const info = e.properties?.info;
|
|
30614
|
+
if (!info || typeof info !== "object")
|
|
30615
|
+
return null;
|
|
30616
|
+
const session = info;
|
|
30617
|
+
if (typeof session.id !== "string")
|
|
30618
|
+
return null;
|
|
30619
|
+
if (typeof session.parentID !== "string" || session.parentID === "")
|
|
30620
|
+
return null;
|
|
30621
|
+
return { childID: session.id, parentID: session.parentID, agent: null };
|
|
30601
30622
|
}
|
|
30602
|
-
|
|
30603
|
-
|
|
30604
|
-
|
|
30605
|
-
|
|
30606
|
-
if (
|
|
30607
|
-
|
|
30608
|
-
|
|
30609
|
-
|
|
30610
|
-
|
|
30611
|
-
|
|
30612
|
-
|
|
30613
|
-
|
|
30614
|
-
|
|
30615
|
-
|
|
30616
|
-
|
|
30617
|
-
|
|
30618
|
-
|
|
30619
|
-
started_at = h.started_at;
|
|
30620
|
-
continue;
|
|
30623
|
+
var ERROR_REASON_MAX_LEN = 300;
|
|
30624
|
+
function extractErrorReason(props) {
|
|
30625
|
+
function sanitize(s) {
|
|
30626
|
+
const folded = s.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
30627
|
+
if (folded.length <= ERROR_REASON_MAX_LEN)
|
|
30628
|
+
return folded;
|
|
30629
|
+
return folded.slice(0, ERROR_REASON_MAX_LEN) + "…";
|
|
30630
|
+
}
|
|
30631
|
+
function extractFromValue(v) {
|
|
30632
|
+
if (typeof v === "string" && v.trim())
|
|
30633
|
+
return sanitize(v);
|
|
30634
|
+
if (v && typeof v === "object") {
|
|
30635
|
+
const obj = v;
|
|
30636
|
+
const dataMsg = obj["data"] && typeof obj["data"] === "object" ? obj["data"]["message"] : undefined;
|
|
30637
|
+
const fromMsg = obj["message"] ?? dataMsg ?? obj["reason"];
|
|
30638
|
+
if (typeof fromMsg === "string" && fromMsg.trim())
|
|
30639
|
+
return sanitize(fromMsg);
|
|
30621
30640
|
}
|
|
30622
|
-
|
|
30623
|
-
events.push(obj);
|
|
30641
|
+
return null;
|
|
30624
30642
|
}
|
|
30625
|
-
|
|
30626
|
-
|
|
30627
|
-
|
|
30628
|
-
if (
|
|
30629
|
-
return
|
|
30630
|
-
|
|
30631
|
-
|
|
30632
|
-
|
|
30633
|
-
|
|
30634
|
-
|
|
30635
|
-
|
|
30636
|
-
try {
|
|
30637
|
-
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
30638
|
-
const s = buf.subarray(0, bytesRead).toString("utf8");
|
|
30639
|
-
const nl = s.indexOf(`
|
|
30640
|
-
`);
|
|
30641
|
-
return nl === -1 ? s : s.slice(0, nl);
|
|
30642
|
-
} finally {
|
|
30643
|
-
await fh.close();
|
|
30643
|
+
const fromError = extractFromValue(props["error"]);
|
|
30644
|
+
if (fromError !== null)
|
|
30645
|
+
return fromError;
|
|
30646
|
+
if (typeof props["message"] === "string" && props["message"].trim())
|
|
30647
|
+
return sanitize(props["message"]);
|
|
30648
|
+
if (typeof props["reason"] === "string" && props["reason"].trim())
|
|
30649
|
+
return sanitize(props["reason"]);
|
|
30650
|
+
if (props["info"] && typeof props["info"] === "object") {
|
|
30651
|
+
const fromInfoError = extractFromValue(props["info"]["error"]);
|
|
30652
|
+
if (fromInfoError !== null)
|
|
30653
|
+
return fromInfoError;
|
|
30644
30654
|
}
|
|
30655
|
+
return null;
|
|
30645
30656
|
}
|
|
30646
|
-
|
|
30647
|
-
|
|
30648
|
-
|
|
30649
|
-
|
|
30650
|
-
|
|
30651
|
-
|
|
30652
|
-
|
|
30653
|
-
|
|
30654
|
-
"pending-changes",
|
|
30655
|
-
"pending_changes",
|
|
30656
|
-
"save_pending_changes",
|
|
30657
|
-
"ast-edit"
|
|
30658
|
-
]);
|
|
30659
|
-
var APPLY_TOOLS = new Set([
|
|
30660
|
-
"apply_changes",
|
|
30661
|
-
"apply-changes",
|
|
30662
|
-
"pending-changes-apply"
|
|
30663
|
-
]);
|
|
30664
|
-
var SUBTASK_OPEN = /^subtask[\.\-_:]?(start|create|spawn|run)$/i;
|
|
30665
|
-
var SUBTASK_CLOSE = /^subtask[\.\-_:]?(complete|done|close|finish)$/i;
|
|
30666
|
-
function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
30667
|
-
const cleanOpts = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined));
|
|
30668
|
-
const o = { ...DEFAULT_OPTS, ...cleanOpts };
|
|
30669
|
-
const now = opts.now ?? Date.now();
|
|
30670
|
-
if (events.length === 0 || !meta.id) {
|
|
30671
|
-
return {
|
|
30672
|
-
last_session_id: meta.id ?? null,
|
|
30673
|
-
reason: "no_session",
|
|
30674
|
-
idle_ms: null,
|
|
30675
|
-
last_user_intent: null,
|
|
30676
|
-
last_agent: null,
|
|
30677
|
-
inflight_tools: [],
|
|
30678
|
-
pending_changes_likely: false,
|
|
30679
|
-
open_subtasks_likely: false,
|
|
30680
|
-
summary: "无历史会话,无需恢复。"
|
|
30681
|
-
};
|
|
30657
|
+
function extractEndedSessionID(event) {
|
|
30658
|
+
if (!event || typeof event !== "object")
|
|
30659
|
+
return null;
|
|
30660
|
+
const e = event;
|
|
30661
|
+
if (typeof e.type !== "string")
|
|
30662
|
+
return null;
|
|
30663
|
+
if (e.type !== "session.idle" && e.type !== "session.deleted" && e.type !== "session.error") {
|
|
30664
|
+
return null;
|
|
30682
30665
|
}
|
|
30683
|
-
const
|
|
30684
|
-
const
|
|
30685
|
-
|
|
30686
|
-
|
|
30687
|
-
|
|
30688
|
-
|
|
30689
|
-
|
|
30690
|
-
|
|
30666
|
+
const props = e.properties ?? {};
|
|
30667
|
+
const direct = props["sessionID"];
|
|
30668
|
+
const sessionID = typeof direct === "string" && direct ? direct : (() => {
|
|
30669
|
+
const info = props["info"];
|
|
30670
|
+
if (info && typeof info === "object") {
|
|
30671
|
+
const sid = info.id;
|
|
30672
|
+
if (typeof sid === "string" && sid)
|
|
30673
|
+
return sid;
|
|
30691
30674
|
}
|
|
30692
|
-
|
|
30693
|
-
|
|
30694
|
-
|
|
30695
|
-
|
|
30696
|
-
|
|
30697
|
-
|
|
30698
|
-
|
|
30675
|
+
return null;
|
|
30676
|
+
})();
|
|
30677
|
+
if (!sessionID)
|
|
30678
|
+
return null;
|
|
30679
|
+
const errorReason = e.type === "session.error" ? extractErrorReason(props) : null;
|
|
30680
|
+
return { type: e.type, sessionID, errorReason };
|
|
30681
|
+
}
|
|
30682
|
+
function extractTaskArgs(args) {
|
|
30683
|
+
if (!args || typeof args !== "object")
|
|
30684
|
+
return null;
|
|
30685
|
+
const a = args;
|
|
30686
|
+
const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
|
|
30687
|
+
const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
|
|
30688
|
+
const description17 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
|
|
30689
|
+
const subagentType = typeof a["subagent_type"] === "string" && a["subagent_type"] || typeof a["agent"] === "string" && a["agent"] || typeof a["agentType"] === "string" && a["agentType"] || typeof a["agent_type"] === "string" && a["agent_type"] || null;
|
|
30690
|
+
if (!description17 && !subagentType)
|
|
30691
|
+
return null;
|
|
30692
|
+
return { description: description17, subagentType };
|
|
30693
|
+
}
|
|
30694
|
+
function enqueuePendingTask(parentID, entry, now = Date.now()) {
|
|
30695
|
+
const ts = entry.ts ?? now;
|
|
30696
|
+
let bucket = pendingTask.get(parentID);
|
|
30697
|
+
if (!bucket) {
|
|
30698
|
+
if (pendingTask.size >= PENDING_TASK_MAX_PARENTS) {
|
|
30699
|
+
let oldestKey = null;
|
|
30700
|
+
let oldestTs = Number.POSITIVE_INFINITY;
|
|
30701
|
+
for (const [k, v] of pendingTask.entries()) {
|
|
30702
|
+
const headTs = v[0]?.ts ?? Number.POSITIVE_INFINITY;
|
|
30703
|
+
if (headTs < oldestTs) {
|
|
30704
|
+
oldestTs = headTs;
|
|
30705
|
+
oldestKey = k;
|
|
30706
|
+
}
|
|
30707
|
+
}
|
|
30708
|
+
if (oldestKey !== null)
|
|
30709
|
+
pendingTask.delete(oldestKey);
|
|
30699
30710
|
}
|
|
30700
|
-
|
|
30701
|
-
|
|
30711
|
+
bucket = [];
|
|
30712
|
+
pendingTask.set(parentID, bucket);
|
|
30702
30713
|
}
|
|
30703
|
-
|
|
30704
|
-
|
|
30705
|
-
|
|
30706
|
-
const cur = toolMap.get(t.tool);
|
|
30707
|
-
const argsExcerpt = clip3(safeJson(t.args), o.excerptLimit);
|
|
30708
|
-
if (cur) {
|
|
30709
|
-
cur.count++;
|
|
30710
|
-
cur.last_ok = t.ok;
|
|
30711
|
-
cur.last_args_excerpt = argsExcerpt;
|
|
30712
|
-
cur.last_ts = t.timestamp;
|
|
30713
|
-
} else {
|
|
30714
|
-
toolMap.set(t.tool, {
|
|
30715
|
-
tool: t.tool,
|
|
30716
|
-
count: 1,
|
|
30717
|
-
last_ok: t.ok,
|
|
30718
|
-
last_args_excerpt: argsExcerpt,
|
|
30719
|
-
last_ts: t.timestamp
|
|
30720
|
-
});
|
|
30721
|
-
}
|
|
30714
|
+
bucket.push({ agent: entry.agent, description: entry.description, ts });
|
|
30715
|
+
while (bucket.length > PENDING_TASK_MAX_PER_PARENT) {
|
|
30716
|
+
bucket.shift();
|
|
30722
30717
|
}
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
const
|
|
30726
|
-
|
|
30727
|
-
|
|
30728
|
-
|
|
30729
|
-
|
|
30730
|
-
openCount++;
|
|
30731
|
-
else if (SUBTASK_CLOSE.test(t.tool) && openCount > 0)
|
|
30732
|
-
openCount--;
|
|
30718
|
+
}
|
|
30719
|
+
function dequeuePendingTask(parentID, now = Date.now()) {
|
|
30720
|
+
const bucket = pendingTask.get(parentID);
|
|
30721
|
+
if (!bucket || bucket.length === 0) {
|
|
30722
|
+
if (bucket)
|
|
30723
|
+
pendingTask.delete(parentID);
|
|
30724
|
+
return null;
|
|
30733
30725
|
}
|
|
30734
|
-
|
|
30735
|
-
|
|
30736
|
-
let reason = "no_completion_marker";
|
|
30737
|
-
if (lastEvent.type === "message" && lastEvent.role === "system" && /\b(suspend(?:ed|ing)?|interrupt(?:ed|ing)?|paused?|pausing)\b/i.test(lastEvent.content) || lastEvent.type === "agent_switch" && /\b(suspend(?:ed|ing)?|paused?)\b/i.test(lastEvent.reason ?? "")) {
|
|
30738
|
-
reason = "explicit_marker";
|
|
30739
|
-
} else if (idleMs >= o.idleMs) {
|
|
30740
|
-
reason = "idle_timeout";
|
|
30726
|
+
while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
|
|
30727
|
+
bucket.shift();
|
|
30741
30728
|
}
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
30746
|
-
|
|
30747
|
-
|
|
30748
|
-
|
|
30749
|
-
|
|
30750
|
-
reason
|
|
30751
|
-
});
|
|
30752
|
-
return {
|
|
30753
|
-
last_session_id: meta.id,
|
|
30754
|
-
reason,
|
|
30755
|
-
idle_ms: idleMs,
|
|
30756
|
-
last_user_intent: lastUser,
|
|
30757
|
-
last_agent: lastAgent,
|
|
30758
|
-
inflight_tools: inflight2,
|
|
30759
|
-
pending_changes_likely,
|
|
30760
|
-
open_subtasks_likely,
|
|
30761
|
-
summary
|
|
30762
|
-
};
|
|
30729
|
+
if (bucket.length === 0) {
|
|
30730
|
+
pendingTask.delete(parentID);
|
|
30731
|
+
return null;
|
|
30732
|
+
}
|
|
30733
|
+
const entry = bucket.shift();
|
|
30734
|
+
if (bucket.length === 0)
|
|
30735
|
+
pendingTask.delete(parentID);
|
|
30736
|
+
return entry;
|
|
30763
30737
|
}
|
|
30764
|
-
function
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
|
|
30770
|
-
|
|
30771
|
-
|
|
30772
|
-
|
|
30773
|
-
lines.push(`\uD83D\uDD27 最近活跃工具:${top.join(", ")}`);
|
|
30738
|
+
function sweepExpiredPendingTasks(now = Date.now()) {
|
|
30739
|
+
let removed = 0;
|
|
30740
|
+
for (const [parentID, bucket] of [...pendingTask.entries()]) {
|
|
30741
|
+
while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
|
|
30742
|
+
bucket.shift();
|
|
30743
|
+
removed++;
|
|
30744
|
+
}
|
|
30745
|
+
if (bucket.length === 0)
|
|
30746
|
+
pendingTask.delete(parentID);
|
|
30774
30747
|
}
|
|
30775
|
-
|
|
30776
|
-
|
|
30777
|
-
|
|
30778
|
-
|
|
30779
|
-
|
|
30780
|
-
|
|
30748
|
+
return removed;
|
|
30749
|
+
}
|
|
30750
|
+
function registerInflight(payload, now = Date.now()) {
|
|
30751
|
+
const r = {
|
|
30752
|
+
childID: payload.childID,
|
|
30753
|
+
parentID: payload.parentID,
|
|
30754
|
+
agent: payload.agent,
|
|
30755
|
+
description: payload.description ?? null,
|
|
30756
|
+
startedAt: now,
|
|
30757
|
+
lastBeatAt: now,
|
|
30758
|
+
lastTool: null,
|
|
30759
|
+
pendingCalls: new Map
|
|
30760
|
+
};
|
|
30761
|
+
inflight2.set(payload.childID, r);
|
|
30762
|
+
return r;
|
|
30763
|
+
}
|
|
30764
|
+
function recordToolBeat(sessionID, tool2, now = Date.now()) {
|
|
30765
|
+
const r = inflight2.get(sessionID);
|
|
30766
|
+
if (!r)
|
|
30767
|
+
return null;
|
|
30768
|
+
r.lastBeatAt = now;
|
|
30769
|
+
r.lastTool = tool2;
|
|
30770
|
+
return r;
|
|
30771
|
+
}
|
|
30772
|
+
function clearInflight2(sessionID) {
|
|
30773
|
+
const r = inflight2.get(sessionID);
|
|
30774
|
+
if (!r)
|
|
30775
|
+
return null;
|
|
30776
|
+
inflight2.delete(sessionID);
|
|
30777
|
+
return r;
|
|
30778
|
+
}
|
|
30779
|
+
function pickHeartbeats(now = Date.now()) {
|
|
30780
|
+
const out = [];
|
|
30781
|
+
for (const r of inflight2.values()) {
|
|
30782
|
+
if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
|
|
30783
|
+
out.push(r);
|
|
30781
30784
|
}
|
|
30782
|
-
return
|
|
30783
|
-
`);
|
|
30785
|
+
return out;
|
|
30784
30786
|
}
|
|
30785
|
-
function
|
|
30786
|
-
|
|
30787
|
-
|
|
30788
|
-
|
|
30789
|
-
|
|
30790
|
-
if (ms < 3600000)
|
|
30791
|
-
return `${Math.round(ms / 60000)}min`;
|
|
30792
|
-
if (ms < 86400000)
|
|
30793
|
-
return `${Math.round(ms / 3600000)}h`;
|
|
30794
|
-
return `${Math.round(ms / 86400000)}d`;
|
|
30787
|
+
function fmtElapsed(ms) {
|
|
30788
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
30789
|
+
const m = Math.floor(total / 60);
|
|
30790
|
+
const s = total % 60;
|
|
30791
|
+
return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
|
|
30795
30792
|
}
|
|
30796
|
-
function
|
|
30793
|
+
function titleCase(s) {
|
|
30797
30794
|
if (!s)
|
|
30798
|
-
return "";
|
|
30799
|
-
if (s.length <= max)
|
|
30800
30795
|
return s;
|
|
30801
|
-
return s.
|
|
30796
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
30802
30797
|
}
|
|
30803
|
-
function
|
|
30804
|
-
|
|
30805
|
-
|
|
30806
|
-
|
|
30807
|
-
|
|
30798
|
+
function sanitizeDescription(s) {
|
|
30799
|
+
const collapsed = s.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
30800
|
+
if (collapsed.length <= DESCRIPTION_MAX_LEN)
|
|
30801
|
+
return collapsed;
|
|
30802
|
+
return collapsed.slice(0, DESCRIPTION_MAX_LEN) + "…";
|
|
30803
|
+
}
|
|
30804
|
+
function buildStartToast(r) {
|
|
30805
|
+
if (r.agent && r.description) {
|
|
30806
|
+
return {
|
|
30807
|
+
message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动 — ${sanitizeDescription(r.description)}`,
|
|
30808
|
+
variant: "info"
|
|
30809
|
+
};
|
|
30810
|
+
}
|
|
30811
|
+
if (r.agent) {
|
|
30812
|
+
return {
|
|
30813
|
+
message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动`,
|
|
30814
|
+
variant: "info"
|
|
30815
|
+
};
|
|
30808
30816
|
}
|
|
30817
|
+
return {
|
|
30818
|
+
message: `\uD83D\uDE80 子 session 启动: subagent`,
|
|
30819
|
+
variant: "info"
|
|
30820
|
+
};
|
|
30809
30821
|
}
|
|
30810
|
-
|
|
30811
|
-
|
|
30812
|
-
|
|
30813
|
-
|
|
30822
|
+
function buildHeartbeatToast(r, now = Date.now()) {
|
|
30823
|
+
const who = r.agent ?? "subagent";
|
|
30824
|
+
const tool2 = r.lastTool ?? "thinking";
|
|
30825
|
+
return {
|
|
30826
|
+
message: `⏳ ${who} 仍在运行 ${fmtElapsed(now - r.startedAt)} | 当前: ${tool2}`,
|
|
30827
|
+
variant: "info"
|
|
30828
|
+
};
|
|
30829
|
+
}
|
|
30830
|
+
function buildEndToast(r, type, now = Date.now()) {
|
|
30831
|
+
const elapsed = fmtElapsed(now - r.startedAt);
|
|
30832
|
+
const variant = type === "session.error" || type === "session.deleted" ? "error" : "success";
|
|
30833
|
+
const emoji3 = type === "session.error" ? "❌" : type === "session.deleted" ? "\uD83D\uDDD1️" : "✅";
|
|
30834
|
+
const verb = type === "session.error" ? "失败" : type === "session.deleted" ? "被取消" : "完成";
|
|
30835
|
+
if (r.agent && r.description) {
|
|
30836
|
+
if (type === "session.idle" || type !== "session.error" && type !== "session.deleted") {
|
|
30814
30837
|
return {
|
|
30815
|
-
|
|
30816
|
-
|
|
30838
|
+
message: `${emoji3} ${titleCase(r.agent)} Task — ${sanitizeDescription(r.description)} (${elapsed})`,
|
|
30839
|
+
variant
|
|
30817
30840
|
};
|
|
30818
30841
|
}
|
|
30819
|
-
const exclude = new Set(opts.excludeIds ?? []);
|
|
30820
|
-
const candidates = sessions.filter((s) => !exclude.has(s.id)).sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
30821
|
-
if (candidates.length === 0) {
|
|
30822
|
-
return { ok: true, plan: emptyPlan("no_session") };
|
|
30823
|
-
}
|
|
30824
|
-
const top = candidates[0];
|
|
30825
|
-
const loaded = await loadSession(top.id, opts);
|
|
30826
|
-
const plan = buildRecoveryPlan(loaded.events, { id: top.id, lastEventAt: top.mtime_ms }, {
|
|
30827
|
-
idleMs: opts.idleMs,
|
|
30828
|
-
windowSize: opts.windowSize,
|
|
30829
|
-
excerptLimit: opts.excerptLimit,
|
|
30830
|
-
now: opts.now
|
|
30831
|
-
});
|
|
30832
|
-
return { ok: true, plan, meta: top };
|
|
30833
|
-
} catch (err) {
|
|
30834
30842
|
return {
|
|
30835
|
-
|
|
30836
|
-
|
|
30843
|
+
message: `${emoji3} ${titleCase(r.agent)} ${verb} — ${sanitizeDescription(r.description)} (${elapsed})`,
|
|
30844
|
+
variant
|
|
30845
|
+
};
|
|
30846
|
+
}
|
|
30847
|
+
if (r.agent) {
|
|
30848
|
+
return {
|
|
30849
|
+
message: `${emoji3} ${titleCase(r.agent)} ${verb} (${elapsed})`,
|
|
30850
|
+
variant
|
|
30837
30851
|
};
|
|
30838
30852
|
}
|
|
30839
|
-
}
|
|
30840
|
-
function emptyPlan(reason) {
|
|
30841
30853
|
return {
|
|
30842
|
-
|
|
30843
|
-
|
|
30844
|
-
idle_ms: null,
|
|
30845
|
-
last_user_intent: null,
|
|
30846
|
-
last_agent: null,
|
|
30847
|
-
inflight_tools: [],
|
|
30848
|
-
pending_changes_likely: false,
|
|
30849
|
-
open_subtasks_likely: false,
|
|
30850
|
-
summary: "无历史会话,无需恢复。"
|
|
30854
|
+
message: `${emoji3} subagent ${verb} (${elapsed})`,
|
|
30855
|
+
variant
|
|
30851
30856
|
};
|
|
30852
30857
|
}
|
|
30853
|
-
function
|
|
30854
|
-
if (!
|
|
30855
|
-
return
|
|
30856
|
-
|
|
30857
|
-
|
|
30858
|
-
|
|
30859
|
-
|
|
30858
|
+
function sanitizeInputSummary(toolName, args) {
|
|
30859
|
+
if (!args || typeof args !== "object")
|
|
30860
|
+
return `${toolName} (no args)`;
|
|
30861
|
+
const a = args;
|
|
30862
|
+
const PATH_KEYS = [
|
|
30863
|
+
"path",
|
|
30864
|
+
"file",
|
|
30865
|
+
"filePath",
|
|
30866
|
+
"file_path",
|
|
30867
|
+
"filepath",
|
|
30868
|
+
"target"
|
|
30869
|
+
];
|
|
30870
|
+
for (const k of PATH_KEYS) {
|
|
30871
|
+
const v = a[k];
|
|
30872
|
+
if (typeof v === "string" && v.length > 0 && v.length <= 200) {
|
|
30873
|
+
return `${toolName} ${v}`;
|
|
30874
|
+
}
|
|
30875
|
+
}
|
|
30876
|
+
const cmd = a["command"];
|
|
30877
|
+
if (typeof cmd === "string" && cmd.length > 0) {
|
|
30878
|
+
const head = cmd.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
30879
|
+
return `${toolName} $ ${head}`;
|
|
30880
|
+
}
|
|
30881
|
+
return `${toolName} (no path)`;
|
|
30860
30882
|
}
|
|
30861
|
-
|
|
30862
|
-
|
|
30863
|
-
|
|
30864
|
-
|
|
30865
|
-
|
|
30866
|
-
|
|
30867
|
-
|
|
30883
|
+
function buildBeforeLogLine(toolName, args, now = Date.now()) {
|
|
30884
|
+
return JSON.stringify({
|
|
30885
|
+
ts: Math.floor(now / 1000),
|
|
30886
|
+
tool: toolName,
|
|
30887
|
+
phase: "before",
|
|
30888
|
+
input_summary: sanitizeInputSummary(toolName, args)
|
|
30889
|
+
});
|
|
30868
30890
|
}
|
|
30869
|
-
function
|
|
30870
|
-
return
|
|
30891
|
+
function buildAfterLogLine(toolName, ok, durationMs, now = Date.now()) {
|
|
30892
|
+
return JSON.stringify({
|
|
30893
|
+
ts: Math.floor(now / 1000),
|
|
30894
|
+
tool: toolName,
|
|
30895
|
+
phase: "after",
|
|
30896
|
+
ok,
|
|
30897
|
+
duration_ms: durationMs
|
|
30898
|
+
});
|
|
30871
30899
|
}
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
}
|
|
30880
|
-
if (!raw)
|
|
30881
|
-
return [];
|
|
30882
|
-
const consumed = new Set;
|
|
30883
|
-
const entries = [];
|
|
30884
|
-
for (const line of raw.split(`
|
|
30885
|
-
`)) {
|
|
30886
|
-
const trimmed = line.trim();
|
|
30887
|
-
if (!trimmed)
|
|
30888
|
-
continue;
|
|
30889
|
-
let obj;
|
|
30890
|
-
try {
|
|
30891
|
-
obj = JSON.parse(trimmed);
|
|
30892
|
-
} catch {
|
|
30893
|
-
continue;
|
|
30894
|
-
}
|
|
30895
|
-
if (!obj || typeof obj !== "object")
|
|
30896
|
-
continue;
|
|
30897
|
-
if (obj["type"] === "consume") {
|
|
30898
|
-
const sid = obj["sessionId"];
|
|
30899
|
-
const ts = obj["timestamp"];
|
|
30900
|
-
if (typeof sid === "string" && typeof ts === "string") {
|
|
30901
|
-
consumed.add(`${sid}|${ts}`);
|
|
30902
|
-
}
|
|
30903
|
-
continue;
|
|
30904
|
-
}
|
|
30905
|
-
const sessionId = obj["sessionId"];
|
|
30906
|
-
const timestamp = obj["timestamp"];
|
|
30907
|
-
if (typeof sessionId !== "string" || typeof timestamp !== "string")
|
|
30908
|
-
continue;
|
|
30909
|
-
const entry = {
|
|
30910
|
-
sessionId,
|
|
30911
|
-
timestamp,
|
|
30912
|
-
reason: typeof obj["reason"] === "string" ? obj["reason"] : undefined,
|
|
30913
|
-
summary_excerpt: typeof obj["summary_excerpt"] === "string" ? obj["summary_excerpt"] : undefined,
|
|
30914
|
-
consumed_at: typeof obj["consumed_at"] === "string" ? obj["consumed_at"] : undefined
|
|
30915
|
-
};
|
|
30916
|
-
entries.push(entry);
|
|
30917
|
-
}
|
|
30918
|
-
return entries.filter((e) => {
|
|
30919
|
-
if (e.consumed_at)
|
|
30920
|
-
return false;
|
|
30921
|
-
if (consumed.has(`${e.sessionId}|${e.timestamp}`))
|
|
30922
|
-
return false;
|
|
30923
|
-
if (filterSessionId && e.sessionId !== filterSessionId)
|
|
30924
|
-
return false;
|
|
30925
|
-
return true;
|
|
30926
|
-
});
|
|
30927
|
-
}
|
|
30928
|
-
async function markBlocksConsumed(absRoot, entries) {
|
|
30929
|
-
if (entries.length === 0)
|
|
30930
|
-
return;
|
|
30931
|
-
const file2 = blockPendingFilePath(absRoot);
|
|
30932
|
-
await fs19.mkdir(path24.dirname(file2), { recursive: true });
|
|
30933
|
-
const now = new Date().toISOString();
|
|
30934
|
-
const lines = entries.map((e) => ({
|
|
30935
|
-
type: "consume",
|
|
30936
|
-
sessionId: e.sessionId,
|
|
30937
|
-
timestamp: e.timestamp,
|
|
30938
|
-
consumed_at: now
|
|
30939
|
-
})).map((row) => JSON.stringify(row)).join(`
|
|
30940
|
-
`) + `
|
|
30941
|
-
`;
|
|
30942
|
-
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
30943
|
-
await fs19.appendFile(file2, lines, "utf8");
|
|
30900
|
+
function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
|
|
30901
|
+
return JSON.stringify({
|
|
30902
|
+
ts: Math.floor(now / 1000),
|
|
30903
|
+
phase: "error",
|
|
30904
|
+
ok: false,
|
|
30905
|
+
error_reason: errorReason ?? "(unknown)",
|
|
30906
|
+
elapsed_ms: elapsedMs
|
|
30944
30907
|
});
|
|
30945
30908
|
}
|
|
30946
|
-
|
|
30947
|
-
|
|
30948
|
-
|
|
30949
|
-
|
|
30950
|
-
|
|
30951
|
-
|
|
30952
|
-
|
|
30953
|
-
|
|
30954
|
-
|
|
30955
|
-
|
|
30956
|
-
excludeIds.add(currentSessionId);
|
|
30957
|
-
const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
|
|
30958
|
-
if (!r.ok) {
|
|
30959
|
-
opts.log?.warn?.(`[${PLUGIN_NAME13}] 扫描失败:${r.error}`);
|
|
30960
|
-
return { ok: false, injected: false, reason: "scan_error", error: r.error };
|
|
30961
|
-
}
|
|
30962
|
-
const plan = r.plan;
|
|
30963
|
-
let pendingBlocks = [];
|
|
30964
|
-
if (opts.root) {
|
|
30965
|
-
try {
|
|
30966
|
-
pendingBlocks = await scanBlockPending(opts.root);
|
|
30967
|
-
} catch (err) {
|
|
30968
|
-
opts.log?.warn?.(`[${PLUGIN_NAME13}] scanBlockPending 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
30969
|
-
}
|
|
30970
|
-
}
|
|
30971
|
-
const hasPendingBlocks = pendingBlocks.length > 0;
|
|
30972
|
-
if (!isRecoveryWorthShowing(plan) && !hasPendingBlocks) {
|
|
30973
|
-
return { ok: true, injected: false, plan, reason: "no_signal", pendingBlocks };
|
|
30974
|
-
}
|
|
30975
|
-
const blockPrompt = hasPendingBlocks ? renderBlockPendingPrompt(pendingBlocks) : "";
|
|
30976
|
-
const recoveryPrompt = isRecoveryWorthShowing(plan) ? renderPrompt(plan) : "";
|
|
30977
|
-
const prompt = [blockPrompt, recoveryPrompt].filter((s) => s.length > 0).join(`
|
|
30978
|
-
|
|
30979
|
-
`);
|
|
30980
|
-
const injection = { source: "session-recovery", plan, prompt };
|
|
30981
|
-
if (opts.injectRecovery) {
|
|
30982
|
-
try {
|
|
30983
|
-
await opts.injectRecovery(injection);
|
|
30984
|
-
} catch (err) {
|
|
30985
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
30986
|
-
opts.log?.warn?.(`[${PLUGIN_NAME13}] injectRecovery 异常:${msg}`);
|
|
30987
|
-
return { ok: false, injected: false, plan, reason: "inject_error", error: msg, pendingBlocks };
|
|
30988
|
-
}
|
|
30909
|
+
async function appendSubagentLog(filePath, line, log7) {
|
|
30910
|
+
try {
|
|
30911
|
+
await fsPromises.mkdir(path23.dirname(filePath), { recursive: true });
|
|
30912
|
+
await fsPromises.appendFile(filePath, line + `
|
|
30913
|
+
`, "utf8");
|
|
30914
|
+
} catch (err) {
|
|
30915
|
+
log7?.debug?.("appendSubagentLog 失败(已隔离)", {
|
|
30916
|
+
error: err instanceof Error ? err.message : String(err),
|
|
30917
|
+
file: filePath
|
|
30918
|
+
});
|
|
30989
30919
|
}
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30920
|
+
}
|
|
30921
|
+
function isLogPersistenceEnabled(cwd) {
|
|
30922
|
+
try {
|
|
30923
|
+
const cfg = getCodeforgeConfig({ root: cwd });
|
|
30924
|
+
const runtime = cfg.runtime;
|
|
30925
|
+
if (runtime && typeof runtime === "object") {
|
|
30926
|
+
const v = runtime.subagent_log_persistence;
|
|
30927
|
+
if (v === false)
|
|
30928
|
+
return false;
|
|
30995
30929
|
}
|
|
30930
|
+
return true;
|
|
30931
|
+
} catch {
|
|
30932
|
+
return true;
|
|
30996
30933
|
}
|
|
30997
|
-
return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
|
|
30998
30934
|
}
|
|
30999
|
-
function
|
|
31000
|
-
|
|
31001
|
-
|
|
31002
|
-
|
|
31003
|
-
entries.forEach((e, i) => {
|
|
31004
|
-
const sidShort = e.sessionId.length > 12 ? e.sessionId.slice(0, 8) + "…" : e.sessionId;
|
|
31005
|
-
const reasonPart = e.reason ? `:${e.reason}` : "";
|
|
31006
|
-
lines.push(`${i + 1}. session \`${sidShort}\` @ ${e.timestamp}${reasonPart}`);
|
|
31007
|
-
if (e.summary_excerpt) {
|
|
31008
|
-
const excerpt = e.summary_excerpt.length > 200 ? e.summary_excerpt.slice(0, 200) + "…" : e.summary_excerpt;
|
|
31009
|
-
lines.push(` > ${excerpt.replace(/\n/g, `
|
|
31010
|
-
> `)}`);
|
|
31011
|
-
}
|
|
31012
|
-
});
|
|
31013
|
-
lines.push("");
|
|
31014
|
-
lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
|
|
31015
|
-
return lines.join(`
|
|
31016
|
-
`);
|
|
30935
|
+
function normalizeVariant2(raw) {
|
|
30936
|
+
if (raw === "info" || raw === "warning")
|
|
30937
|
+
return "default";
|
|
30938
|
+
return raw;
|
|
31017
30939
|
}
|
|
31018
|
-
function
|
|
31019
|
-
|
|
31020
|
-
|
|
31021
|
-
|
|
31022
|
-
lines.push("");
|
|
31023
|
-
lines.push("如果用户接下来未明确提及,请优先:");
|
|
31024
|
-
if (plan.pending_changes_likely) {
|
|
31025
|
-
lines.push(" • 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
|
|
30940
|
+
async function showToast2(client, payload, log7) {
|
|
30941
|
+
if (typeof client?.tui?.showToast !== "function") {
|
|
30942
|
+
log7?.debug?.("tui.showToast 不可用,noop");
|
|
30943
|
+
return false;
|
|
31026
30944
|
}
|
|
31027
|
-
|
|
31028
|
-
|
|
30945
|
+
try {
|
|
30946
|
+
await client.tui.showToast({
|
|
30947
|
+
body: {
|
|
30948
|
+
message: payload.message,
|
|
30949
|
+
variant: normalizeVariant2(payload.variant),
|
|
30950
|
+
duration: payload.duration ?? TOAST_DURATION_MS2,
|
|
30951
|
+
title: payload.title ?? "CodeForge"
|
|
30952
|
+
}
|
|
30953
|
+
});
|
|
30954
|
+
return true;
|
|
30955
|
+
} catch (err) {
|
|
30956
|
+
log7?.warn("tui.showToast 抛错(已隔离)", {
|
|
30957
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30958
|
+
});
|
|
30959
|
+
return false;
|
|
31029
30960
|
}
|
|
31030
|
-
|
|
31031
|
-
|
|
30961
|
+
}
|
|
30962
|
+
var log7 = makePluginLogger(PLUGIN_NAME10);
|
|
30963
|
+
var subtaskHeartbeatServer = async (ctx) => {
|
|
30964
|
+
logLifecycle(PLUGIN_NAME10, "activate", {
|
|
30965
|
+
directory: ctx.directory,
|
|
30966
|
+
intervalMs: HEARTBEAT_INTERVAL_MS2
|
|
30967
|
+
});
|
|
30968
|
+
const client = ctx.client;
|
|
30969
|
+
const cwd = ctx.directory;
|
|
30970
|
+
_setPersistRootForTests2(cwd);
|
|
30971
|
+
try {
|
|
30972
|
+
const restored = await loadParentMap(cwd);
|
|
30973
|
+
if (restored.size > 0) {
|
|
30974
|
+
const entries = [...restored.entries()].map(([childID, v]) => ({
|
|
30975
|
+
childID,
|
|
30976
|
+
parentID: v.parentID,
|
|
30977
|
+
ts: v.ts
|
|
30978
|
+
}));
|
|
30979
|
+
_bulkInjectSessionParentMap2(entries);
|
|
30980
|
+
const cappedOut = _capSessionParentMap2();
|
|
30981
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
30982
|
+
hook: "activate",
|
|
30983
|
+
type: "parent-map.restore",
|
|
30984
|
+
restored: restored.size,
|
|
30985
|
+
capped: cappedOut
|
|
30986
|
+
});
|
|
30987
|
+
}
|
|
30988
|
+
} catch (err) {
|
|
30989
|
+
log7.warn("loadParentMap 失败(已隔离),降级为空表", {
|
|
30990
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30991
|
+
});
|
|
31032
30992
|
}
|
|
31033
|
-
|
|
31034
|
-
|
|
30993
|
+
const interval = setInterval(() => {
|
|
30994
|
+
safeAsync(PLUGIN_NAME10, "interval", async () => {
|
|
30995
|
+
const swept = sweepExpiredPendingTasks();
|
|
30996
|
+
if (swept > 0) {
|
|
30997
|
+
safeWriteLog(PLUGIN_NAME10, { hook: "interval", pending_task_swept: swept });
|
|
30998
|
+
}
|
|
30999
|
+
const sweptParents = sweepExpiredSessionParents2();
|
|
31000
|
+
if (sweptParents > 0) {
|
|
31001
|
+
safeWriteLog(PLUGIN_NAME10, { hook: "interval", session_parent_swept: sweptParents });
|
|
31002
|
+
}
|
|
31003
|
+
const beats = pickHeartbeats();
|
|
31004
|
+
if (beats.length === 0)
|
|
31005
|
+
return;
|
|
31006
|
+
for (const r of beats) {
|
|
31007
|
+
const t = buildHeartbeatToast(r);
|
|
31008
|
+
const sent = await showToast2(client, t, log7);
|
|
31009
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31010
|
+
hook: "interval",
|
|
31011
|
+
child: r.childID,
|
|
31012
|
+
parent: r.parentID,
|
|
31013
|
+
tool: r.lastTool,
|
|
31014
|
+
elapsed_ms: Date.now() - r.startedAt,
|
|
31015
|
+
toast_sent: sent
|
|
31016
|
+
});
|
|
31017
|
+
r.lastBeatAt = Date.now();
|
|
31018
|
+
}
|
|
31019
|
+
});
|
|
31020
|
+
}, HEARTBEAT_INTERVAL_MS2);
|
|
31021
|
+
if (typeof interval.unref === "function") {
|
|
31022
|
+
interval.unref();
|
|
31035
31023
|
}
|
|
31036
|
-
lines.push("");
|
|
31037
|
-
lines.push("(如果用户明确开了新话题,本提示可忽略)");
|
|
31038
|
-
return lines.join(`
|
|
31039
|
-
`);
|
|
31040
|
-
}
|
|
31041
|
-
var log8 = makePluginLogger(PLUGIN_NAME13);
|
|
31042
|
-
var _lastInjection = null;
|
|
31043
|
-
var sessionRecoveryServer = async (ctx) => {
|
|
31044
|
-
logLifecycle(PLUGIN_NAME13, "activate", { directory: ctx.directory });
|
|
31045
31024
|
return {
|
|
31046
31025
|
event: async ({ event }) => {
|
|
31047
|
-
await safeAsync(
|
|
31048
|
-
|
|
31049
|
-
|
|
31050
|
-
|
|
31051
|
-
|
|
31052
|
-
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
|
|
31026
|
+
await safeAsync(PLUGIN_NAME10, "event", async () => {
|
|
31027
|
+
try {
|
|
31028
|
+
if (detectUnparsedParentID(event) && _parentParseFailLogged < PARENT_PARSE_FAIL_MAX_LOG) {
|
|
31029
|
+
_parentParseFailLogged++;
|
|
31030
|
+
log7.warn("session.created 含 parentID 关键字但 extractCreatedChild 解析失败,可能 SDK schema 变更", {
|
|
31031
|
+
sample_count: _parentParseFailLogged,
|
|
31032
|
+
hint: "如频繁出现请检查 plugins/subtask-heartbeat.ts::extractCreatedChild 是否适配新 SDK"
|
|
31033
|
+
});
|
|
31034
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31035
|
+
hook: "event",
|
|
31036
|
+
type: "session.created.unparsed-parent-id",
|
|
31037
|
+
sample_count: _parentParseFailLogged
|
|
31038
|
+
});
|
|
31039
|
+
}
|
|
31040
|
+
} catch {}
|
|
31041
|
+
const created = extractCreatedChild(event);
|
|
31042
|
+
if (created) {
|
|
31043
|
+
try {
|
|
31044
|
+
recordSessionParent2(created.childID, created.parentID);
|
|
31045
|
+
} catch (err) {
|
|
31046
|
+
log7.warn("recordSessionParent 抛错(已隔离)", {
|
|
31047
|
+
error: err instanceof Error ? err.message : String(err)
|
|
31048
|
+
});
|
|
31049
|
+
}
|
|
31050
|
+
const pending = dequeuePendingTask(created.parentID);
|
|
31051
|
+
const record2 = registerInflight({
|
|
31052
|
+
childID: created.childID,
|
|
31053
|
+
parentID: created.parentID,
|
|
31054
|
+
agent: pending?.agent ?? created.agent,
|
|
31055
|
+
description: pending?.description ?? null
|
|
31056
|
+
});
|
|
31057
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31058
|
+
hook: "event",
|
|
31059
|
+
type: "session.created",
|
|
31060
|
+
child: created.childID,
|
|
31061
|
+
parent: created.parentID,
|
|
31062
|
+
pending_task_matched: pending !== null,
|
|
31063
|
+
agent: record2.agent,
|
|
31064
|
+
description_len: record2.description?.length ?? 0
|
|
31065
|
+
});
|
|
31066
|
+
const startToast = buildStartToast(record2);
|
|
31067
|
+
const sent = await showToast2(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log7);
|
|
31068
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31069
|
+
hook: "event",
|
|
31070
|
+
type: "session.created.toast",
|
|
31071
|
+
child: created.childID,
|
|
31072
|
+
toast_sent: sent,
|
|
31073
|
+
start_toast_message: startToast.message
|
|
31074
|
+
});
|
|
31075
|
+
return;
|
|
31076
|
+
}
|
|
31077
|
+
const ended = extractEndedSessionID(event);
|
|
31078
|
+
if (ended) {
|
|
31079
|
+
if (ended.type === "session.deleted") {
|
|
31080
|
+
try {
|
|
31081
|
+
deleteSessionParent2(ended.sessionID);
|
|
31082
|
+
} catch {}
|
|
31083
|
+
}
|
|
31084
|
+
const r = clearInflight2(ended.sessionID);
|
|
31085
|
+
if (r) {
|
|
31086
|
+
const t = buildEndToast(r, ended.type);
|
|
31087
|
+
const sent = await showToast2(client, t, log7);
|
|
31088
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31089
|
+
hook: "event",
|
|
31090
|
+
type: ended.type,
|
|
31091
|
+
child: r.childID,
|
|
31092
|
+
elapsed_ms: Date.now() - r.startedAt,
|
|
31093
|
+
toast_sent: sent,
|
|
31094
|
+
end_toast_message: t.message
|
|
31095
|
+
});
|
|
31096
|
+
if (ended.type === "session.error" && isLogPersistenceEnabled(cwd)) {
|
|
31097
|
+
const elapsedMs = Date.now() - r.startedAt;
|
|
31098
|
+
const errLine = buildErrorLogLine(ended.errorReason, elapsedMs);
|
|
31099
|
+
const logFile = subagentLogPath(cwd, r.parentID, r.childID);
|
|
31100
|
+
appendSubagentLog(logFile, errLine, log7);
|
|
31101
|
+
const parentID = r.parentID;
|
|
31102
|
+
const who = r.agent ? titleCase(r.agent) : "subagent";
|
|
31103
|
+
const elapsed = fmtElapsed(elapsedMs);
|
|
31104
|
+
const noticeText = [
|
|
31105
|
+
`❌ ${who} 失败(${elapsed})`,
|
|
31106
|
+
ended.errorReason ? `错误:${ended.errorReason.slice(0, 150)}` : null,
|
|
31107
|
+
`日志:${logFile}`,
|
|
31108
|
+
`排查:cat "${logFile}" | tail -30`
|
|
31109
|
+
].filter(Boolean).join(`
|
|
31110
|
+
`);
|
|
31111
|
+
const logAdapter = (level, msg, data) => {
|
|
31112
|
+
if (level === "warn" || level === "error") {
|
|
31113
|
+
log7.warn(`[sendParentNotice] ${msg}`, data);
|
|
31114
|
+
}
|
|
31115
|
+
};
|
|
31116
|
+
(async () => {
|
|
31117
|
+
try {
|
|
31118
|
+
await sendParentNotice(client, parentID, noticeText, { directory: cwd, log: logAdapter });
|
|
31119
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31120
|
+
hook: "event",
|
|
31121
|
+
type: "session.error.notified",
|
|
31122
|
+
child: r.childID,
|
|
31123
|
+
parent: r.parentID,
|
|
31124
|
+
has_error_reason: ended.errorReason !== null,
|
|
31125
|
+
notice_text_len: noticeText.length
|
|
31126
|
+
});
|
|
31127
|
+
} catch {}
|
|
31128
|
+
})();
|
|
31129
|
+
}
|
|
31130
|
+
if (ended.type === "session.deleted") {
|
|
31131
|
+
const logFile = subagentLogPath(cwd, r.parentID, r.childID);
|
|
31132
|
+
fsPromises.unlink(logFile).catch(() => {});
|
|
31133
|
+
}
|
|
31134
|
+
}
|
|
31135
|
+
}
|
|
31136
|
+
});
|
|
31137
|
+
},
|
|
31138
|
+
"tool.execute.before": async (input, output) => {
|
|
31139
|
+
const isTaskTool = input?.tool === "task";
|
|
31140
|
+
if (inflight2.size === 0 && !isTaskTool)
|
|
31141
|
+
return;
|
|
31142
|
+
await safeAsync(PLUGIN_NAME10, "tool.execute.before", async () => {
|
|
31143
|
+
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
31144
|
+
return;
|
|
31145
|
+
if (isTaskTool) {
|
|
31146
|
+
const args = output?.args ?? null;
|
|
31147
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31148
|
+
hook: "tool.execute.before.task",
|
|
31149
|
+
sessionID: input.sessionID,
|
|
31150
|
+
args_keys: args && typeof args === "object" ? Object.keys(args) : null,
|
|
31151
|
+
args_raw: args
|
|
31152
|
+
});
|
|
31153
|
+
const extracted = extractTaskArgs(args);
|
|
31154
|
+
if (extracted) {
|
|
31155
|
+
enqueuePendingTask(input.sessionID, {
|
|
31156
|
+
agent: extracted.subagentType,
|
|
31157
|
+
description: extracted.description
|
|
31158
|
+
});
|
|
31159
|
+
safeWriteLog(PLUGIN_NAME10, {
|
|
31160
|
+
hook: "tool.execute.before.task.enqueued",
|
|
31161
|
+
parent: input.sessionID,
|
|
31162
|
+
agent: extracted.subagentType,
|
|
31163
|
+
description_len: extracted.description?.length ?? 0
|
|
31164
|
+
});
|
|
31165
|
+
}
|
|
31166
|
+
}
|
|
31167
|
+
const rec = inflight2.get(input.sessionID);
|
|
31168
|
+
if (rec) {
|
|
31169
|
+
recordToolBeat(input.sessionID, input.tool);
|
|
31170
|
+
if (!rec.pendingCalls)
|
|
31171
|
+
rec.pendingCalls = new Map;
|
|
31172
|
+
if (typeof input.callID === "string") {
|
|
31173
|
+
rec.pendingCalls.set(input.callID, Date.now());
|
|
31174
|
+
}
|
|
31175
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
31176
|
+
const args = output?.args ?? null;
|
|
31177
|
+
const line = buildBeforeLogLine(input.tool, args);
|
|
31178
|
+
const file2 = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
31179
|
+
appendSubagentLog(file2, line, log7);
|
|
31180
|
+
}
|
|
31181
|
+
}
|
|
31182
|
+
});
|
|
31183
|
+
},
|
|
31184
|
+
"tool.execute.after": async (input, _output) => {
|
|
31185
|
+
if (inflight2.size === 0)
|
|
31186
|
+
return;
|
|
31187
|
+
await safeAsync(PLUGIN_NAME10, "tool.execute.after", async () => {
|
|
31188
|
+
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
31189
|
+
return;
|
|
31190
|
+
const rec = inflight2.get(input.sessionID);
|
|
31191
|
+
if (!rec)
|
|
31192
|
+
return;
|
|
31193
|
+
let durationMs = 0;
|
|
31194
|
+
if (rec.pendingCalls && typeof input.callID === "string") {
|
|
31195
|
+
const startedAt = rec.pendingCalls.get(input.callID);
|
|
31196
|
+
if (typeof startedAt === "number") {
|
|
31197
|
+
durationMs = Date.now() - startedAt;
|
|
31198
|
+
rec.pendingCalls.delete(input.callID);
|
|
31199
|
+
}
|
|
31200
|
+
}
|
|
31201
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
31202
|
+
const line = buildAfterLogLine(input.tool, true, durationMs);
|
|
31203
|
+
const file2 = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
31204
|
+
appendSubagentLog(file2, line, log7);
|
|
31205
|
+
}
|
|
31206
|
+
});
|
|
31207
|
+
}
|
|
31208
|
+
};
|
|
31209
|
+
};
|
|
31210
|
+
var handler10 = subtaskHeartbeatServer;
|
|
31211
|
+
|
|
31212
|
+
// plugins/model-fallback.ts
|
|
31213
|
+
var PLUGIN_NAME11 = "model-fallback";
|
|
31214
|
+
var SELF_ABORT_WINDOW_MS = 2000;
|
|
31215
|
+
var STALE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
31216
|
+
var COOLDOWN_SEC = 5 * 60;
|
|
31217
|
+
var state = {
|
|
31218
|
+
config: null,
|
|
31219
|
+
configPath: null,
|
|
31220
|
+
warnings: [],
|
|
31221
|
+
error: null
|
|
31222
|
+
};
|
|
31223
|
+
var sessionFallbacks = new Map;
|
|
31224
|
+
var inflightReplay = new Set;
|
|
31225
|
+
logLifecycle(PLUGIN_NAME11, "import");
|
|
31226
|
+
function loadOnce(root) {
|
|
31227
|
+
if (state.config !== null || state.error !== null)
|
|
31228
|
+
return;
|
|
31229
|
+
const r = loadModelConfigSync({ root });
|
|
31230
|
+
if (r.ok && r.config) {
|
|
31231
|
+
state.config = r.config;
|
|
31232
|
+
state.configPath = r.path ?? null;
|
|
31233
|
+
state.warnings = r.warnings;
|
|
31234
|
+
if (r.warnings.length > 0) {
|
|
31235
|
+
safeWriteLog(PLUGIN_NAME11, { phase: "load.warnings", warnings: r.warnings });
|
|
31236
|
+
}
|
|
31237
|
+
} else {
|
|
31238
|
+
state.error = r.error ?? "unknown_load_error";
|
|
31239
|
+
safeWriteLog(PLUGIN_NAME11, { phase: "load.failed", error: state.error, path: r.path });
|
|
31240
|
+
}
|
|
31241
|
+
}
|
|
31242
|
+
var MODEL_ERR_PATTERNS = [
|
|
31243
|
+
/rate.?limit/i,
|
|
31244
|
+
/quota/i,
|
|
31245
|
+
/\b(429|503)\b/,
|
|
31246
|
+
/unavailable/i,
|
|
31247
|
+
/context.*length/i,
|
|
31248
|
+
/model.*not.*found/i,
|
|
31249
|
+
/overload/i,
|
|
31250
|
+
/timeout/i
|
|
31251
|
+
];
|
|
31252
|
+
function looksLikeModelError(message) {
|
|
31253
|
+
if (!message)
|
|
31254
|
+
return false;
|
|
31255
|
+
return MODEL_ERR_PATTERNS.some((re) => re.test(message));
|
|
31256
|
+
}
|
|
31257
|
+
function buildFallbackMeta(cfg, agent, currentModel) {
|
|
31258
|
+
const r = resolveAgentModel(cfg, agent);
|
|
31259
|
+
if (!r)
|
|
31260
|
+
return null;
|
|
31261
|
+
const idx = r.chain.indexOf(currentModel);
|
|
31262
|
+
const next = idx >= 0 ? r.chain[idx + 1] ?? null : r.chain[0] ?? null;
|
|
31263
|
+
return {
|
|
31264
|
+
agent,
|
|
31265
|
+
current_model: currentModel,
|
|
31266
|
+
resolved: r,
|
|
31267
|
+
next_fallback: next,
|
|
31268
|
+
chain: r.chain,
|
|
31269
|
+
source: r.source
|
|
31270
|
+
};
|
|
31271
|
+
}
|
|
31272
|
+
function buildSuggestion(meta, errorMessage2) {
|
|
31273
|
+
const head = `⚠️ \`${meta.agent}\` agent 调用 \`${meta.current_model}\` 失败`;
|
|
31274
|
+
const reason = errorMessage2 ? `(${errorMessage2.slice(0, 80)})` : "";
|
|
31275
|
+
if (!meta.next_fallback) {
|
|
31276
|
+
return `${head}${reason}
|
|
31277
|
+
fallback 链已用尽:${meta.chain.join(" → ")}`;
|
|
31278
|
+
}
|
|
31279
|
+
return `${head}${reason}
|
|
31280
|
+
建议手动切换:\`/model-switch ${meta.agent} ${meta.next_fallback}\`
|
|
31281
|
+
完整链:${meta.chain.join(" → ")}`;
|
|
31282
|
+
}
|
|
31283
|
+
async function safeToast(client, message, variant) {
|
|
31284
|
+
const c = client;
|
|
31285
|
+
if (typeof c?.tui?.showToast !== "function")
|
|
31286
|
+
return;
|
|
31287
|
+
try {
|
|
31288
|
+
await c.tui.showToast({ body: { message, variant } });
|
|
31289
|
+
} catch {}
|
|
31290
|
+
}
|
|
31291
|
+
async function runFallback(s, errorMessage2, rc) {
|
|
31292
|
+
const now = Date.now();
|
|
31293
|
+
if (s.inflightReplay || inflightReplay.has(s.sessionID))
|
|
31294
|
+
return false;
|
|
31295
|
+
if (now - s.lastReplayAt < SELF_ABORT_WINDOW_MS && s.lastReplayAt > 0)
|
|
31296
|
+
return false;
|
|
31297
|
+
const resolved = resolveAgentModel(rc.cfg, s.agent);
|
|
31298
|
+
const chain = resolved?.chain ?? [];
|
|
31299
|
+
const plan = planFallback(s, chain, { maxAttempts: rc.maxAttempts, errorMessage: errorMessage2, now });
|
|
31300
|
+
if (!plan) {
|
|
31301
|
+
const meta = buildFallbackMeta(rc.cfg, s.agent, s.currentModel);
|
|
31302
|
+
const suggestion = meta ? buildSuggestion(meta, errorMessage2) : `⚠️ ${s.agent} fallback 链已用尽`;
|
|
31303
|
+
if (rc.notify)
|
|
31304
|
+
await safeToast(rc.client, `fallback 链已用尽(${s.agent})`, "error");
|
|
31305
|
+
safeWriteLog(PLUGIN_NAME11, { hook: "fallback.exhausted", agent: s.agent, model: s.currentModel, suggestion });
|
|
31306
|
+
return false;
|
|
31307
|
+
}
|
|
31308
|
+
beginPlannedFallback(s, plan, now);
|
|
31309
|
+
const userParts = await fetchLastUserParts(rc.client, s.sessionID, s.directory);
|
|
31310
|
+
if (!userParts || userParts.length === 0) {
|
|
31311
|
+
rollbackFallback(s, plan, { abortFailed: true, now: Date.now() });
|
|
31312
|
+
const meta = buildFallbackMeta(rc.cfg, s.agent, s.currentModel);
|
|
31313
|
+
const suggestion = meta ? buildSuggestion(meta, errorMessage2) : "";
|
|
31314
|
+
if (rc.notify)
|
|
31315
|
+
await safeToast(rc.client, `无法获取上次输入,请手动 /model-switch ${s.agent} ${plan.nextModel}`, "error");
|
|
31316
|
+
safeWriteLog(PLUGIN_NAME11, { hook: "fallback.no_parts", agent: s.agent, suggestion });
|
|
31317
|
+
return false;
|
|
31318
|
+
}
|
|
31319
|
+
const degraded = degradeParts(userParts, plan.tier);
|
|
31320
|
+
beginReplaying(s, Date.now());
|
|
31321
|
+
inflightReplay.add(s.sessionID);
|
|
31322
|
+
let result;
|
|
31323
|
+
try {
|
|
31324
|
+
result = await executeReplay(rc.client, {
|
|
31325
|
+
sessionID: s.sessionID,
|
|
31326
|
+
model: plan.nextModel,
|
|
31327
|
+
agent: s.agent,
|
|
31328
|
+
parts: degraded.parts,
|
|
31329
|
+
directory: s.directory
|
|
31330
|
+
});
|
|
31331
|
+
} finally {
|
|
31332
|
+
inflightReplay.delete(s.sessionID);
|
|
31333
|
+
}
|
|
31334
|
+
if (result.aborted && result.prompted) {
|
|
31335
|
+
commitFallback(s, plan, Date.now());
|
|
31336
|
+
if (rc.notify)
|
|
31337
|
+
await safeToast(rc.client, `已用原始问题切换到 ${plan.nextModel} 重试(${s.agent})`, "warning");
|
|
31338
|
+
safeWriteLog(PLUGIN_NAME11, {
|
|
31339
|
+
hook: "fallback.committed",
|
|
31340
|
+
agent: s.agent,
|
|
31341
|
+
from: s.originalModel,
|
|
31342
|
+
to: plan.nextModel,
|
|
31343
|
+
tier: plan.tier,
|
|
31344
|
+
dropped: degraded.dropped.length
|
|
31345
|
+
});
|
|
31346
|
+
return true;
|
|
31347
|
+
}
|
|
31348
|
+
rollbackFallback(s, plan, { abortFailed: !result.aborted, now: Date.now() });
|
|
31349
|
+
if (rc.notify)
|
|
31350
|
+
await safeToast(rc.client, `切换 ${plan.nextModel} 失败(${s.agent}),已回滚`, "error");
|
|
31351
|
+
safeWriteLog(PLUGIN_NAME11, {
|
|
31352
|
+
hook: "fallback.rolled_back",
|
|
31353
|
+
agent: s.agent,
|
|
31354
|
+
target: plan.nextModel,
|
|
31355
|
+
aborted: result.aborted,
|
|
31356
|
+
error: result.error
|
|
31357
|
+
});
|
|
31358
|
+
return false;
|
|
31359
|
+
}
|
|
31360
|
+
function assistantProducedNothing(messages) {
|
|
31361
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
31362
|
+
return false;
|
|
31363
|
+
let last = null;
|
|
31364
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
31365
|
+
const m = messages[i];
|
|
31366
|
+
const role = m?.role ?? m?.info?.role;
|
|
31367
|
+
if (role === "assistant") {
|
|
31368
|
+
last = m;
|
|
31369
|
+
break;
|
|
31370
|
+
}
|
|
31371
|
+
if (role === "user") {
|
|
31372
|
+
return true;
|
|
31373
|
+
}
|
|
31374
|
+
}
|
|
31375
|
+
if (!last)
|
|
31376
|
+
return false;
|
|
31377
|
+
const parts = Array.isArray(last.parts) ? last.parts : [];
|
|
31378
|
+
for (const p of parts) {
|
|
31379
|
+
const t = p && typeof p === "object" ? p.type : undefined;
|
|
31380
|
+
if (t === "text" && typeof p.text === "string" && p.text.trim())
|
|
31381
|
+
return false;
|
|
31382
|
+
if (t === "tool" || t === "tool-call" || t === "tool_use")
|
|
31383
|
+
return false;
|
|
31384
|
+
}
|
|
31385
|
+
return true;
|
|
31386
|
+
}
|
|
31387
|
+
async function fetchMessagesRaw(client, sessionID, directory) {
|
|
31388
|
+
const c = client;
|
|
31389
|
+
const fn = c?.session?.messages;
|
|
31390
|
+
if (typeof fn !== "function")
|
|
31391
|
+
return null;
|
|
31392
|
+
try {
|
|
31393
|
+
const res = await fn({
|
|
31394
|
+
path: { id: sessionID },
|
|
31395
|
+
query: directory ? { directory } : undefined
|
|
31396
|
+
});
|
|
31397
|
+
if (res && typeof res === "object" && "error" in res && res.error)
|
|
31398
|
+
return null;
|
|
31399
|
+
const data = res && typeof res === "object" ? res.data : undefined;
|
|
31400
|
+
return Array.isArray(data) ? data : null;
|
|
31401
|
+
} catch {
|
|
31402
|
+
return null;
|
|
31403
|
+
}
|
|
31404
|
+
}
|
|
31405
|
+
var modelFallbackServer = async (ctx) => {
|
|
31406
|
+
const log8 = makePluginLogger(PLUGIN_NAME11);
|
|
31407
|
+
const directory = ctx.directory ?? process.cwd();
|
|
31408
|
+
loadOnce(directory);
|
|
31409
|
+
logLifecycle(PLUGIN_NAME11, "activate", {
|
|
31410
|
+
directory: ctx.directory,
|
|
31411
|
+
config_path: state.configPath,
|
|
31412
|
+
config_loaded: state.config !== null,
|
|
31413
|
+
config_error: state.error,
|
|
31414
|
+
agents: state.config ? Object.keys(state.config.agents) : [],
|
|
31415
|
+
runtime_fallback: state.config?.runtime_fallback ?? null
|
|
31416
|
+
});
|
|
31417
|
+
function runtimeEnabled() {
|
|
31418
|
+
const rf = state.config?.runtime_fallback;
|
|
31419
|
+
return rf ? rf.enabled !== false : true;
|
|
31420
|
+
}
|
|
31421
|
+
function eventEnabled(type) {
|
|
31422
|
+
const triggerEvents = state.config?.runtime_fallback?.trigger_events;
|
|
31423
|
+
if (!triggerEvents || triggerEvents.length === 0)
|
|
31424
|
+
return true;
|
|
31425
|
+
return triggerEvents.includes(type);
|
|
31426
|
+
}
|
|
31427
|
+
function notifyEnabled() {
|
|
31428
|
+
const rf = state.config?.runtime_fallback;
|
|
31429
|
+
if (!rf)
|
|
31430
|
+
return true;
|
|
31431
|
+
return rf.notify_on_fallback !== false;
|
|
31432
|
+
}
|
|
31433
|
+
function maxAttempts() {
|
|
31434
|
+
return state.config?.runtime_fallback?.max_fallback_attempts ?? 3;
|
|
31435
|
+
}
|
|
31436
|
+
return {
|
|
31437
|
+
"chat.params": async (input, output) => {
|
|
31438
|
+
await safeAsync(PLUGIN_NAME11, "chat.params", async () => {
|
|
31439
|
+
if (!state.config)
|
|
31440
|
+
return;
|
|
31441
|
+
const agent = input.agent;
|
|
31442
|
+
const modelObj = input.model;
|
|
31443
|
+
if (!agent || !modelObj)
|
|
31444
|
+
return;
|
|
31445
|
+
const providerID = modelObj.providerID ?? "";
|
|
31446
|
+
const modelID = modelObj.modelID ?? modelObj.id ?? "";
|
|
31447
|
+
if (!providerID || !modelID)
|
|
31448
|
+
return;
|
|
31449
|
+
const currentModel = `${providerID}/${modelID}`;
|
|
31450
|
+
const meta = buildFallbackMeta(state.config, agent, currentModel);
|
|
31451
|
+
if (!meta)
|
|
31452
|
+
return;
|
|
31453
|
+
output.options = output.options ?? {};
|
|
31454
|
+
output.options.codeforge_fallback = {
|
|
31455
|
+
chain: meta.chain,
|
|
31456
|
+
next: meta.next_fallback,
|
|
31457
|
+
source: meta.source
|
|
31458
|
+
};
|
|
31459
|
+
const sid = input.sessionID;
|
|
31460
|
+
if (typeof sid === "string" && sid) {
|
|
31461
|
+
markDispatched(sessionFallbacks, {
|
|
31462
|
+
sessionID: sid,
|
|
31463
|
+
agent,
|
|
31464
|
+
model: currentModel,
|
|
31465
|
+
directory
|
|
31466
|
+
});
|
|
31467
|
+
}
|
|
31468
|
+
safeWriteLog(PLUGIN_NAME11, {
|
|
31469
|
+
hook: "chat.params",
|
|
31470
|
+
agent,
|
|
31471
|
+
model: currentModel,
|
|
31472
|
+
chain: meta.chain,
|
|
31473
|
+
next: meta.next_fallback
|
|
31474
|
+
});
|
|
31475
|
+
});
|
|
31476
|
+
},
|
|
31477
|
+
event: async ({ event }) => {
|
|
31478
|
+
await safeAsync(PLUGIN_NAME11, "event", async () => {
|
|
31479
|
+
if (!state.config)
|
|
31480
|
+
return;
|
|
31481
|
+
if (!runtimeEnabled())
|
|
31482
|
+
return;
|
|
31483
|
+
const e = event;
|
|
31484
|
+
if (!e || typeof e.type !== "string")
|
|
31485
|
+
return;
|
|
31486
|
+
sweepStale(sessionFallbacks, STALE_TTL_MS);
|
|
31487
|
+
const rc = {
|
|
31488
|
+
client: ctx.client,
|
|
31489
|
+
directory,
|
|
31490
|
+
cfg: state.config,
|
|
31491
|
+
maxAttempts: maxAttempts(),
|
|
31492
|
+
notify: notifyEnabled()
|
|
31493
|
+
};
|
|
31494
|
+
if (e.type === "message.updated") {
|
|
31495
|
+
const props = e.properties ?? {};
|
|
31496
|
+
const sid = typeof props["sessionID"] === "string" && props["sessionID"] || (() => {
|
|
31497
|
+
const info = props["info"];
|
|
31498
|
+
if (info && typeof info === "object") {
|
|
31499
|
+
const s2 = info.sessionID;
|
|
31500
|
+
if (typeof s2 === "string")
|
|
31501
|
+
return s2;
|
|
31502
|
+
}
|
|
31503
|
+
return "";
|
|
31504
|
+
})();
|
|
31505
|
+
if (sid) {
|
|
31506
|
+
const s = sessionFallbacks.get(sid);
|
|
31507
|
+
if (s && s.awaitingFirstAssistantOutput)
|
|
31508
|
+
markFirstOutput(s);
|
|
31509
|
+
}
|
|
31510
|
+
return;
|
|
31511
|
+
}
|
|
31512
|
+
if (e.type === "session.deleted") {
|
|
31513
|
+
const ended = extractEndedSessionID(event);
|
|
31514
|
+
if (ended)
|
|
31515
|
+
sessionFallbacks.delete(ended.sessionID);
|
|
31516
|
+
return;
|
|
31517
|
+
}
|
|
31518
|
+
if (e.type === "session.error" && eventEnabled("session.error")) {
|
|
31519
|
+
const ended = extractEndedSessionID(event);
|
|
31520
|
+
if (!ended)
|
|
31521
|
+
return;
|
|
31522
|
+
const message = ended.errorReason ?? "";
|
|
31523
|
+
if (!looksLikeModelError(message))
|
|
31524
|
+
return;
|
|
31525
|
+
let s = sessionFallbacks.get(ended.sessionID);
|
|
31526
|
+
if (!s) {
|
|
31527
|
+
const agent = "unknown";
|
|
31528
|
+
const meta = buildFallbackMeta(state.config, agent, "unknown/unknown");
|
|
31529
|
+
const suggestion = meta ? buildSuggestion(meta, message) : `⚠️ ${message}`;
|
|
31530
|
+
log8.warn(`[${PLUGIN_NAME11}] ${suggestion}`);
|
|
31531
|
+
safeWriteLog(PLUGIN_NAME11, { hook: "event.error.no_state", sessionID: ended.sessionID, message });
|
|
31532
|
+
return;
|
|
31060
31533
|
}
|
|
31061
|
-
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
|
|
31071
|
-
|
|
31072
|
-
|
|
31534
|
+
s.lastActiveAt = Date.now();
|
|
31535
|
+
await runFallback(s, message, rc);
|
|
31536
|
+
return;
|
|
31537
|
+
}
|
|
31538
|
+
if (e.type === "session.idle" && eventEnabled("session.idle")) {
|
|
31539
|
+
const ended = extractEndedSessionID(event);
|
|
31540
|
+
if (!ended)
|
|
31541
|
+
return;
|
|
31542
|
+
const s = sessionFallbacks.get(ended.sessionID);
|
|
31543
|
+
if (!s)
|
|
31544
|
+
return;
|
|
31545
|
+
s.lastActiveAt = Date.now();
|
|
31546
|
+
if (s.phase === "pending_output" && s.awaitingFirstAssistantOutput) {
|
|
31547
|
+
const msgs = await fetchMessagesRaw(ctx.client, s.sessionID, s.directory);
|
|
31548
|
+
if (assistantProducedNothing(msgs)) {
|
|
31549
|
+
await runFallback(s, "silent_no_output", rc);
|
|
31550
|
+
return;
|
|
31551
|
+
}
|
|
31552
|
+
markFirstOutput(s);
|
|
31553
|
+
}
|
|
31554
|
+
const recovered = recoverToOriginal(s, COOLDOWN_SEC);
|
|
31555
|
+
if (recovered) {
|
|
31556
|
+
await safeToast(ctx.client, `cooldown 结束,切回主模型 ${s.originalModel}(${s.agent})`, "info");
|
|
31557
|
+
safeWriteLog(PLUGIN_NAME11, { hook: "fallback.recovered", agent: s.agent, model: s.originalModel });
|
|
31558
|
+
}
|
|
31559
|
+
return;
|
|
31073
31560
|
}
|
|
31074
31561
|
});
|
|
31075
31562
|
}
|
|
31076
31563
|
};
|
|
31077
31564
|
};
|
|
31078
|
-
var
|
|
31565
|
+
var handler11 = modelFallbackServer;
|
|
31079
31566
|
|
|
31080
|
-
// plugins/
|
|
31081
|
-
import {
|
|
31082
|
-
import
|
|
31083
|
-
|
|
31084
|
-
var
|
|
31085
|
-
|
|
31086
|
-
var
|
|
31087
|
-
|
|
31088
|
-
|
|
31089
|
-
|
|
31090
|
-
|
|
31091
|
-
|
|
31092
|
-
var
|
|
31093
|
-
var
|
|
31094
|
-
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
var inflight2 = new Map;
|
|
31102
|
-
var pendingTask = new Map;
|
|
31103
|
-
var _parentParseFailLogged = 0;
|
|
31104
|
-
function detectUnparsedParentID(event) {
|
|
31105
|
-
if (!event || typeof event !== "object")
|
|
31106
|
-
return false;
|
|
31107
|
-
const e = event;
|
|
31108
|
-
if (e.type !== "session.created")
|
|
31109
|
-
return false;
|
|
31110
|
-
if (extractCreatedChild(event))
|
|
31111
|
-
return false;
|
|
31567
|
+
// plugins/parallel-tool-nudge.ts
|
|
31568
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
31569
|
+
import { join as join21 } from "node:path";
|
|
31570
|
+
import { homedir as homedir8 } from "node:os";
|
|
31571
|
+
var PLUGIN_NAME12 = "parallel-tool-nudge";
|
|
31572
|
+
logLifecycle(PLUGIN_NAME12, "import", {});
|
|
31573
|
+
var PARALLEL_SAFE_TOOLS = [
|
|
31574
|
+
"read",
|
|
31575
|
+
"repo_map",
|
|
31576
|
+
"webfetch"
|
|
31577
|
+
];
|
|
31578
|
+
var DEFAULT_AGENT_KEY = "__default__";
|
|
31579
|
+
var NUDGE_MAX_LEN2 = 800;
|
|
31580
|
+
var agentToolsMap = new Map;
|
|
31581
|
+
function defaultReader2(p) {
|
|
31582
|
+
return readFileSync4(p, "utf8");
|
|
31583
|
+
}
|
|
31584
|
+
function defaultDirReader2(p) {
|
|
31585
|
+
return readdirSync2(p);
|
|
31586
|
+
}
|
|
31587
|
+
function defaultDirExists2(p) {
|
|
31112
31588
|
try {
|
|
31113
|
-
|
|
31114
|
-
return /\bparent[_-]?[iI][dD]\b/.test(json2);
|
|
31589
|
+
return statSync3(p).isDirectory();
|
|
31115
31590
|
} catch {
|
|
31116
31591
|
return false;
|
|
31117
31592
|
}
|
|
31118
31593
|
}
|
|
31119
|
-
function
|
|
31120
|
-
|
|
31121
|
-
|
|
31122
|
-
const e = event;
|
|
31123
|
-
if (e.type !== "session.created")
|
|
31594
|
+
function parseAgentFrontmatter(content) {
|
|
31595
|
+
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
31596
|
+
if (!fmMatch || !fmMatch[1])
|
|
31124
31597
|
return null;
|
|
31125
|
-
|
|
31126
|
-
|
|
31598
|
+
let parsed;
|
|
31599
|
+
try {
|
|
31600
|
+
parsed = $parse(fmMatch[1]);
|
|
31601
|
+
} catch {
|
|
31127
31602
|
return null;
|
|
31128
|
-
|
|
31129
|
-
if (typeof
|
|
31603
|
+
}
|
|
31604
|
+
if (!parsed || typeof parsed !== "object")
|
|
31130
31605
|
return null;
|
|
31131
|
-
|
|
31606
|
+
const obj = parsed;
|
|
31607
|
+
const name = typeof obj["name"] === "string" ? obj["name"] : null;
|
|
31608
|
+
const tools = obj["allowed_tools"];
|
|
31609
|
+
if (!name || !Array.isArray(tools))
|
|
31132
31610
|
return null;
|
|
31133
|
-
|
|
31134
|
-
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
function sanitize(s) {
|
|
31138
|
-
const folded = s.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
31139
|
-
if (folded.length <= ERROR_REASON_MAX_LEN)
|
|
31140
|
-
return folded;
|
|
31141
|
-
return folded.slice(0, ERROR_REASON_MAX_LEN) + "…";
|
|
31611
|
+
const allowedTools = [];
|
|
31612
|
+
for (const t of tools) {
|
|
31613
|
+
if (typeof t === "string")
|
|
31614
|
+
allowedTools.push(t);
|
|
31142
31615
|
}
|
|
31143
|
-
|
|
31144
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
31147
|
-
|
|
31148
|
-
|
|
31149
|
-
|
|
31150
|
-
|
|
31151
|
-
|
|
31616
|
+
return { name, allowedTools };
|
|
31617
|
+
}
|
|
31618
|
+
function loadAgentToolsMap(rootDir, opts = {}) {
|
|
31619
|
+
const reader = opts.reader ?? defaultReader2;
|
|
31620
|
+
const dirReader = opts.dirReader ?? defaultDirReader2;
|
|
31621
|
+
const dirExists = opts.dirExists ?? defaultDirExists2;
|
|
31622
|
+
const homeAgentsDir = opts.homeAgentsDir ?? join21(homedir8(), ".config", "opencode", "agents");
|
|
31623
|
+
const candidateDirs = [
|
|
31624
|
+
join21(rootDir, ".codeforge", "agents"),
|
|
31625
|
+
join21(rootDir, "agents"),
|
|
31626
|
+
homeAgentsDir
|
|
31627
|
+
];
|
|
31628
|
+
const result = new Map;
|
|
31629
|
+
const safeSet = new Set(PARALLEL_SAFE_TOOLS);
|
|
31630
|
+
const unionTools = new Set;
|
|
31631
|
+
const log8 = makePluginLogger(PLUGIN_NAME12);
|
|
31632
|
+
for (const dir of candidateDirs) {
|
|
31633
|
+
if (!dirExists(dir))
|
|
31634
|
+
continue;
|
|
31635
|
+
let entries;
|
|
31636
|
+
try {
|
|
31637
|
+
entries = dirReader(dir);
|
|
31638
|
+
} catch (err) {
|
|
31639
|
+
log8.warn(`agents 目录读取失败(已跳过)`, {
|
|
31640
|
+
dir,
|
|
31641
|
+
error: err instanceof Error ? err.message : String(err)
|
|
31642
|
+
});
|
|
31643
|
+
continue;
|
|
31644
|
+
}
|
|
31645
|
+
for (const entry of entries) {
|
|
31646
|
+
if (!entry.endsWith(".md"))
|
|
31647
|
+
continue;
|
|
31648
|
+
const path24 = join21(dir, entry);
|
|
31649
|
+
let content;
|
|
31650
|
+
try {
|
|
31651
|
+
content = reader(path24);
|
|
31652
|
+
} catch (err) {
|
|
31653
|
+
log8.warn(`agent.md 读取失败(已跳过)`, {
|
|
31654
|
+
path: path24,
|
|
31655
|
+
error: err instanceof Error ? err.message : String(err)
|
|
31656
|
+
});
|
|
31657
|
+
continue;
|
|
31658
|
+
}
|
|
31659
|
+
const parsed = parseAgentFrontmatter(content);
|
|
31660
|
+
if (!parsed) {
|
|
31661
|
+
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path24 });
|
|
31662
|
+
continue;
|
|
31663
|
+
}
|
|
31664
|
+
if (result.has(parsed.name))
|
|
31665
|
+
continue;
|
|
31666
|
+
const intersect = [];
|
|
31667
|
+
const seen = new Set;
|
|
31668
|
+
for (const t of parsed.allowedTools) {
|
|
31669
|
+
if (safeSet.has(t) && !seen.has(t)) {
|
|
31670
|
+
intersect.push(t);
|
|
31671
|
+
seen.add(t);
|
|
31672
|
+
unionTools.add(t);
|
|
31673
|
+
}
|
|
31674
|
+
}
|
|
31675
|
+
result.set(parsed.name, intersect);
|
|
31152
31676
|
}
|
|
31153
|
-
return null;
|
|
31154
31677
|
}
|
|
31155
|
-
|
|
31156
|
-
|
|
31157
|
-
|
|
31158
|
-
|
|
31159
|
-
|
|
31160
|
-
|
|
31161
|
-
|
|
31162
|
-
|
|
31163
|
-
const
|
|
31164
|
-
if (
|
|
31165
|
-
|
|
31678
|
+
result.set(DEFAULT_AGENT_KEY, Array.from(unionTools));
|
|
31679
|
+
return result;
|
|
31680
|
+
}
|
|
31681
|
+
var sessionAgentMap2 = new Map;
|
|
31682
|
+
var SESSION_CAP3 = 200;
|
|
31683
|
+
var SESSION_TTL_MS3 = 24 * 60 * 60 * 1000;
|
|
31684
|
+
function pruneIfOversize3() {
|
|
31685
|
+
while (sessionAgentMap2.size > SESSION_CAP3) {
|
|
31686
|
+
const oldestKey = sessionAgentMap2.keys().next().value;
|
|
31687
|
+
if (oldestKey === undefined)
|
|
31688
|
+
break;
|
|
31689
|
+
sessionAgentMap2.delete(oldestKey);
|
|
31166
31690
|
}
|
|
31167
|
-
return null;
|
|
31168
31691
|
}
|
|
31169
|
-
function
|
|
31170
|
-
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
if (
|
|
31174
|
-
return
|
|
31175
|
-
|
|
31176
|
-
|
|
31692
|
+
function isExpired3(entry, now) {
|
|
31693
|
+
return now - entry.ts > SESSION_TTL_MS3;
|
|
31694
|
+
}
|
|
31695
|
+
function resolveAgent(sessionID, nowFn = Date.now) {
|
|
31696
|
+
if (!sessionID)
|
|
31697
|
+
return "unknown";
|
|
31698
|
+
const entry = sessionAgentMap2.get(sessionID);
|
|
31699
|
+
if (!entry)
|
|
31700
|
+
return "unknown";
|
|
31701
|
+
const now = nowFn();
|
|
31702
|
+
if (isExpired3(entry, now)) {
|
|
31703
|
+
sessionAgentMap2.delete(sessionID);
|
|
31704
|
+
return "unknown";
|
|
31705
|
+
}
|
|
31706
|
+
sessionAgentMap2.delete(sessionID);
|
|
31707
|
+
sessionAgentMap2.set(sessionID, entry);
|
|
31708
|
+
return entry.agent;
|
|
31709
|
+
}
|
|
31710
|
+
function renderNudge(agent, tools) {
|
|
31711
|
+
const isUnknown = agent === "unknown";
|
|
31712
|
+
const agentLine = isUnknown ? `You are running as a CodeForge agent (specific agent unknown for this turn).` : `You are the \`${agent}\` agent.`;
|
|
31713
|
+
const toolsBulleted = tools.length > 0 ? tools.map((t) => ` - \`${t}\``).join(`
|
|
31714
|
+
`) : ` - (no parallel-safe tools registered for this agent)`;
|
|
31715
|
+
const body = `────────────────────────────────────────
|
|
31716
|
+
PARALLEL TOOL CALL DIRECTIVE (auto-injected by parallel-tool-nudge plugin)
|
|
31717
|
+
────────────────────────────────────────
|
|
31718
|
+
${agentLine} The opencode runtime executes tool calls in your single
|
|
31719
|
+
response **truly in parallel** (verified: up to 22 tools in 72ms).
|
|
31720
|
+
You MUST batch-emit independent read-only tool calls in one response
|
|
31721
|
+
whenever possible.
|
|
31722
|
+
|
|
31723
|
+
Parallel-safe tools available to you:
|
|
31724
|
+
${toolsBulleted}
|
|
31725
|
+
|
|
31726
|
+
Heuristics:
|
|
31727
|
+
- Need to read 3 files? → emit 3 \`read\` calls in ONE response, not 3 sequential turns
|
|
31728
|
+
- Need to read + map? → emit \`read\` + \`repo_map\` in ONE response
|
|
31729
|
+
- DO NOT batch write tools (edit / write / ast_edit / bash)
|
|
31730
|
+
- DO NOT chain reads where each read's path depends on previous read's output
|
|
31731
|
+
|
|
31732
|
+
This directive is re-injected every turn — it is not optional advice.
|
|
31733
|
+
────────────────────────────────────────`;
|
|
31734
|
+
if (body.length <= NUDGE_MAX_LEN2)
|
|
31735
|
+
return body;
|
|
31736
|
+
return body.slice(0, NUDGE_MAX_LEN2 - 4) + `
|
|
31737
|
+
…`;
|
|
31738
|
+
}
|
|
31739
|
+
var log8 = makePluginLogger(PLUGIN_NAME12);
|
|
31740
|
+
var parallelToolNudgeServer = async (ctx) => {
|
|
31741
|
+
try {
|
|
31742
|
+
const loaded = loadAgentToolsMap(ctx.directory ?? process.cwd());
|
|
31743
|
+
agentToolsMap.clear();
|
|
31744
|
+
for (const [k, v] of loaded.entries())
|
|
31745
|
+
agentToolsMap.set(k, v);
|
|
31746
|
+
} catch (err) {
|
|
31747
|
+
log8.warn(`loadAgentToolsMap 失败(plugin 将 no-op)`, {
|
|
31748
|
+
error: err instanceof Error ? err.message : String(err)
|
|
31749
|
+
});
|
|
31177
31750
|
}
|
|
31178
|
-
const
|
|
31179
|
-
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31751
|
+
const realAgentCount = Math.max(0, agentToolsMap.size - (agentToolsMap.has(DEFAULT_AGENT_KEY) ? 1 : 0));
|
|
31752
|
+
if (realAgentCount === 0) {
|
|
31753
|
+
agentToolsMap.clear();
|
|
31754
|
+
log8.warn(`0 real agents loaded; plugin will be no-op for this session`, {
|
|
31755
|
+
directory: ctx.directory
|
|
31756
|
+
});
|
|
31757
|
+
}
|
|
31758
|
+
const defaultTools = agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
|
|
31759
|
+
logLifecycle(PLUGIN_NAME12, "activate", {
|
|
31760
|
+
directory: ctx.directory,
|
|
31761
|
+
real_agents_loaded: realAgentCount,
|
|
31762
|
+
default_tools_union: defaultTools,
|
|
31763
|
+
parallel_safe_whitelist: PARALLEL_SAFE_TOOLS,
|
|
31764
|
+
session_cap: SESSION_CAP3,
|
|
31765
|
+
session_ttl_ms: SESSION_TTL_MS3,
|
|
31766
|
+
no_op: agentToolsMap.size === 0
|
|
31767
|
+
});
|
|
31768
|
+
return {
|
|
31769
|
+
"chat.params": async (input, _output) => {
|
|
31770
|
+
await safeAsync(PLUGIN_NAME12, "chat.params", async () => {
|
|
31771
|
+
if (agentToolsMap.size === 0)
|
|
31772
|
+
return;
|
|
31773
|
+
const sid = input?.sessionID;
|
|
31774
|
+
const agent = input?.agent;
|
|
31775
|
+
if (!sid || !agent)
|
|
31776
|
+
return;
|
|
31777
|
+
sessionAgentMap2.set(sid, { agent, ts: Date.now() });
|
|
31778
|
+
pruneIfOversize3();
|
|
31779
|
+
});
|
|
31780
|
+
},
|
|
31781
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
31782
|
+
await safeAsync(PLUGIN_NAME12, "experimental.chat.system.transform", async () => {
|
|
31783
|
+
if (agentToolsMap.size === 0)
|
|
31784
|
+
return;
|
|
31785
|
+
if (!output || !Array.isArray(output.system))
|
|
31786
|
+
return;
|
|
31787
|
+
const sid = input?.sessionID;
|
|
31788
|
+
const agent = resolveAgent(sid);
|
|
31789
|
+
const tools = agent !== "unknown" ? agentToolsMap.get(agent) ?? agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [] : agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
|
|
31790
|
+
const nudge = renderNudge(agent, tools);
|
|
31791
|
+
output.system.push(nudge);
|
|
31792
|
+
safeWriteLog(PLUGIN_NAME12, {
|
|
31793
|
+
hook: "experimental.chat.system.transform",
|
|
31794
|
+
sessionID: sid,
|
|
31795
|
+
agent,
|
|
31796
|
+
injected_tools: tools,
|
|
31797
|
+
injected_chars: nudge.length,
|
|
31798
|
+
fallback: agent === "unknown"
|
|
31799
|
+
});
|
|
31800
|
+
});
|
|
31186
31801
|
}
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
31193
|
-
}
|
|
31194
|
-
|
|
31195
|
-
|
|
31196
|
-
|
|
31197
|
-
|
|
31198
|
-
|
|
31199
|
-
|
|
31200
|
-
|
|
31201
|
-
|
|
31202
|
-
if (
|
|
31203
|
-
return
|
|
31204
|
-
return
|
|
31802
|
+
};
|
|
31803
|
+
};
|
|
31804
|
+
var handler12 = parallelToolNudgeServer;
|
|
31805
|
+
|
|
31806
|
+
// plugins/pwsh-utf8.ts
|
|
31807
|
+
var PLUGIN_NAME13 = "pwsh-utf8";
|
|
31808
|
+
logLifecycle(PLUGIN_NAME13, "import", {});
|
|
31809
|
+
var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
|
|
31810
|
+
function prependUtf8Prelude(command) {
|
|
31811
|
+
if (typeof command !== "string")
|
|
31812
|
+
return;
|
|
31813
|
+
if (command.length === 0)
|
|
31814
|
+
return command;
|
|
31815
|
+
if (/chcp\s+65001/i.test(command))
|
|
31816
|
+
return command;
|
|
31817
|
+
if (/\[Console\]::OutputEncoding\s*=/i.test(command))
|
|
31818
|
+
return command;
|
|
31819
|
+
return PRELUDE + command;
|
|
31205
31820
|
}
|
|
31206
|
-
|
|
31207
|
-
const
|
|
31208
|
-
|
|
31209
|
-
|
|
31210
|
-
|
|
31211
|
-
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
if (
|
|
31216
|
-
|
|
31217
|
-
|
|
31821
|
+
var handler13 = async (_ctx4) => {
|
|
31822
|
+
const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
|
|
31823
|
+
const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
|
|
31824
|
+
logLifecycle(PLUGIN_NAME13, "activate", { enabled, platform: process.platform, reason });
|
|
31825
|
+
if (!enabled)
|
|
31826
|
+
return {};
|
|
31827
|
+
return {
|
|
31828
|
+
"tool.execute.before": async (input, output) => {
|
|
31829
|
+
await safeAsync(PLUGIN_NAME13, "tool.execute.before", async () => {
|
|
31830
|
+
if (input.tool !== "bash")
|
|
31831
|
+
return;
|
|
31832
|
+
const args = output.args ?? {};
|
|
31833
|
+
const original = args["command"];
|
|
31834
|
+
const next = prependUtf8Prelude(original);
|
|
31835
|
+
if (next !== undefined && next !== original) {
|
|
31836
|
+
args["command"] = next;
|
|
31837
|
+
output.args = args;
|
|
31838
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
31839
|
+
hook: "tool.execute.before",
|
|
31840
|
+
tool: input.tool,
|
|
31841
|
+
callID: input.callID,
|
|
31842
|
+
sessionID: input.sessionID,
|
|
31843
|
+
injected: true
|
|
31844
|
+
});
|
|
31218
31845
|
}
|
|
31219
|
-
}
|
|
31220
|
-
if (oldestKey !== null)
|
|
31221
|
-
pendingTask.delete(oldestKey);
|
|
31846
|
+
});
|
|
31222
31847
|
}
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31848
|
+
};
|
|
31849
|
+
};
|
|
31850
|
+
|
|
31851
|
+
// lib/event-stream.ts
|
|
31852
|
+
import { promises as fs18 } from "node:fs";
|
|
31853
|
+
import * as path24 from "node:path";
|
|
31854
|
+
async function loadSession(id, opts = {}) {
|
|
31855
|
+
const file2 = resolveSessionFile(id, opts);
|
|
31856
|
+
const raw = await fs18.readFile(file2, "utf8");
|
|
31857
|
+
return parseJsonl(id, raw);
|
|
31230
31858
|
}
|
|
31231
|
-
function
|
|
31232
|
-
const
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
|
|
31240
|
-
}
|
|
31241
|
-
if (bucket.length === 0) {
|
|
31242
|
-
pendingTask.delete(parentID);
|
|
31243
|
-
return null;
|
|
31859
|
+
async function listSessions(opts = {}) {
|
|
31860
|
+
const dir = resolveDir(opts);
|
|
31861
|
+
let entries;
|
|
31862
|
+
try {
|
|
31863
|
+
entries = await fs18.readdir(dir, { withFileTypes: true });
|
|
31864
|
+
} catch (err) {
|
|
31865
|
+
if (err.code === "ENOENT")
|
|
31866
|
+
return [];
|
|
31867
|
+
throw err;
|
|
31244
31868
|
}
|
|
31245
|
-
const
|
|
31246
|
-
|
|
31247
|
-
|
|
31248
|
-
|
|
31249
|
-
|
|
31250
|
-
|
|
31251
|
-
|
|
31252
|
-
|
|
31253
|
-
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
|
|
31869
|
+
const out = [];
|
|
31870
|
+
for (const e of entries) {
|
|
31871
|
+
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
31872
|
+
continue;
|
|
31873
|
+
const file2 = path24.join(dir, e.name);
|
|
31874
|
+
const id = e.name.replace(/\.jsonl$/, "");
|
|
31875
|
+
try {
|
|
31876
|
+
const stat = await fs18.stat(file2);
|
|
31877
|
+
const headerLine = await readFirstLine(file2);
|
|
31878
|
+
let started_at = stat.birthtimeMs;
|
|
31879
|
+
if (headerLine) {
|
|
31880
|
+
try {
|
|
31881
|
+
const h = JSON.parse(headerLine);
|
|
31882
|
+
if (h.__header && typeof h.started_at === "number") {
|
|
31883
|
+
started_at = h.started_at;
|
|
31884
|
+
}
|
|
31885
|
+
} catch {}
|
|
31886
|
+
}
|
|
31887
|
+
out.push({
|
|
31888
|
+
id,
|
|
31889
|
+
file: file2,
|
|
31890
|
+
started_at,
|
|
31891
|
+
size: stat.size,
|
|
31892
|
+
mtime_ms: stat.mtimeMs
|
|
31893
|
+
});
|
|
31894
|
+
} catch {}
|
|
31259
31895
|
}
|
|
31260
|
-
|
|
31261
|
-
|
|
31262
|
-
function registerInflight(payload, now = Date.now()) {
|
|
31263
|
-
const r = {
|
|
31264
|
-
childID: payload.childID,
|
|
31265
|
-
parentID: payload.parentID,
|
|
31266
|
-
agent: payload.agent,
|
|
31267
|
-
description: payload.description ?? null,
|
|
31268
|
-
startedAt: now,
|
|
31269
|
-
lastBeatAt: now,
|
|
31270
|
-
lastTool: null,
|
|
31271
|
-
pendingCalls: new Map
|
|
31272
|
-
};
|
|
31273
|
-
inflight2.set(payload.childID, r);
|
|
31274
|
-
return r;
|
|
31896
|
+
out.sort((a, b) => b.started_at - a.started_at);
|
|
31897
|
+
return out;
|
|
31275
31898
|
}
|
|
31276
|
-
function
|
|
31277
|
-
const
|
|
31278
|
-
|
|
31279
|
-
return null;
|
|
31280
|
-
r.lastBeatAt = now;
|
|
31281
|
-
r.lastTool = tool2;
|
|
31282
|
-
return r;
|
|
31899
|
+
function resolveDir(opts = {}) {
|
|
31900
|
+
const root = path24.resolve(opts.root ?? process.cwd());
|
|
31901
|
+
return opts.sessions_dir ? path24.resolve(root, opts.sessions_dir) : path24.join(runtimeDir(root), "sessions");
|
|
31283
31902
|
}
|
|
31284
|
-
function
|
|
31285
|
-
|
|
31286
|
-
if (!r)
|
|
31287
|
-
return null;
|
|
31288
|
-
inflight2.delete(sessionID);
|
|
31289
|
-
return r;
|
|
31903
|
+
function resolveSessionFile(id, opts = {}) {
|
|
31904
|
+
return path24.join(resolveDir(opts), `${id}.jsonl`);
|
|
31290
31905
|
}
|
|
31291
|
-
function
|
|
31292
|
-
const
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31906
|
+
function parseJsonl(id, raw) {
|
|
31907
|
+
const events = [];
|
|
31908
|
+
let started_at = null;
|
|
31909
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
31910
|
+
if (!line.trim())
|
|
31911
|
+
continue;
|
|
31912
|
+
let obj;
|
|
31913
|
+
try {
|
|
31914
|
+
obj = JSON.parse(line);
|
|
31915
|
+
} catch {
|
|
31916
|
+
continue;
|
|
31917
|
+
}
|
|
31918
|
+
if (!obj || typeof obj !== "object")
|
|
31919
|
+
continue;
|
|
31920
|
+
if (obj.__header) {
|
|
31921
|
+
const h = obj;
|
|
31922
|
+
if (typeof h.started_at === "number")
|
|
31923
|
+
started_at = h.started_at;
|
|
31924
|
+
continue;
|
|
31925
|
+
}
|
|
31926
|
+
if (isEvent(obj))
|
|
31927
|
+
events.push(obj);
|
|
31296
31928
|
}
|
|
31297
|
-
return
|
|
31298
|
-
}
|
|
31299
|
-
function fmtElapsed(ms) {
|
|
31300
|
-
const total = Math.max(0, Math.floor(ms / 1000));
|
|
31301
|
-
const m = Math.floor(total / 60);
|
|
31302
|
-
const s = total % 60;
|
|
31303
|
-
return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
|
|
31929
|
+
return { id, started_at, events };
|
|
31304
31930
|
}
|
|
31305
|
-
function
|
|
31306
|
-
if (!
|
|
31307
|
-
return
|
|
31308
|
-
|
|
31931
|
+
function isEvent(obj) {
|
|
31932
|
+
if (!obj || typeof obj !== "object")
|
|
31933
|
+
return false;
|
|
31934
|
+
const e = obj;
|
|
31935
|
+
return typeof e.id === "string" && typeof e.timestamp === "number" && typeof e.session_id === "string" && typeof e.agent === "string" && typeof e.mode === "string" && typeof e.type === "string" && (e.type === "tool_call" || e.type === "message" || e.type === "agent_switch");
|
|
31309
31936
|
}
|
|
31310
|
-
function
|
|
31311
|
-
const
|
|
31312
|
-
|
|
31313
|
-
|
|
31314
|
-
|
|
31937
|
+
async function readFirstLine(file2) {
|
|
31938
|
+
const buf = Buffer.alloc(4096);
|
|
31939
|
+
const fh = await fs18.open(file2, "r");
|
|
31940
|
+
try {
|
|
31941
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
31942
|
+
const s = buf.subarray(0, bytesRead).toString("utf8");
|
|
31943
|
+
const nl = s.indexOf(`
|
|
31944
|
+
`);
|
|
31945
|
+
return nl === -1 ? s : s.slice(0, nl);
|
|
31946
|
+
} finally {
|
|
31947
|
+
await fh.close();
|
|
31948
|
+
}
|
|
31315
31949
|
}
|
|
31316
|
-
|
|
31317
|
-
|
|
31950
|
+
|
|
31951
|
+
// lib/session-recovery.ts
|
|
31952
|
+
var DEFAULT_OPTS = {
|
|
31953
|
+
idleMs: 5 * 60000,
|
|
31954
|
+
windowSize: 30,
|
|
31955
|
+
excerptLimit: 200
|
|
31956
|
+
};
|
|
31957
|
+
var PENDING_CHANGES_TOOLS = new Set([
|
|
31958
|
+
"pending-changes",
|
|
31959
|
+
"pending_changes",
|
|
31960
|
+
"save_pending_changes",
|
|
31961
|
+
"ast-edit"
|
|
31962
|
+
]);
|
|
31963
|
+
var APPLY_TOOLS = new Set([
|
|
31964
|
+
"apply_changes",
|
|
31965
|
+
"apply-changes",
|
|
31966
|
+
"pending-changes-apply"
|
|
31967
|
+
]);
|
|
31968
|
+
var SUBTASK_OPEN = /^subtask[\.\-_:]?(start|create|spawn|run)$/i;
|
|
31969
|
+
var SUBTASK_CLOSE = /^subtask[\.\-_:]?(complete|done|close|finish)$/i;
|
|
31970
|
+
function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
31971
|
+
const cleanOpts = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined));
|
|
31972
|
+
const o = { ...DEFAULT_OPTS, ...cleanOpts };
|
|
31973
|
+
const now = opts.now ?? Date.now();
|
|
31974
|
+
if (events.length === 0 || !meta.id) {
|
|
31318
31975
|
return {
|
|
31319
|
-
|
|
31320
|
-
|
|
31976
|
+
last_session_id: meta.id ?? null,
|
|
31977
|
+
reason: "no_session",
|
|
31978
|
+
idle_ms: null,
|
|
31979
|
+
last_user_intent: null,
|
|
31980
|
+
last_agent: null,
|
|
31981
|
+
inflight_tools: [],
|
|
31982
|
+
pending_changes_likely: false,
|
|
31983
|
+
open_subtasks_likely: false,
|
|
31984
|
+
summary: "无历史会话,无需恢复。"
|
|
31321
31985
|
};
|
|
31322
31986
|
}
|
|
31323
|
-
|
|
31324
|
-
|
|
31325
|
-
|
|
31326
|
-
|
|
31327
|
-
|
|
31987
|
+
const lastTs = meta.lastEventAt ?? events[events.length - 1].timestamp;
|
|
31988
|
+
const idleMs = now - lastTs;
|
|
31989
|
+
let lastUser = null;
|
|
31990
|
+
for (let i = events.length - 1;i >= 0; i--) {
|
|
31991
|
+
const e = events[i];
|
|
31992
|
+
if (e.type === "message" && e.role === "user") {
|
|
31993
|
+
lastUser = clip3(e.content, o.excerptLimit);
|
|
31994
|
+
break;
|
|
31995
|
+
}
|
|
31328
31996
|
}
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31334
|
-
|
|
31335
|
-
const who = r.agent ?? "subagent";
|
|
31336
|
-
const tool2 = r.lastTool ?? "thinking";
|
|
31337
|
-
return {
|
|
31338
|
-
message: `⏳ ${who} 仍在运行 ${fmtElapsed(now - r.startedAt)} | 当前: ${tool2}`,
|
|
31339
|
-
variant: "info"
|
|
31340
|
-
};
|
|
31341
|
-
}
|
|
31342
|
-
function buildEndToast(r, type, now = Date.now()) {
|
|
31343
|
-
const elapsed = fmtElapsed(now - r.startedAt);
|
|
31344
|
-
const variant = type === "session.error" || type === "session.deleted" ? "error" : "success";
|
|
31345
|
-
const emoji3 = type === "session.error" ? "❌" : type === "session.deleted" ? "\uD83D\uDDD1️" : "✅";
|
|
31346
|
-
const verb = type === "session.error" ? "失败" : type === "session.deleted" ? "被取消" : "完成";
|
|
31347
|
-
if (r.agent && r.description) {
|
|
31348
|
-
if (type === "session.idle" || type !== "session.error" && type !== "session.deleted") {
|
|
31349
|
-
return {
|
|
31350
|
-
message: `${emoji3} ${titleCase(r.agent)} Task — ${sanitizeDescription(r.description)} (${elapsed})`,
|
|
31351
|
-
variant
|
|
31352
|
-
};
|
|
31997
|
+
let lastAgent = null;
|
|
31998
|
+
for (let i = events.length - 1;i >= 0; i--) {
|
|
31999
|
+
const e = events[i];
|
|
32000
|
+
if (e.type === "agent_switch") {
|
|
32001
|
+
lastAgent = e.to;
|
|
32002
|
+
break;
|
|
31353
32003
|
}
|
|
31354
|
-
|
|
31355
|
-
|
|
31356
|
-
variant
|
|
31357
|
-
};
|
|
32004
|
+
if (e.agent && lastAgent === null)
|
|
32005
|
+
lastAgent = e.agent;
|
|
31358
32006
|
}
|
|
31359
|
-
|
|
31360
|
-
|
|
31361
|
-
|
|
31362
|
-
|
|
31363
|
-
|
|
32007
|
+
const window = events.slice(-o.windowSize).filter((e) => e.type === "tool_call");
|
|
32008
|
+
const toolMap = new Map;
|
|
32009
|
+
for (const t of window) {
|
|
32010
|
+
const cur = toolMap.get(t.tool);
|
|
32011
|
+
const argsExcerpt = clip3(safeJson(t.args), o.excerptLimit);
|
|
32012
|
+
if (cur) {
|
|
32013
|
+
cur.count++;
|
|
32014
|
+
cur.last_ok = t.ok;
|
|
32015
|
+
cur.last_args_excerpt = argsExcerpt;
|
|
32016
|
+
cur.last_ts = t.timestamp;
|
|
32017
|
+
} else {
|
|
32018
|
+
toolMap.set(t.tool, {
|
|
32019
|
+
tool: t.tool,
|
|
32020
|
+
count: 1,
|
|
32021
|
+
last_ok: t.ok,
|
|
32022
|
+
last_args_excerpt: argsExcerpt,
|
|
32023
|
+
last_ts: t.timestamp
|
|
32024
|
+
});
|
|
32025
|
+
}
|
|
32026
|
+
}
|
|
32027
|
+
const inflight3 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
|
|
32028
|
+
const proposed = window.some((t) => PENDING_CHANGES_TOOLS.has(t.tool));
|
|
32029
|
+
const applied = window.some((t) => APPLY_TOOLS.has(t.tool) && t.ok);
|
|
32030
|
+
const pending_changes_likely = proposed && !applied;
|
|
32031
|
+
let openCount = 0;
|
|
32032
|
+
for (const t of window) {
|
|
32033
|
+
if (SUBTASK_OPEN.test(t.tool))
|
|
32034
|
+
openCount++;
|
|
32035
|
+
else if (SUBTASK_CLOSE.test(t.tool) && openCount > 0)
|
|
32036
|
+
openCount--;
|
|
32037
|
+
}
|
|
32038
|
+
const open_subtasks_likely = openCount > 0;
|
|
32039
|
+
const lastEvent = events[events.length - 1];
|
|
32040
|
+
let reason = "no_completion_marker";
|
|
32041
|
+
if (lastEvent.type === "message" && lastEvent.role === "system" && /\b(suspend(?:ed|ing)?|interrupt(?:ed|ing)?|paused?|pausing)\b/i.test(lastEvent.content) || lastEvent.type === "agent_switch" && /\b(suspend(?:ed|ing)?|paused?)\b/i.test(lastEvent.reason ?? "")) {
|
|
32042
|
+
reason = "explicit_marker";
|
|
32043
|
+
} else if (idleMs >= o.idleMs) {
|
|
32044
|
+
reason = "idle_timeout";
|
|
31364
32045
|
}
|
|
32046
|
+
const summary = renderSummary2({
|
|
32047
|
+
sessionId: meta.id,
|
|
32048
|
+
idleMs,
|
|
32049
|
+
lastUser,
|
|
32050
|
+
lastAgent,
|
|
32051
|
+
inflight: inflight3,
|
|
32052
|
+
pending_changes_likely,
|
|
32053
|
+
open_subtasks_likely,
|
|
32054
|
+
reason
|
|
32055
|
+
});
|
|
31365
32056
|
return {
|
|
31366
|
-
|
|
31367
|
-
|
|
32057
|
+
last_session_id: meta.id,
|
|
32058
|
+
reason,
|
|
32059
|
+
idle_ms: idleMs,
|
|
32060
|
+
last_user_intent: lastUser,
|
|
32061
|
+
last_agent: lastAgent,
|
|
32062
|
+
inflight_tools: inflight3,
|
|
32063
|
+
pending_changes_likely,
|
|
32064
|
+
open_subtasks_likely,
|
|
32065
|
+
summary
|
|
31368
32066
|
};
|
|
31369
32067
|
}
|
|
31370
|
-
function
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31374
|
-
|
|
31375
|
-
|
|
31376
|
-
|
|
31377
|
-
|
|
31378
|
-
"
|
|
31379
|
-
"
|
|
31380
|
-
"target"
|
|
31381
|
-
];
|
|
31382
|
-
for (const k of PATH_KEYS) {
|
|
31383
|
-
const v = a[k];
|
|
31384
|
-
if (typeof v === "string" && v.length > 0 && v.length <= 200) {
|
|
31385
|
-
return `${toolName} ${v}`;
|
|
31386
|
-
}
|
|
32068
|
+
function renderSummary2(args) {
|
|
32069
|
+
const lines = [];
|
|
32070
|
+
lines.push(`\uD83D\uDCCD 上次会话 ${args.sessionId.slice(0, 8)}… 中断(${formatIdle(args.idleMs)}前 / ${args.reason})`);
|
|
32071
|
+
if (args.lastUser)
|
|
32072
|
+
lines.push(`▶ 用户上次说:"${args.lastUser}"`);
|
|
32073
|
+
if (args.lastAgent)
|
|
32074
|
+
lines.push(`\uD83D\uDC64 当时 agent:${args.lastAgent}`);
|
|
32075
|
+
if (args.inflight.length > 0) {
|
|
32076
|
+
const top = args.inflight.slice(0, 3).map((t) => `${t.tool}×${t.count}${t.last_ok ? "" : "✗"}`);
|
|
32077
|
+
lines.push(`\uD83D\uDD27 最近活跃工具:${top.join(", ")}`);
|
|
31387
32078
|
}
|
|
31388
|
-
|
|
31389
|
-
|
|
31390
|
-
|
|
31391
|
-
|
|
32079
|
+
if (args.pending_changes_likely)
|
|
32080
|
+
lines.push("⚠ 似有暂存区改动尚未 apply(pending-changes propose 后无 apply)");
|
|
32081
|
+
if (args.open_subtasks_likely)
|
|
32082
|
+
lines.push("⚠ 似有子任务未关闭");
|
|
32083
|
+
if (args.inflight.length === 0 && !args.pending_changes_likely && !args.open_subtasks_likely) {
|
|
32084
|
+
lines.push("(未检测到 in-flight 工作;可继续 / 也可不恢复)");
|
|
31392
32085
|
}
|
|
31393
|
-
return
|
|
31394
|
-
|
|
31395
|
-
function buildBeforeLogLine(toolName, args, now = Date.now()) {
|
|
31396
|
-
return JSON.stringify({
|
|
31397
|
-
ts: Math.floor(now / 1000),
|
|
31398
|
-
tool: toolName,
|
|
31399
|
-
phase: "before",
|
|
31400
|
-
input_summary: sanitizeInputSummary(toolName, args)
|
|
31401
|
-
});
|
|
32086
|
+
return lines.join(`
|
|
32087
|
+
`);
|
|
31402
32088
|
}
|
|
31403
|
-
function
|
|
31404
|
-
|
|
31405
|
-
|
|
31406
|
-
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
32089
|
+
function formatIdle(ms) {
|
|
32090
|
+
if (ms < 0)
|
|
32091
|
+
return "刚刚";
|
|
32092
|
+
if (ms < 60000)
|
|
32093
|
+
return `${Math.round(ms / 1000)}s`;
|
|
32094
|
+
if (ms < 3600000)
|
|
32095
|
+
return `${Math.round(ms / 60000)}min`;
|
|
32096
|
+
if (ms < 86400000)
|
|
32097
|
+
return `${Math.round(ms / 3600000)}h`;
|
|
32098
|
+
return `${Math.round(ms / 86400000)}d`;
|
|
31411
32099
|
}
|
|
31412
|
-
function
|
|
31413
|
-
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
|
|
31418
|
-
elapsed_ms: elapsedMs
|
|
31419
|
-
});
|
|
32100
|
+
function clip3(s, max) {
|
|
32101
|
+
if (!s)
|
|
32102
|
+
return "";
|
|
32103
|
+
if (s.length <= max)
|
|
32104
|
+
return s;
|
|
32105
|
+
return s.slice(0, max - 1) + "…";
|
|
31420
32106
|
}
|
|
31421
|
-
|
|
32107
|
+
function safeJson(v) {
|
|
31422
32108
|
try {
|
|
31423
|
-
|
|
31424
|
-
|
|
31425
|
-
|
|
31426
|
-
} catch (err) {
|
|
31427
|
-
log9?.debug?.("appendSubagentLog 失败(已隔离)", {
|
|
31428
|
-
error: err instanceof Error ? err.message : String(err),
|
|
31429
|
-
file: filePath
|
|
31430
|
-
});
|
|
32109
|
+
return JSON.stringify(v);
|
|
32110
|
+
} catch {
|
|
32111
|
+
return String(v);
|
|
31431
32112
|
}
|
|
31432
32113
|
}
|
|
31433
|
-
function
|
|
32114
|
+
async function scanLastSession(opts = {}) {
|
|
31434
32115
|
try {
|
|
31435
|
-
const
|
|
31436
|
-
|
|
31437
|
-
|
|
31438
|
-
|
|
31439
|
-
|
|
31440
|
-
|
|
32116
|
+
const sessions = await listSessions(opts);
|
|
32117
|
+
if (sessions.length === 0) {
|
|
32118
|
+
return {
|
|
32119
|
+
ok: true,
|
|
32120
|
+
plan: emptyPlan("no_session")
|
|
32121
|
+
};
|
|
31441
32122
|
}
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
32123
|
+
const exclude = new Set(opts.excludeIds ?? []);
|
|
32124
|
+
const candidates = sessions.filter((s) => !exclude.has(s.id)).sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
32125
|
+
if (candidates.length === 0) {
|
|
32126
|
+
return { ok: true, plan: emptyPlan("no_session") };
|
|
32127
|
+
}
|
|
32128
|
+
const top = candidates[0];
|
|
32129
|
+
const loaded = await loadSession(top.id, opts);
|
|
32130
|
+
const plan = buildRecoveryPlan(loaded.events, { id: top.id, lastEventAt: top.mtime_ms }, {
|
|
32131
|
+
idleMs: opts.idleMs,
|
|
32132
|
+
windowSize: opts.windowSize,
|
|
32133
|
+
excerptLimit: opts.excerptLimit,
|
|
32134
|
+
now: opts.now
|
|
32135
|
+
});
|
|
32136
|
+
return { ok: true, plan, meta: top };
|
|
32137
|
+
} catch (err) {
|
|
32138
|
+
return {
|
|
32139
|
+
ok: false,
|
|
32140
|
+
error: err instanceof Error ? err.message : String(err)
|
|
32141
|
+
};
|
|
31445
32142
|
}
|
|
31446
32143
|
}
|
|
31447
|
-
function
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
32144
|
+
function emptyPlan(reason) {
|
|
32145
|
+
return {
|
|
32146
|
+
last_session_id: null,
|
|
32147
|
+
reason,
|
|
32148
|
+
idle_ms: null,
|
|
32149
|
+
last_user_intent: null,
|
|
32150
|
+
last_agent: null,
|
|
32151
|
+
inflight_tools: [],
|
|
32152
|
+
pending_changes_likely: false,
|
|
32153
|
+
open_subtasks_likely: false,
|
|
32154
|
+
summary: "无历史会话,无需恢复。"
|
|
32155
|
+
};
|
|
31451
32156
|
}
|
|
31452
|
-
|
|
31453
|
-
if (
|
|
31454
|
-
log9?.debug?.("tui.showToast 不可用,noop");
|
|
32157
|
+
function isRecoveryWorthShowing(plan) {
|
|
32158
|
+
if (!plan.last_session_id)
|
|
31455
32159
|
return false;
|
|
31456
|
-
|
|
32160
|
+
if (plan.reason === "no_session")
|
|
32161
|
+
return false;
|
|
32162
|
+
const hasSignal = plan.pending_changes_likely || plan.open_subtasks_likely || plan.inflight_tools.length > 0 || plan.reason === "explicit_marker";
|
|
32163
|
+
return hasSignal;
|
|
32164
|
+
}
|
|
32165
|
+
|
|
32166
|
+
// lib/block-pending.ts
|
|
32167
|
+
import { promises as fs19 } from "node:fs";
|
|
32168
|
+
import * as path25 from "node:path";
|
|
32169
|
+
function blockPendingFilePath(absRoot) {
|
|
32170
|
+
const rd = runtimeDir(absRoot, { ensure: false });
|
|
32171
|
+
return path25.join(rd, "sessions", "autonomous-blocks.ndjson");
|
|
32172
|
+
}
|
|
32173
|
+
function consumeLockPath(absRoot) {
|
|
32174
|
+
return blockPendingFilePath(absRoot) + ".consume.lock";
|
|
32175
|
+
}
|
|
32176
|
+
async function scanBlockPending(absRoot, filterSessionId) {
|
|
32177
|
+
const file2 = blockPendingFilePath(absRoot);
|
|
32178
|
+
let raw;
|
|
31457
32179
|
try {
|
|
31458
|
-
await
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
32180
|
+
raw = await fs19.readFile(file2, "utf8");
|
|
32181
|
+
} catch {
|
|
32182
|
+
return [];
|
|
32183
|
+
}
|
|
32184
|
+
if (!raw)
|
|
32185
|
+
return [];
|
|
32186
|
+
const consumed = new Set;
|
|
32187
|
+
const entries = [];
|
|
32188
|
+
for (const line of raw.split(`
|
|
32189
|
+
`)) {
|
|
32190
|
+
const trimmed = line.trim();
|
|
32191
|
+
if (!trimmed)
|
|
32192
|
+
continue;
|
|
32193
|
+
let obj;
|
|
32194
|
+
try {
|
|
32195
|
+
obj = JSON.parse(trimmed);
|
|
32196
|
+
} catch {
|
|
32197
|
+
continue;
|
|
32198
|
+
}
|
|
32199
|
+
if (!obj || typeof obj !== "object")
|
|
32200
|
+
continue;
|
|
32201
|
+
if (obj["type"] === "consume") {
|
|
32202
|
+
const sid = obj["sessionId"];
|
|
32203
|
+
const ts = obj["timestamp"];
|
|
32204
|
+
if (typeof sid === "string" && typeof ts === "string") {
|
|
32205
|
+
consumed.add(`${sid}|${ts}`);
|
|
31464
32206
|
}
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
32207
|
+
continue;
|
|
32208
|
+
}
|
|
32209
|
+
const sessionId = obj["sessionId"];
|
|
32210
|
+
const timestamp = obj["timestamp"];
|
|
32211
|
+
if (typeof sessionId !== "string" || typeof timestamp !== "string")
|
|
32212
|
+
continue;
|
|
32213
|
+
const entry = {
|
|
32214
|
+
sessionId,
|
|
32215
|
+
timestamp,
|
|
32216
|
+
reason: typeof obj["reason"] === "string" ? obj["reason"] : undefined,
|
|
32217
|
+
summary_excerpt: typeof obj["summary_excerpt"] === "string" ? obj["summary_excerpt"] : undefined,
|
|
32218
|
+
consumed_at: typeof obj["consumed_at"] === "string" ? obj["consumed_at"] : undefined
|
|
32219
|
+
};
|
|
32220
|
+
entries.push(entry);
|
|
31472
32221
|
}
|
|
32222
|
+
return entries.filter((e) => {
|
|
32223
|
+
if (e.consumed_at)
|
|
32224
|
+
return false;
|
|
32225
|
+
if (consumed.has(`${e.sessionId}|${e.timestamp}`))
|
|
32226
|
+
return false;
|
|
32227
|
+
if (filterSessionId && e.sessionId !== filterSessionId)
|
|
32228
|
+
return false;
|
|
32229
|
+
return true;
|
|
32230
|
+
});
|
|
31473
32231
|
}
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
32232
|
+
async function markBlocksConsumed(absRoot, entries) {
|
|
32233
|
+
if (entries.length === 0)
|
|
32234
|
+
return;
|
|
32235
|
+
const file2 = blockPendingFilePath(absRoot);
|
|
32236
|
+
await fs19.mkdir(path25.dirname(file2), { recursive: true });
|
|
32237
|
+
const now = new Date().toISOString();
|
|
32238
|
+
const lines = entries.map((e) => ({
|
|
32239
|
+
type: "consume",
|
|
32240
|
+
sessionId: e.sessionId,
|
|
32241
|
+
timestamp: e.timestamp,
|
|
32242
|
+
consumed_at: now
|
|
32243
|
+
})).map((row) => JSON.stringify(row)).join(`
|
|
32244
|
+
`) + `
|
|
32245
|
+
`;
|
|
32246
|
+
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
32247
|
+
await fs19.appendFile(file2, lines, "utf8");
|
|
31479
32248
|
});
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
|
|
31497
|
-
|
|
31498
|
-
|
|
32249
|
+
}
|
|
32250
|
+
|
|
32251
|
+
// plugins/session-recovery.ts
|
|
32252
|
+
var PLUGIN_NAME14 = "session-recovery";
|
|
32253
|
+
logLifecycle(PLUGIN_NAME14, "import", {});
|
|
32254
|
+
async function processSessionStart(currentSessionId, opts = {}) {
|
|
32255
|
+
if (opts.disabled) {
|
|
32256
|
+
return { ok: true, injected: false, reason: "disabled" };
|
|
32257
|
+
}
|
|
32258
|
+
const excludeIds = new Set(opts.excludeIds ?? []);
|
|
32259
|
+
if (currentSessionId)
|
|
32260
|
+
excludeIds.add(currentSessionId);
|
|
32261
|
+
const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
|
|
32262
|
+
if (!r.ok) {
|
|
32263
|
+
opts.log?.warn?.(`[${PLUGIN_NAME14}] 扫描失败:${r.error}`);
|
|
32264
|
+
return { ok: false, injected: false, reason: "scan_error", error: r.error };
|
|
32265
|
+
}
|
|
32266
|
+
const plan = r.plan;
|
|
32267
|
+
let pendingBlocks = [];
|
|
32268
|
+
if (opts.root) {
|
|
32269
|
+
try {
|
|
32270
|
+
pendingBlocks = await scanBlockPending(opts.root);
|
|
32271
|
+
} catch (err) {
|
|
32272
|
+
opts.log?.warn?.(`[${PLUGIN_NAME14}] scanBlockPending 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
32273
|
+
}
|
|
32274
|
+
}
|
|
32275
|
+
const hasPendingBlocks = pendingBlocks.length > 0;
|
|
32276
|
+
if (!isRecoveryWorthShowing(plan) && !hasPendingBlocks) {
|
|
32277
|
+
return { ok: true, injected: false, plan, reason: "no_signal", pendingBlocks };
|
|
32278
|
+
}
|
|
32279
|
+
const blockPrompt = hasPendingBlocks ? renderBlockPendingPrompt(pendingBlocks) : "";
|
|
32280
|
+
const recoveryPrompt = isRecoveryWorthShowing(plan) ? renderPrompt(plan) : "";
|
|
32281
|
+
const prompt = [blockPrompt, recoveryPrompt].filter((s) => s.length > 0).join(`
|
|
32282
|
+
|
|
32283
|
+
`);
|
|
32284
|
+
const injection = { source: "session-recovery", plan, prompt };
|
|
32285
|
+
if (opts.injectRecovery) {
|
|
32286
|
+
try {
|
|
32287
|
+
await opts.injectRecovery(injection);
|
|
32288
|
+
} catch (err) {
|
|
32289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32290
|
+
opts.log?.warn?.(`[${PLUGIN_NAME14}] injectRecovery 异常:${msg}`);
|
|
32291
|
+
return { ok: false, injected: false, plan, reason: "inject_error", error: msg, pendingBlocks };
|
|
32292
|
+
}
|
|
32293
|
+
}
|
|
32294
|
+
if (hasPendingBlocks && opts.root) {
|
|
32295
|
+
try {
|
|
32296
|
+
await markBlocksConsumed(opts.root, pendingBlocks);
|
|
32297
|
+
} catch (err) {
|
|
32298
|
+
opts.log?.warn?.(`[${PLUGIN_NAME14}] markBlocksConsumed 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
32299
|
+
}
|
|
32300
|
+
}
|
|
32301
|
+
return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
|
|
32302
|
+
}
|
|
32303
|
+
function renderBlockPendingPrompt(entries) {
|
|
32304
|
+
const lines = [];
|
|
32305
|
+
lines.push(`⚠️ **${entries.length} 个未读 BLOCK 通知**(上次 auto 模式 reviewer 阻断、channels 通知失败):`);
|
|
32306
|
+
lines.push("");
|
|
32307
|
+
entries.forEach((e, i) => {
|
|
32308
|
+
const sidShort = e.sessionId.length > 12 ? e.sessionId.slice(0, 8) + "…" : e.sessionId;
|
|
32309
|
+
const reasonPart = e.reason ? `:${e.reason}` : "";
|
|
32310
|
+
lines.push(`${i + 1}. session \`${sidShort}\` @ ${e.timestamp}${reasonPart}`);
|
|
32311
|
+
if (e.summary_excerpt) {
|
|
32312
|
+
const excerpt = e.summary_excerpt.length > 200 ? e.summary_excerpt.slice(0, 200) + "…" : e.summary_excerpt;
|
|
32313
|
+
lines.push(` > ${excerpt.replace(/\n/g, `
|
|
32314
|
+
> `)}`);
|
|
31499
32315
|
}
|
|
31500
|
-
}
|
|
31501
|
-
|
|
31502
|
-
|
|
31503
|
-
|
|
32316
|
+
});
|
|
32317
|
+
lines.push("");
|
|
32318
|
+
lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
|
|
32319
|
+
return lines.join(`
|
|
32320
|
+
`);
|
|
32321
|
+
}
|
|
32322
|
+
function renderPrompt(plan) {
|
|
32323
|
+
const lines = [];
|
|
32324
|
+
lines.push("【会话恢复提示】");
|
|
32325
|
+
lines.push(plan.summary);
|
|
32326
|
+
lines.push("");
|
|
32327
|
+
lines.push("如果用户接下来未明确提及,请优先:");
|
|
32328
|
+
if (plan.pending_changes_likely) {
|
|
32329
|
+
lines.push(" • 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
|
|
31504
32330
|
}
|
|
31505
|
-
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
safeWriteLog(PLUGIN_NAME14, { hook: "interval", session_parent_swept: sweptParents });
|
|
31514
|
-
}
|
|
31515
|
-
const beats = pickHeartbeats();
|
|
31516
|
-
if (beats.length === 0)
|
|
31517
|
-
return;
|
|
31518
|
-
for (const r of beats) {
|
|
31519
|
-
const t = buildHeartbeatToast(r);
|
|
31520
|
-
const sent = await showToast2(client, t, log9);
|
|
31521
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31522
|
-
hook: "interval",
|
|
31523
|
-
child: r.childID,
|
|
31524
|
-
parent: r.parentID,
|
|
31525
|
-
tool: r.lastTool,
|
|
31526
|
-
elapsed_ms: Date.now() - r.startedAt,
|
|
31527
|
-
toast_sent: sent
|
|
31528
|
-
});
|
|
31529
|
-
r.lastBeatAt = Date.now();
|
|
31530
|
-
}
|
|
31531
|
-
});
|
|
31532
|
-
}, HEARTBEAT_INTERVAL_MS2);
|
|
31533
|
-
if (typeof interval.unref === "function") {
|
|
31534
|
-
interval.unref();
|
|
32331
|
+
if (plan.open_subtasks_likely) {
|
|
32332
|
+
lines.push(" • 询问是否要继续上次未完成的子任务");
|
|
32333
|
+
}
|
|
32334
|
+
if (plan.last_user_intent) {
|
|
32335
|
+
lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
|
|
32336
|
+
}
|
|
32337
|
+
if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
|
|
32338
|
+
lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
|
|
31535
32339
|
}
|
|
32340
|
+
lines.push("");
|
|
32341
|
+
lines.push("(如果用户明确开了新话题,本提示可忽略)");
|
|
32342
|
+
return lines.join(`
|
|
32343
|
+
`);
|
|
32344
|
+
}
|
|
32345
|
+
var log9 = makePluginLogger(PLUGIN_NAME14);
|
|
32346
|
+
var _lastInjection = null;
|
|
32347
|
+
var sessionRecoveryServer = async (ctx) => {
|
|
32348
|
+
logLifecycle(PLUGIN_NAME14, "activate", { directory: ctx.directory });
|
|
31536
32349
|
return {
|
|
31537
32350
|
event: async ({ event }) => {
|
|
31538
32351
|
await safeAsync(PLUGIN_NAME14, "event", async () => {
|
|
31539
|
-
|
|
31540
|
-
|
|
31541
|
-
_parentParseFailLogged++;
|
|
31542
|
-
log9.warn("session.created 含 parentID 关键字但 extractCreatedChild 解析失败,可能 SDK schema 变更", {
|
|
31543
|
-
sample_count: _parentParseFailLogged,
|
|
31544
|
-
hint: "如频繁出现请检查 plugins/subtask-heartbeat.ts::extractCreatedChild 是否适配新 SDK"
|
|
31545
|
-
});
|
|
31546
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31547
|
-
hook: "event",
|
|
31548
|
-
type: "session.created.unparsed-parent-id",
|
|
31549
|
-
sample_count: _parentParseFailLogged
|
|
31550
|
-
});
|
|
31551
|
-
}
|
|
31552
|
-
} catch {}
|
|
31553
|
-
const created = extractCreatedChild(event);
|
|
31554
|
-
if (created) {
|
|
31555
|
-
try {
|
|
31556
|
-
recordSessionParent2(created.childID, created.parentID);
|
|
31557
|
-
} catch (err) {
|
|
31558
|
-
log9.warn("recordSessionParent 抛错(已隔离)", {
|
|
31559
|
-
error: err instanceof Error ? err.message : String(err)
|
|
31560
|
-
});
|
|
31561
|
-
}
|
|
31562
|
-
const pending = dequeuePendingTask(created.parentID);
|
|
31563
|
-
const record2 = registerInflight({
|
|
31564
|
-
childID: created.childID,
|
|
31565
|
-
parentID: created.parentID,
|
|
31566
|
-
agent: pending?.agent ?? created.agent,
|
|
31567
|
-
description: pending?.description ?? null
|
|
31568
|
-
});
|
|
31569
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31570
|
-
hook: "event",
|
|
31571
|
-
type: "session.created",
|
|
31572
|
-
child: created.childID,
|
|
31573
|
-
parent: created.parentID,
|
|
31574
|
-
pending_task_matched: pending !== null,
|
|
31575
|
-
agent: record2.agent,
|
|
31576
|
-
description_len: record2.description?.length ?? 0
|
|
31577
|
-
});
|
|
31578
|
-
const startToast = buildStartToast(record2);
|
|
31579
|
-
const sent = await showToast2(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log9);
|
|
31580
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31581
|
-
hook: "event",
|
|
31582
|
-
type: "session.created.toast",
|
|
31583
|
-
child: created.childID,
|
|
31584
|
-
toast_sent: sent,
|
|
31585
|
-
start_toast_message: startToast.message
|
|
31586
|
-
});
|
|
31587
|
-
return;
|
|
31588
|
-
}
|
|
31589
|
-
const ended = extractEndedSessionID(event);
|
|
31590
|
-
if (ended) {
|
|
31591
|
-
if (ended.type === "session.deleted") {
|
|
31592
|
-
try {
|
|
31593
|
-
deleteSessionParent2(ended.sessionID);
|
|
31594
|
-
} catch {}
|
|
31595
|
-
}
|
|
31596
|
-
const r = clearInflight2(ended.sessionID);
|
|
31597
|
-
if (r) {
|
|
31598
|
-
const t = buildEndToast(r, ended.type);
|
|
31599
|
-
const sent = await showToast2(client, t, log9);
|
|
31600
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31601
|
-
hook: "event",
|
|
31602
|
-
type: ended.type,
|
|
31603
|
-
child: r.childID,
|
|
31604
|
-
elapsed_ms: Date.now() - r.startedAt,
|
|
31605
|
-
toast_sent: sent,
|
|
31606
|
-
end_toast_message: t.message
|
|
31607
|
-
});
|
|
31608
|
-
if (ended.type === "session.error" && isLogPersistenceEnabled(cwd)) {
|
|
31609
|
-
const elapsedMs = Date.now() - r.startedAt;
|
|
31610
|
-
const errLine = buildErrorLogLine(ended.errorReason, elapsedMs);
|
|
31611
|
-
const logFile = subagentLogPath(cwd, r.parentID, r.childID);
|
|
31612
|
-
appendSubagentLog(logFile, errLine, log9);
|
|
31613
|
-
const parentID = r.parentID;
|
|
31614
|
-
const who = r.agent ? titleCase(r.agent) : "subagent";
|
|
31615
|
-
const elapsed = fmtElapsed(elapsedMs);
|
|
31616
|
-
const noticeText = [
|
|
31617
|
-
`❌ ${who} 失败(${elapsed})`,
|
|
31618
|
-
ended.errorReason ? `错误:${ended.errorReason.slice(0, 150)}` : null,
|
|
31619
|
-
`日志:${logFile}`,
|
|
31620
|
-
`排查:cat "${logFile}" | tail -30`
|
|
31621
|
-
].filter(Boolean).join(`
|
|
31622
|
-
`);
|
|
31623
|
-
const logAdapter = (level, msg, data) => {
|
|
31624
|
-
if (level === "warn" || level === "error") {
|
|
31625
|
-
log9.warn(`[sendParentNotice] ${msg}`, data);
|
|
31626
|
-
}
|
|
31627
|
-
};
|
|
31628
|
-
(async () => {
|
|
31629
|
-
try {
|
|
31630
|
-
await sendParentNotice(client, parentID, noticeText, { directory: cwd, log: logAdapter });
|
|
31631
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31632
|
-
hook: "event",
|
|
31633
|
-
type: "session.error.notified",
|
|
31634
|
-
child: r.childID,
|
|
31635
|
-
parent: r.parentID,
|
|
31636
|
-
has_error_reason: ended.errorReason !== null,
|
|
31637
|
-
notice_text_len: noticeText.length
|
|
31638
|
-
});
|
|
31639
|
-
} catch {}
|
|
31640
|
-
})();
|
|
31641
|
-
}
|
|
31642
|
-
if (ended.type === "session.deleted") {
|
|
31643
|
-
const logFile = subagentLogPath(cwd, r.parentID, r.childID);
|
|
31644
|
-
fsPromises.unlink(logFile).catch(() => {});
|
|
31645
|
-
}
|
|
31646
|
-
}
|
|
31647
|
-
}
|
|
31648
|
-
});
|
|
31649
|
-
},
|
|
31650
|
-
"tool.execute.before": async (input, output) => {
|
|
31651
|
-
const isTaskTool = input?.tool === "task";
|
|
31652
|
-
if (inflight2.size === 0 && !isTaskTool)
|
|
31653
|
-
return;
|
|
31654
|
-
await safeAsync(PLUGIN_NAME14, "tool.execute.before", async () => {
|
|
31655
|
-
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
31656
|
-
return;
|
|
31657
|
-
if (isTaskTool) {
|
|
31658
|
-
const args = output?.args ?? null;
|
|
31659
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31660
|
-
hook: "tool.execute.before.task",
|
|
31661
|
-
sessionID: input.sessionID,
|
|
31662
|
-
args_keys: args && typeof args === "object" ? Object.keys(args) : null,
|
|
31663
|
-
args_raw: args
|
|
31664
|
-
});
|
|
31665
|
-
const extracted = extractTaskArgs(args);
|
|
31666
|
-
if (extracted) {
|
|
31667
|
-
enqueuePendingTask(input.sessionID, {
|
|
31668
|
-
agent: extracted.subagentType,
|
|
31669
|
-
description: extracted.description
|
|
31670
|
-
});
|
|
31671
|
-
safeWriteLog(PLUGIN_NAME14, {
|
|
31672
|
-
hook: "tool.execute.before.task.enqueued",
|
|
31673
|
-
parent: input.sessionID,
|
|
31674
|
-
agent: extracted.subagentType,
|
|
31675
|
-
description_len: extracted.description?.length ?? 0
|
|
31676
|
-
});
|
|
31677
|
-
}
|
|
31678
|
-
}
|
|
31679
|
-
const rec = inflight2.get(input.sessionID);
|
|
31680
|
-
if (rec) {
|
|
31681
|
-
recordToolBeat(input.sessionID, input.tool);
|
|
31682
|
-
if (!rec.pendingCalls)
|
|
31683
|
-
rec.pendingCalls = new Map;
|
|
31684
|
-
if (typeof input.callID === "string") {
|
|
31685
|
-
rec.pendingCalls.set(input.callID, Date.now());
|
|
31686
|
-
}
|
|
31687
|
-
if (isLogPersistenceEnabled(cwd)) {
|
|
31688
|
-
const args = output?.args ?? null;
|
|
31689
|
-
const line = buildBeforeLogLine(input.tool, args);
|
|
31690
|
-
const file2 = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
31691
|
-
appendSubagentLog(file2, line, log9);
|
|
31692
|
-
}
|
|
31693
|
-
}
|
|
31694
|
-
});
|
|
31695
|
-
},
|
|
31696
|
-
"tool.execute.after": async (input, _output) => {
|
|
31697
|
-
if (inflight2.size === 0)
|
|
31698
|
-
return;
|
|
31699
|
-
await safeAsync(PLUGIN_NAME14, "tool.execute.after", async () => {
|
|
31700
|
-
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
32352
|
+
const e = event;
|
|
32353
|
+
if (!e || typeof e.type !== "string")
|
|
31701
32354
|
return;
|
|
31702
|
-
|
|
31703
|
-
if (!rec)
|
|
32355
|
+
if (e.type !== "session.start")
|
|
31704
32356
|
return;
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31708
|
-
|
|
31709
|
-
|
|
31710
|
-
|
|
32357
|
+
const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
|
|
32358
|
+
const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
|
|
32359
|
+
const r = await processSessionStart(sid, {
|
|
32360
|
+
root,
|
|
32361
|
+
log: log9,
|
|
32362
|
+
injectRecovery: (inj) => {
|
|
32363
|
+
_lastInjection = inj;
|
|
31711
32364
|
}
|
|
31712
|
-
}
|
|
31713
|
-
|
|
31714
|
-
|
|
31715
|
-
|
|
31716
|
-
|
|
32365
|
+
});
|
|
32366
|
+
safeWriteLog(PLUGIN_NAME14, {
|
|
32367
|
+
hook: "event",
|
|
32368
|
+
type: "session.start",
|
|
32369
|
+
ok: r.ok,
|
|
32370
|
+
injected: r.injected,
|
|
32371
|
+
reason: r.reason,
|
|
32372
|
+
last_session_id: r.plan?.last_session_id,
|
|
32373
|
+
pending_blocks_count: r.pendingBlocks?.length ?? 0
|
|
32374
|
+
});
|
|
32375
|
+
if (r.injected && r.plan) {
|
|
32376
|
+
log9.info(`[${PLUGIN_NAME14}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason}, pending_blocks=${r.pendingBlocks?.length ?? 0})`);
|
|
31717
32377
|
}
|
|
31718
32378
|
});
|
|
31719
32379
|
}
|
|
31720
32380
|
};
|
|
31721
32381
|
};
|
|
31722
|
-
var handler14 =
|
|
32382
|
+
var handler14 = sessionRecoveryServer;
|
|
31723
32383
|
|
|
31724
32384
|
// plugins/subtasks.ts
|
|
31725
32385
|
import { promises as fs20 } from "node:fs";
|
|
@@ -32757,7 +33417,7 @@ import * as https from "node:https";
|
|
|
32757
33417
|
// lib/version-injected.ts
|
|
32758
33418
|
function getInjectedVersion() {
|
|
32759
33419
|
try {
|
|
32760
|
-
const v = "0.8.
|
|
33420
|
+
const v = "0.8.19";
|
|
32761
33421
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
32762
33422
|
return v;
|
|
32763
33423
|
}
|
|
@@ -32882,7 +33542,7 @@ function defaultHttpFetcher(url3, timeoutMs) {
|
|
|
32882
33542
|
|
|
32883
33543
|
// plugins/update-checker.ts
|
|
32884
33544
|
var PLUGIN_NAME19 = "update-checker";
|
|
32885
|
-
var PLUGIN_VERSION = "
|
|
33545
|
+
var PLUGIN_VERSION = "4.0.0";
|
|
32886
33546
|
var _updateCheckStarted = false;
|
|
32887
33547
|
function getCacheFile() {
|
|
32888
33548
|
return join27(process.env["CODEFORGE_CACHE_DIR"] ?? join27(homedir9(), ".cache", "codeforge"), "update-check.json");
|
|
@@ -32970,12 +33630,32 @@ function spawnAsync(cmd, args, opts = {}) {
|
|
|
32970
33630
|
}
|
|
32971
33631
|
async function resolveNodeBin() {
|
|
32972
33632
|
const w = process.platform === "win32";
|
|
33633
|
+
if (w) {
|
|
33634
|
+
try {
|
|
33635
|
+
const r = await spawnAsync("where", ["node.exe"], { timeout: 3000, shell: true });
|
|
33636
|
+
if (r.status === 0 && r.stdout.trim())
|
|
33637
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
33638
|
+
} catch {}
|
|
33639
|
+
try {
|
|
33640
|
+
const r = await spawnAsync("where", ["node"], { timeout: 3000, shell: true });
|
|
33641
|
+
if (r.status === 0 && r.stdout.trim())
|
|
33642
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
33643
|
+
} catch {}
|
|
33644
|
+
for (const c of ["C:\\Program Files\\nodejs\\node.exe", "C:\\Program Files (x86)\\nodejs\\node.exe"]) {
|
|
33645
|
+
try {
|
|
33646
|
+
const r = await spawnAsync(c, ["--version"], { timeout: 2000, shell: false });
|
|
33647
|
+
if (r.status === 0)
|
|
33648
|
+
return c;
|
|
33649
|
+
} catch {}
|
|
33650
|
+
}
|
|
33651
|
+
return process.execPath;
|
|
33652
|
+
}
|
|
32973
33653
|
try {
|
|
32974
|
-
const r = await spawnAsync(
|
|
33654
|
+
const r = await spawnAsync("which", ["node"], { timeout: 3000 });
|
|
32975
33655
|
if (r.status === 0 && r.stdout.trim())
|
|
32976
33656
|
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
32977
33657
|
} catch {}
|
|
32978
|
-
for (const c of
|
|
33658
|
+
for (const c of ["/usr/local/bin/node", "/usr/bin/node", "/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node"]) {
|
|
32979
33659
|
try {
|
|
32980
33660
|
const r = await spawnAsync(c, ["--version"], { timeout: 2000 });
|
|
32981
33661
|
if (r.status === 0)
|
|
@@ -32986,12 +33666,34 @@ async function resolveNodeBin() {
|
|
|
32986
33666
|
}
|
|
32987
33667
|
async function resolveNpmBin() {
|
|
32988
33668
|
const w = process.platform === "win32";
|
|
33669
|
+
if (w) {
|
|
33670
|
+
try {
|
|
33671
|
+
const r = await spawnAsync("where", ["npm.cmd"], { timeout: 3000, shell: true });
|
|
33672
|
+
if (r.status === 0 && r.stdout.trim())
|
|
33673
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
33674
|
+
} catch {}
|
|
33675
|
+
try {
|
|
33676
|
+
const r = await spawnAsync("where", ["npm"], { timeout: 3000, shell: true });
|
|
33677
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
33678
|
+
const p = r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
33679
|
+
return p.replace(/\.cmd$/i, "") + ".cmd";
|
|
33680
|
+
}
|
|
33681
|
+
} catch {}
|
|
33682
|
+
for (const c of ["C:\\Program Files\\nodejs\\npm.cmd", "C:\\Program Files (x86)\\nodejs\\npm.cmd"]) {
|
|
33683
|
+
try {
|
|
33684
|
+
const r = await spawnAsync(c, ["--version"], { timeout: 2000, shell: true });
|
|
33685
|
+
if (r.status === 0)
|
|
33686
|
+
return c;
|
|
33687
|
+
} catch {}
|
|
33688
|
+
}
|
|
33689
|
+
return "npm.cmd";
|
|
33690
|
+
}
|
|
32989
33691
|
try {
|
|
32990
|
-
const r = await spawnAsync(
|
|
33692
|
+
const r = await spawnAsync("which", ["npm"], { timeout: 3000 });
|
|
32991
33693
|
if (r.status === 0 && r.stdout.trim())
|
|
32992
33694
|
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
32993
33695
|
} catch {}
|
|
32994
|
-
for (const c of
|
|
33696
|
+
for (const c of ["/usr/local/bin/npm", "/usr/bin/npm", "/opt/homebrew/bin/npm"]) {
|
|
32995
33697
|
try {
|
|
32996
33698
|
const r = await spawnAsync(c, ["--version"], { timeout: 2000 });
|
|
32997
33699
|
if (r.status === 0)
|
|
@@ -35095,11 +35797,11 @@ var HANDLERS = [
|
|
|
35095
35797
|
{ name: "codeforge-tools", init: handler7 },
|
|
35096
35798
|
{ name: "discover-spec-suggest", init: handler8 },
|
|
35097
35799
|
{ name: "memories-context", init: handler9 },
|
|
35098
|
-
{ name: "model-fallback", init:
|
|
35099
|
-
{ name: "parallel-tool-nudge", init:
|
|
35100
|
-
{ name: "pwsh-utf8", init:
|
|
35101
|
-
{ name: "session-recovery", init:
|
|
35102
|
-
{ name: "subtask-heartbeat", init:
|
|
35800
|
+
{ name: "model-fallback", init: handler11 },
|
|
35801
|
+
{ name: "parallel-tool-nudge", init: handler12 },
|
|
35802
|
+
{ name: "pwsh-utf8", init: handler13 },
|
|
35803
|
+
{ name: "session-recovery", init: handler14 },
|
|
35804
|
+
{ name: "subtask-heartbeat", init: handler10 },
|
|
35103
35805
|
{ name: "subtasks", init: handler15 },
|
|
35104
35806
|
{ name: "terminal-monitor", init: handler16 },
|
|
35105
35807
|
{ name: "token-manager", init: handler17 },
|