@andyqiu/codeforge 0.8.18 → 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/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
- ...sendProgress ? {
26951
- onProgress: (state, detail) => {
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
- return { ok: true, action: "merge", data: result };
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
- // plugins/model-fallback.ts
30123
- var PLUGIN_NAME10 = "model-fallback";
30124
- var state = {
30125
- config: null,
30126
- configPath: null,
30127
- warnings: [],
30128
- error: null
30129
- };
30130
- logLifecycle(PLUGIN_NAME10, "import");
30131
- function loadOnce(root) {
30132
- if (state.config !== null || state.error !== null)
30133
- return;
30134
- const r = loadModelConfigSync({ root });
30135
- if (r.ok && r.config) {
30136
- state.config = r.config;
30137
- state.configPath = r.path ?? null;
30138
- state.warnings = r.warnings;
30139
- if (r.warnings.length > 0) {
30140
- safeWriteLog(PLUGIN_NAME10, { phase: "load.warnings", warnings: r.warnings });
30141
- }
30142
- } else {
30143
- state.error = r.error ?? "unknown_load_error";
30144
- safeWriteLog(PLUGIN_NAME10, { phase: "load.failed", error: state.error, path: r.path });
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
- var MODEL_ERR_PATTERNS = [
30148
- /rate.?limit/i,
30149
- /quota/i,
30150
- /\b(429|503)\b/,
30151
- /unavailable/i,
30152
- /context.*length/i,
30153
- /model.*not.*found/i,
30154
- /overload/i,
30155
- /timeout/i
30156
- ];
30157
- function looksLikeModelError(message) {
30158
- if (!message)
30159
- return false;
30160
- return MODEL_ERR_PATTERNS.some((re) => re.test(message));
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
- const idx = r.chain.indexOf(currentModel);
30167
- const next = idx >= 0 ? r.chain[idx + 1] ?? null : r.chain[0] ?? null;
30168
- return {
30169
- agent,
30170
- current_model: currentModel,
30171
- resolved: r,
30172
- next_fallback: next,
30173
- chain: r.chain,
30174
- source: r.source
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 buildSuggestion(meta, errorMessage2) {
30178
- const head = `⚠️ \`${meta.agent}\` agent 调用 \`${meta.current_model}\` 失败`;
30179
- const reason = errorMessage2 ? `(${errorMessage2.slice(0, 80)})` : "";
30180
- if (!meta.next_fallback) {
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
- return `${head}${reason}
30185
- 建议手动切换:\`/model-switch ${meta.agent} ${meta.next_fallback}\`
30186
- 完整链:${meta.chain.join(" ")}`;
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
- var modelFallbackServer = async (ctx) => {
30189
- const log7 = makePluginLogger(PLUGIN_NAME10);
30190
- loadOnce(ctx.directory ?? process.cwd());
30191
- logLifecycle(PLUGIN_NAME10, "activate", {
30192
- directory: ctx.directory,
30193
- config_path: state.configPath,
30194
- config_loaded: state.config !== null,
30195
- config_error: state.error,
30196
- agents: state.config ? Object.keys(state.config.agents) : [],
30197
- runtime_fallback: state.config?.runtime_fallback ?? null
30198
- });
30199
- return {
30200
- "chat.params": async (input, output) => {
30201
- await safeAsync(PLUGIN_NAME10, "chat.params", async () => {
30202
- if (!state.config)
30203
- return;
30204
- const agent = input.agent;
30205
- const modelObj = input.model;
30206
- if (!agent || !modelObj)
30207
- return;
30208
- const providerID = modelObj.providerID ?? "";
30209
- const modelID = modelObj.modelID ?? modelObj.id ?? "";
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
- var handler10 = modelFallbackServer;
30423
+ }
30424
+ return removed;
30425
+ }
30262
30426
 
30263
- // plugins/parallel-tool-nudge.ts
30264
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
30265
- import { join as join21 } from "node:path";
30266
- import { homedir as homedir8 } from "node:os";
30267
- var PLUGIN_NAME11 = "parallel-tool-nudge";
30268
- logLifecycle(PLUGIN_NAME11, "import", {});
30269
- var PARALLEL_SAFE_TOOLS = [
30270
- "read",
30271
- "repo_map",
30272
- "webfetch"
30273
- ];
30274
- var DEFAULT_AGENT_KEY = "__default__";
30275
- var NUDGE_MAX_LEN2 = 800;
30276
- var agentToolsMap = new Map;
30277
- function defaultReader2(p) {
30278
- return readFileSync4(p, "utf8");
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 defaultDirReader2(p) {
30281
- return readdirSync2(p);
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 defaultDirExists2(p) {
30284
- try {
30285
- return statSync3(p).isDirectory();
30286
- } catch {
30287
- return false;
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 parseAgentFrontmatter(content) {
30291
- const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
30292
- if (!fmMatch || !fmMatch[1])
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
- parsed = $parse(fmMatch[1]);
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
- if (!parsed || typeof parsed !== "object")
30301
- return null;
30302
- const obj = parsed;
30303
- const name = typeof obj["name"] === "string" ? obj["name"] : null;
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
- const allowedTools = [];
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 loadAgentToolsMap(rootDir, opts = {}) {
30315
- const reader = opts.reader ?? defaultReader2;
30316
- const dirReader = opts.dirReader ?? defaultDirReader2;
30317
- const dirExists = opts.dirExists ?? defaultDirExists2;
30318
- const homeAgentsDir = opts.homeAgentsDir ?? join21(homedir8(), ".config", "opencode", "agents");
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
- result.set(DEFAULT_AGENT_KEY, Array.from(unionTools));
30375
- return result;
30519
+ return out;
30376
30520
  }
30377
- var sessionAgentMap2 = new Map;
30378
- var SESSION_CAP3 = 200;
30379
- var SESSION_TTL_MS3 = 24 * 60 * 60 * 1000;
30380
- function pruneIfOversize3() {
30381
- while (sessionAgentMap2.size > SESSION_CAP3) {
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
- function isExpired3(entry, now) {
30389
- return now - entry.ts > SESSION_TTL_MS3;
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 loaded = loadAgentToolsMap(ctx.directory ?? process.cwd());
30439
- agentToolsMap.clear();
30440
- for (const [k, v] of loaded.entries())
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
- const realAgentCount = Math.max(0, agentToolsMap.size - (agentToolsMap.has(DEFAULT_AGENT_KEY) ? 1 : 0));
30448
- if (realAgentCount === 0) {
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 defaultTools = agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
30455
- logLifecycle(PLUGIN_NAME11, "activate", {
30456
- directory: ctx.directory,
30457
- real_agents_loaded: realAgentCount,
30458
- default_tools_union: defaultTools,
30459
- parallel_safe_whitelist: PARALLEL_SAFE_TOOLS,
30460
- session_cap: SESSION_CAP3,
30461
- session_ttl_ms: SESSION_TTL_MS3,
30462
- no_op: agentToolsMap.size === 0
30463
- });
30464
- return {
30465
- "chat.params": async (input, _output) => {
30466
- await safeAsync(PLUGIN_NAME11, "chat.params", async () => {
30467
- if (agentToolsMap.size === 0)
30468
- return;
30469
- const sid = input?.sessionID;
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
- var handler11 = parallelToolNudgeServer;
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
- // lib/event-stream.ts
30548
- import { promises as fs18 } from "node:fs";
30568
+ // plugins/subtask-heartbeat.ts
30569
+ import { promises as fsPromises } from "node:fs";
30549
30570
  import * as path23 from "node:path";
30550
- async function loadSession(id, opts = {}) {
30551
- const file2 = resolveSessionFile(id, opts);
30552
- const raw = await fs18.readFile(file2, "utf8");
30553
- return parseJsonl(id, raw);
30554
- }
30555
- async function listSessions(opts = {}) {
30556
- const dir = resolveDir(opts);
30557
- let entries;
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
- entries = await fs18.readdir(dir, { withFileTypes: true });
30560
- } catch (err) {
30561
- if (err.code === "ENOENT")
30562
- return [];
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 resolveSessionFile(id, opts = {}) {
30600
- return path23.join(resolveDir(opts), `${id}.jsonl`);
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
- function parseJsonl(id, raw) {
30603
- const events = [];
30604
- let started_at = null;
30605
- for (const line of raw.split(/\r?\n/)) {
30606
- if (!line.trim())
30607
- continue;
30608
- let obj;
30609
- try {
30610
- obj = JSON.parse(line);
30611
- } catch {
30612
- continue;
30613
- }
30614
- if (!obj || typeof obj !== "object")
30615
- continue;
30616
- if (obj.__header) {
30617
- const h = obj;
30618
- if (typeof h.started_at === "number")
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
- if (isEvent(obj))
30623
- events.push(obj);
30641
+ return null;
30624
30642
  }
30625
- return { id, started_at, events };
30626
- }
30627
- function isEvent(obj) {
30628
- if (!obj || typeof obj !== "object")
30629
- return false;
30630
- const e = obj;
30631
- 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");
30632
- }
30633
- async function readFirstLine(file2) {
30634
- const buf = Buffer.alloc(4096);
30635
- const fh = await fs18.open(file2, "r");
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
- // lib/session-recovery.ts
30648
- var DEFAULT_OPTS = {
30649
- idleMs: 5 * 60000,
30650
- windowSize: 30,
30651
- excerptLimit: 200
30652
- };
30653
- var PENDING_CHANGES_TOOLS = new Set([
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 lastTs = meta.lastEventAt ?? events[events.length - 1].timestamp;
30684
- const idleMs = now - lastTs;
30685
- let lastUser = null;
30686
- for (let i = events.length - 1;i >= 0; i--) {
30687
- const e = events[i];
30688
- if (e.type === "message" && e.role === "user") {
30689
- lastUser = clip3(e.content, o.excerptLimit);
30690
- break;
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
- let lastAgent = null;
30694
- for (let i = events.length - 1;i >= 0; i--) {
30695
- const e = events[i];
30696
- if (e.type === "agent_switch") {
30697
- lastAgent = e.to;
30698
- break;
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
- if (e.agent && lastAgent === null)
30701
- lastAgent = e.agent;
30711
+ bucket = [];
30712
+ pendingTask.set(parentID, bucket);
30702
30713
  }
30703
- const window = events.slice(-o.windowSize).filter((e) => e.type === "tool_call");
30704
- const toolMap = new Map;
30705
- for (const t of window) {
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
- const inflight2 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
30724
- const proposed = window.some((t) => PENDING_CHANGES_TOOLS.has(t.tool));
30725
- const applied = window.some((t) => APPLY_TOOLS.has(t.tool) && t.ok);
30726
- const pending_changes_likely = proposed && !applied;
30727
- let openCount = 0;
30728
- for (const t of window) {
30729
- if (SUBTASK_OPEN.test(t.tool))
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
- const open_subtasks_likely = openCount > 0;
30735
- const lastEvent = events[events.length - 1];
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
- const summary = renderSummary2({
30743
- sessionId: meta.id,
30744
- idleMs,
30745
- lastUser,
30746
- lastAgent,
30747
- inflight: inflight2,
30748
- pending_changes_likely,
30749
- open_subtasks_likely,
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 renderSummary2(args) {
30765
- const lines = [];
30766
- lines.push(`\uD83D\uDCCD 上次会话 ${args.sessionId.slice(0, 8)}… 中断(${formatIdle(args.idleMs)}前 / ${args.reason})`);
30767
- if (args.lastUser)
30768
- lines.push(`▶ 用户上次说:"${args.lastUser}"`);
30769
- if (args.lastAgent)
30770
- lines.push(`\uD83D\uDC64 当时 agent:${args.lastAgent}`);
30771
- if (args.inflight.length > 0) {
30772
- const top = args.inflight.slice(0, 3).map((t) => `${t.tool}×${t.count}${t.last_ok ? "" : "✗"}`);
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
- if (args.pending_changes_likely)
30776
- lines.push("⚠ 似有暂存区改动尚未 apply(pending-changes propose 后无 apply)");
30777
- if (args.open_subtasks_likely)
30778
- lines.push("⚠ 似有子任务未关闭");
30779
- if (args.inflight.length === 0 && !args.pending_changes_likely && !args.open_subtasks_likely) {
30780
- lines.push("(未检测到 in-flight 工作;可继续 / 也可不恢复)");
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 lines.join(`
30783
- `);
30785
+ return out;
30784
30786
  }
30785
- function formatIdle(ms) {
30786
- if (ms < 0)
30787
- return "刚刚";
30788
- if (ms < 60000)
30789
- return `${Math.round(ms / 1000)}s`;
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 clip3(s, max) {
30793
+ function titleCase(s) {
30797
30794
  if (!s)
30798
- return "";
30799
- if (s.length <= max)
30800
30795
  return s;
30801
- return s.slice(0, max - 1) + "…";
30796
+ return s.charAt(0).toUpperCase() + s.slice(1);
30802
30797
  }
30803
- function safeJson(v) {
30804
- try {
30805
- return JSON.stringify(v);
30806
- } catch {
30807
- return String(v);
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
- async function scanLastSession(opts = {}) {
30811
- try {
30812
- const sessions = await listSessions(opts);
30813
- if (sessions.length === 0) {
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
- ok: true,
30816
- plan: emptyPlan("no_session")
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
- ok: false,
30836
- error: err instanceof Error ? err.message : String(err)
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
- last_session_id: null,
30843
- reason,
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 isRecoveryWorthShowing(plan) {
30854
- if (!plan.last_session_id)
30855
- return false;
30856
- if (plan.reason === "no_session")
30857
- return false;
30858
- const hasSignal = plan.pending_changes_likely || plan.open_subtasks_likely || plan.inflight_tools.length > 0 || plan.reason === "explicit_marker";
30859
- return hasSignal;
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
- // lib/block-pending.ts
30863
- import { promises as fs19 } from "node:fs";
30864
- import * as path24 from "node:path";
30865
- function blockPendingFilePath(absRoot) {
30866
- const rd = runtimeDir(absRoot, { ensure: false });
30867
- return path24.join(rd, "sessions", "autonomous-blocks.ndjson");
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 consumeLockPath(absRoot) {
30870
- return blockPendingFilePath(absRoot) + ".consume.lock";
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
- async function scanBlockPending(absRoot, filterSessionId) {
30873
- const file2 = blockPendingFilePath(absRoot);
30874
- let raw;
30875
- try {
30876
- raw = await fs19.readFile(file2, "utf8");
30877
- } catch {
30878
- return [];
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
- // plugins/session-recovery.ts
30948
- var PLUGIN_NAME13 = "session-recovery";
30949
- logLifecycle(PLUGIN_NAME13, "import", {});
30950
- async function processSessionStart(currentSessionId, opts = {}) {
30951
- if (opts.disabled) {
30952
- return { ok: true, injected: false, reason: "disabled" };
30953
- }
30954
- const excludeIds = new Set(opts.excludeIds ?? []);
30955
- if (currentSessionId)
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
- if (hasPendingBlocks && opts.root) {
30991
- try {
30992
- await markBlocksConsumed(opts.root, pendingBlocks);
30993
- } catch (err) {
30994
- opts.log?.warn?.(`[${PLUGIN_NAME13}] markBlocksConsumed 异常:${err instanceof Error ? err.message : String(err)}`);
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 renderBlockPendingPrompt(entries) {
31000
- const lines = [];
31001
- lines.push(`⚠️ **${entries.length} 个未读 BLOCK 通知**(上次 auto 模式 reviewer 阻断、channels 通知失败):`);
31002
- lines.push("");
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 renderPrompt(plan) {
31019
- const lines = [];
31020
- lines.push("【会话恢复提示】");
31021
- lines.push(plan.summary);
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
- if (plan.open_subtasks_likely) {
31028
- lines.push(" • 询问是否要继续上次未完成的子任务");
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
- if (plan.last_user_intent) {
31031
- lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
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
- if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
31034
- lines.push(" 询问是否要恢复上次的工作(信号较弱,可能不需要)");
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(PLUGIN_NAME13, "event", async () => {
31048
- const e = event;
31049
- if (!e || typeof e.type !== "string")
31050
- return;
31051
- if (e.type !== "session.start")
31052
- return;
31053
- const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
31054
- const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
31055
- const r = await processSessionStart(sid, {
31056
- root,
31057
- log: log8,
31058
- injectRecovery: (inj) => {
31059
- _lastInjection = inj;
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;
31533
+ }
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);
31060
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;
31560
+ }
31561
+ });
31562
+ }
31563
+ };
31564
+ };
31565
+ var handler11 = modelFallbackServer;
31566
+
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) {
31588
+ try {
31589
+ return statSync3(p).isDirectory();
31590
+ } catch {
31591
+ return false;
31592
+ }
31593
+ }
31594
+ function parseAgentFrontmatter(content) {
31595
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
31596
+ if (!fmMatch || !fmMatch[1])
31597
+ return null;
31598
+ let parsed;
31599
+ try {
31600
+ parsed = $parse(fmMatch[1]);
31601
+ } catch {
31602
+ return null;
31603
+ }
31604
+ if (!parsed || typeof parsed !== "object")
31605
+ return null;
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))
31610
+ return null;
31611
+ const allowedTools = [];
31612
+ for (const t of tools) {
31613
+ if (typeof t === "string")
31614
+ allowedTools.push(t);
31615
+ }
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)
31061
31656
  });
31062
- safeWriteLog(PLUGIN_NAME13, {
31063
- hook: "event",
31064
- type: "session.start",
31065
- ok: r.ok,
31066
- injected: r.injected,
31067
- reason: r.reason,
31068
- last_session_id: r.plan?.last_session_id,
31069
- pending_blocks_count: r.pendingBlocks?.length ?? 0
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);
31676
+ }
31677
+ }
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);
31690
+ }
31691
+ }
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
+ });
31750
+ }
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"
31070
31799
  });
31071
- if (r.injected && r.plan) {
31072
- log8.info(`[${PLUGIN_NAME13}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason}, pending_blocks=${r.pendingBlocks?.length ?? 0})`);
31800
+ });
31801
+ }
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;
31820
+ }
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
+ });
31073
31845
  }
31074
31846
  });
31075
31847
  }
31076
31848
  };
31077
31849
  };
31078
- var handler13 = sessionRecoveryServer;
31079
31850
 
31080
- // plugins/subtask-heartbeat.ts
31081
- import { promises as fsPromises } from "node:fs";
31082
- import * as path25 from "node:path";
31083
- var recordSessionParent2 = recordSessionParent;
31084
- var lookupParentSessionId2 = lookupParentSessionId;
31085
- var deleteSessionParent2 = deleteSessionParent;
31086
- var sweepExpiredSessionParents2 = sweepExpiredSessionParents;
31087
- var _bulkInjectSessionParentMap2 = _bulkInjectSessionParentMap;
31088
- var _capSessionParentMap2 = _capSessionParentMap;
31089
- var _setPersistRootForTests2 = _setPersistRootForTests;
31090
- var PLUGIN_NAME14 = "subtask-heartbeat";
31091
- logLifecycle(PLUGIN_NAME14, "import", {});
31092
- var HEARTBEAT_INTERVAL_MS2 = 30000;
31093
- var HEARTBEAT_DEBOUNCE_MS = 25000;
31094
- var TOAST_DURATION_MS2 = 5000;
31095
- var START_TOAST_DURATION_MS = 2000;
31096
- var PENDING_TASK_TTL_MS = 60000;
31097
- var PENDING_TASK_MAX_PARENTS = 64;
31098
- var PENDING_TASK_MAX_PER_PARENT = 16;
31099
- var DESCRIPTION_MAX_LEN = 60;
31100
- var PARENT_PARSE_FAIL_MAX_LOG = 10;
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;
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);
31858
+ }
31859
+ async function listSessions(opts = {}) {
31860
+ const dir = resolveDir(opts);
31861
+ let entries;
31112
31862
  try {
31113
- const json2 = JSON.stringify(event);
31114
- return /\bparent[_-]?[iI][dD]\b/.test(json2);
31115
- } catch {
31116
- return false;
31863
+ entries = await fs18.readdir(dir, { withFileTypes: true });
31864
+ } catch (err) {
31865
+ if (err.code === "ENOENT")
31866
+ return [];
31867
+ throw err;
31868
+ }
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 {}
31117
31895
  }
31896
+ out.sort((a, b) => b.started_at - a.started_at);
31897
+ return out;
31118
31898
  }
31119
- function extractCreatedChild(event) {
31120
- if (!event || typeof event !== "object")
31121
- return null;
31122
- const e = event;
31123
- if (e.type !== "session.created")
31124
- return null;
31125
- const info = e.properties?.info;
31126
- if (!info || typeof info !== "object")
31127
- return null;
31128
- const session = info;
31129
- if (typeof session.id !== "string")
31130
- return null;
31131
- if (typeof session.parentID !== "string" || session.parentID === "")
31132
- return null;
31133
- return { childID: session.id, parentID: session.parentID, agent: null };
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");
31134
31902
  }
31135
- var ERROR_REASON_MAX_LEN = 300;
31136
- function extractErrorReason(props) {
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) + "…";
31142
- }
31143
- function extractFromValue(v) {
31144
- if (typeof v === "string" && v.trim())
31145
- return sanitize(v);
31146
- if (v && typeof v === "object") {
31147
- const obj = v;
31148
- const dataMsg = obj["data"] && typeof obj["data"] === "object" ? obj["data"]["message"] : undefined;
31149
- const fromMsg = obj["message"] ?? dataMsg ?? obj["reason"];
31150
- if (typeof fromMsg === "string" && fromMsg.trim())
31151
- return sanitize(fromMsg);
31903
+ function resolveSessionFile(id, opts = {}) {
31904
+ return path24.join(resolveDir(opts), `${id}.jsonl`);
31905
+ }
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;
31152
31917
  }
31153
- return null;
31154
- }
31155
- const fromError = extractFromValue(props["error"]);
31156
- if (fromError !== null)
31157
- return fromError;
31158
- if (typeof props["message"] === "string" && props["message"].trim())
31159
- return sanitize(props["message"]);
31160
- if (typeof props["reason"] === "string" && props["reason"].trim())
31161
- return sanitize(props["reason"]);
31162
- if (props["info"] && typeof props["info"] === "object") {
31163
- const fromInfoError = extractFromValue(props["info"]["error"]);
31164
- if (fromInfoError !== null)
31165
- return fromInfoError;
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);
31166
31928
  }
31167
- return null;
31929
+ return { id, started_at, events };
31168
31930
  }
31169
- function extractEndedSessionID(event) {
31170
- if (!event || typeof event !== "object")
31171
- return null;
31172
- const e = event;
31173
- if (typeof e.type !== "string")
31174
- return null;
31175
- if (e.type !== "session.idle" && e.type !== "session.deleted" && e.type !== "session.error") {
31176
- return null;
31177
- }
31178
- const props = e.properties ?? {};
31179
- const direct = props["sessionID"];
31180
- const sessionID = typeof direct === "string" && direct ? direct : (() => {
31181
- const info = props["info"];
31182
- if (info && typeof info === "object") {
31183
- const sid = info.id;
31184
- if (typeof sid === "string" && sid)
31185
- return sid;
31186
- }
31187
- return null;
31188
- })();
31189
- if (!sessionID)
31190
- return null;
31191
- const errorReason = e.type === "session.error" ? extractErrorReason(props) : null;
31192
- return { type: e.type, sessionID, errorReason };
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");
31193
31936
  }
31194
- function extractTaskArgs(args) {
31195
- if (!args || typeof args !== "object")
31196
- return null;
31197
- const a = args;
31198
- const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
31199
- const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
31200
- const description17 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
31201
- 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;
31202
- if (!description17 && !subagentType)
31203
- return null;
31204
- return { description: description17, subagentType };
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
+ }
31205
31949
  }
31206
- function enqueuePendingTask(parentID, entry, now = Date.now()) {
31207
- const ts = entry.ts ?? now;
31208
- let bucket = pendingTask.get(parentID);
31209
- if (!bucket) {
31210
- if (pendingTask.size >= PENDING_TASK_MAX_PARENTS) {
31211
- let oldestKey = null;
31212
- let oldestTs = Number.POSITIVE_INFINITY;
31213
- for (const [k, v] of pendingTask.entries()) {
31214
- const headTs = v[0]?.ts ?? Number.POSITIVE_INFINITY;
31215
- if (headTs < oldestTs) {
31216
- oldestTs = headTs;
31217
- oldestKey = k;
31218
- }
31219
- }
31220
- if (oldestKey !== null)
31221
- pendingTask.delete(oldestKey);
31222
- }
31223
- bucket = [];
31224
- pendingTask.set(parentID, bucket);
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) {
31975
+ return {
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: "无历史会话,无需恢复。"
31985
+ };
31225
31986
  }
31226
- bucket.push({ agent: entry.agent, description: entry.description, ts });
31227
- while (bucket.length > PENDING_TASK_MAX_PER_PARENT) {
31228
- bucket.shift();
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
+ }
31229
31996
  }
31230
- }
31231
- function dequeuePendingTask(parentID, now = Date.now()) {
31232
- const bucket = pendingTask.get(parentID);
31233
- if (!bucket || bucket.length === 0) {
31234
- if (bucket)
31235
- pendingTask.delete(parentID);
31236
- return null;
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;
32003
+ }
32004
+ if (e.agent && lastAgent === null)
32005
+ lastAgent = e.agent;
31237
32006
  }
31238
- while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
31239
- bucket.shift();
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
+ }
31240
32026
  }
31241
- if (bucket.length === 0) {
31242
- pendingTask.delete(parentID);
31243
- return null;
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--;
31244
32037
  }
31245
- const entry = bucket.shift();
31246
- if (bucket.length === 0)
31247
- pendingTask.delete(parentID);
31248
- return entry;
31249
- }
31250
- function sweepExpiredPendingTasks(now = Date.now()) {
31251
- let removed = 0;
31252
- for (const [parentID, bucket] of [...pendingTask.entries()]) {
31253
- while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
31254
- bucket.shift();
31255
- removed++;
31256
- }
31257
- if (bucket.length === 0)
31258
- pendingTask.delete(parentID);
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";
31259
32045
  }
31260
- return removed;
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
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
+ });
32056
+ return {
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
31272
32066
  };
31273
- inflight2.set(payload.childID, r);
31274
- return r;
31275
- }
31276
- function recordToolBeat(sessionID, tool2, now = Date.now()) {
31277
- const r = inflight2.get(sessionID);
31278
- if (!r)
31279
- return null;
31280
- r.lastBeatAt = now;
31281
- r.lastTool = tool2;
31282
- return r;
31283
- }
31284
- function clearInflight2(sessionID) {
31285
- const r = inflight2.get(sessionID);
31286
- if (!r)
31287
- return null;
31288
- inflight2.delete(sessionID);
31289
- return r;
31290
32067
  }
31291
- function pickHeartbeats(now = Date.now()) {
31292
- const out = [];
31293
- for (const r of inflight2.values()) {
31294
- if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
31295
- out.push(r);
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(", ")}`);
31296
32078
  }
31297
- return out;
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 工作;可继续 / 也可不恢复)");
32085
+ }
32086
+ return lines.join(`
32087
+ `);
31298
32088
  }
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`;
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`;
31304
32099
  }
31305
- function titleCase(s) {
32100
+ function clip3(s, max) {
31306
32101
  if (!s)
32102
+ return "";
32103
+ if (s.length <= max)
31307
32104
  return s;
31308
- return s.charAt(0).toUpperCase() + s.slice(1);
31309
- }
31310
- function sanitizeDescription(s) {
31311
- const collapsed = s.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
31312
- if (collapsed.length <= DESCRIPTION_MAX_LEN)
31313
- return collapsed;
31314
- return collapsed.slice(0, DESCRIPTION_MAX_LEN) + "…";
32105
+ return s.slice(0, max - 1) + "…";
31315
32106
  }
31316
- function buildStartToast(r) {
31317
- if (r.agent && r.description) {
31318
- return {
31319
- message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动 — ${sanitizeDescription(r.description)}`,
31320
- variant: "info"
31321
- };
31322
- }
31323
- if (r.agent) {
31324
- return {
31325
- message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动`,
31326
- variant: "info"
31327
- };
32107
+ function safeJson(v) {
32108
+ try {
32109
+ return JSON.stringify(v);
32110
+ } catch {
32111
+ return String(v);
31328
32112
  }
31329
- return {
31330
- message: `\uD83D\uDE80 子 session 启动: subagent`,
31331
- variant: "info"
31332
- };
31333
- }
31334
- function buildHeartbeatToast(r, now = Date.now()) {
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
32113
  }
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") {
32114
+ async function scanLastSession(opts = {}) {
32115
+ try {
32116
+ const sessions = await listSessions(opts);
32117
+ if (sessions.length === 0) {
31349
32118
  return {
31350
- message: `${emoji3} ${titleCase(r.agent)} Task — ${sanitizeDescription(r.description)} (${elapsed})`,
31351
- variant
32119
+ ok: true,
32120
+ plan: emptyPlan("no_session")
31352
32121
  };
31353
32122
  }
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) {
31354
32138
  return {
31355
- message: `${emoji3} ${titleCase(r.agent)} ${verb} — ${sanitizeDescription(r.description)} (${elapsed})`,
31356
- variant
31357
- };
31358
- }
31359
- if (r.agent) {
31360
- return {
31361
- message: `${emoji3} ${titleCase(r.agent)} ${verb} (${elapsed})`,
31362
- variant
32139
+ ok: false,
32140
+ error: err instanceof Error ? err.message : String(err)
31363
32141
  };
31364
32142
  }
32143
+ }
32144
+ function emptyPlan(reason) {
31365
32145
  return {
31366
- message: `${emoji3} subagent ${verb} (${elapsed})`,
31367
- variant
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: "无历史会话,无需恢复。"
31368
32155
  };
31369
32156
  }
31370
- function sanitizeInputSummary(toolName, args) {
31371
- if (!args || typeof args !== "object")
31372
- return `${toolName} (no args)`;
31373
- const a = args;
31374
- const PATH_KEYS = [
31375
- "path",
31376
- "file",
31377
- "filePath",
31378
- "file_path",
31379
- "filepath",
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
- }
31387
- }
31388
- const cmd = a["command"];
31389
- if (typeof cmd === "string" && cmd.length > 0) {
31390
- const head = cmd.replace(/\s+/g, " ").trim().slice(0, 60);
31391
- return `${toolName} $ ${head}`;
31392
- }
31393
- return `${toolName} (no path)`;
32157
+ function isRecoveryWorthShowing(plan) {
32158
+ if (!plan.last_session_id)
32159
+ return false;
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;
31394
32164
  }
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
- });
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");
31402
32172
  }
31403
- function buildAfterLogLine(toolName, ok, durationMs, now = Date.now()) {
31404
- return JSON.stringify({
31405
- ts: Math.floor(now / 1000),
31406
- tool: toolName,
31407
- phase: "after",
31408
- ok,
31409
- duration_ms: durationMs
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;
32179
+ try {
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}`);
32206
+ }
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);
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;
31410
32230
  });
31411
32231
  }
31412
- function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
31413
- return JSON.stringify({
31414
- ts: Math.floor(now / 1000),
31415
- phase: "error",
31416
- ok: false,
31417
- error_reason: errorReason ?? "(unknown)",
31418
- elapsed_ms: elapsedMs
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");
31419
32248
  });
31420
32249
  }
31421
- async function appendSubagentLog(filePath, line, log9) {
31422
- try {
31423
- await fsPromises.mkdir(path25.dirname(filePath), { recursive: true });
31424
- await fsPromises.appendFile(filePath, line + `
31425
- `, "utf8");
31426
- } catch (err) {
31427
- log9?.debug?.("appendSubagentLog 失败(已隔离)", {
31428
- error: err instanceof Error ? err.message : String(err),
31429
- file: filePath
31430
- });
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" };
31431
32257
  }
31432
- }
31433
- function isLogPersistenceEnabled(cwd) {
31434
- try {
31435
- const cfg = getCodeforgeConfig({ root: cwd });
31436
- const runtime = cfg.runtime;
31437
- if (runtime && typeof runtime === "object") {
31438
- const v = runtime.subagent_log_persistence;
31439
- if (v === false)
31440
- return false;
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)}`);
31441
32299
  }
31442
- return true;
31443
- } catch {
31444
- return true;
31445
32300
  }
32301
+ return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
31446
32302
  }
31447
- function normalizeVariant2(raw) {
31448
- if (raw === "info" || raw === "warning")
31449
- return "default";
31450
- return raw;
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
+ > `)}`);
32315
+ }
32316
+ });
32317
+ lines.push("");
32318
+ lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
32319
+ return lines.join(`
32320
+ `);
31451
32321
  }
31452
- async function showToast2(client, payload, log9) {
31453
- if (typeof client?.tui?.showToast !== "function") {
31454
- log9?.debug?.("tui.showToast 不可用,noop");
31455
- return false;
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 / 丢弃");
32330
+ }
32331
+ if (plan.open_subtasks_likely) {
32332
+ lines.push(" • 询问是否要继续上次未完成的子任务");
32333
+ }
32334
+ if (plan.last_user_intent) {
32335
+ lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
31456
32336
  }
31457
- try {
31458
- await client.tui.showToast({
31459
- body: {
31460
- message: payload.message,
31461
- variant: normalizeVariant2(payload.variant),
31462
- duration: payload.duration ?? TOAST_DURATION_MS2,
31463
- title: payload.title ?? "CodeForge"
31464
- }
31465
- });
31466
- return true;
31467
- } catch (err) {
31468
- log9?.warn("tui.showToast 抛错(已隔离)", {
31469
- error: err instanceof Error ? err.message : String(err)
31470
- });
31471
- return false;
32337
+ if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
32338
+ lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
31472
32339
  }
32340
+ lines.push("");
32341
+ lines.push("(如果用户明确开了新话题,本提示可忽略)");
32342
+ return lines.join(`
32343
+ `);
31473
32344
  }
31474
32345
  var log9 = makePluginLogger(PLUGIN_NAME14);
31475
- var subtaskHeartbeatServer = async (ctx) => {
31476
- logLifecycle(PLUGIN_NAME14, "activate", {
31477
- directory: ctx.directory,
31478
- intervalMs: HEARTBEAT_INTERVAL_MS2
31479
- });
31480
- const client = ctx.client;
31481
- const cwd = ctx.directory;
31482
- _setPersistRootForTests2(cwd);
31483
- try {
31484
- const restored = await loadParentMap(cwd);
31485
- if (restored.size > 0) {
31486
- const entries = [...restored.entries()].map(([childID, v]) => ({
31487
- childID,
31488
- parentID: v.parentID,
31489
- ts: v.ts
31490
- }));
31491
- _bulkInjectSessionParentMap2(entries);
31492
- const cappedOut = _capSessionParentMap2();
31493
- safeWriteLog(PLUGIN_NAME14, {
31494
- hook: "activate",
31495
- type: "parent-map.restore",
31496
- restored: restored.size,
31497
- capped: cappedOut
31498
- });
31499
- }
31500
- } catch (err) {
31501
- log9.warn("loadParentMap 失败(已隔离),降级为空表", {
31502
- error: err instanceof Error ? err.message : String(err)
31503
- });
31504
- }
31505
- const interval = setInterval(() => {
31506
- safeAsync(PLUGIN_NAME14, "interval", async () => {
31507
- const swept = sweepExpiredPendingTasks();
31508
- if (swept > 0) {
31509
- safeWriteLog(PLUGIN_NAME14, { hook: "interval", pending_task_swept: swept });
31510
- }
31511
- const sweptParents = sweepExpiredSessionParents2();
31512
- if (sweptParents > 0) {
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();
31535
- }
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
- try {
31540
- if (detectUnparsedParentID(event) && _parentParseFailLogged < PARENT_PARSE_FAIL_MAX_LOG) {
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
- const rec = inflight2.get(input.sessionID);
31703
- if (!rec)
32355
+ if (e.type !== "session.start")
31704
32356
  return;
31705
- let durationMs = 0;
31706
- if (rec.pendingCalls && typeof input.callID === "string") {
31707
- const startedAt = rec.pendingCalls.get(input.callID);
31708
- if (typeof startedAt === "number") {
31709
- durationMs = Date.now() - startedAt;
31710
- rec.pendingCalls.delete(input.callID);
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
- if (isLogPersistenceEnabled(cwd)) {
31714
- const line = buildAfterLogLine(input.tool, true, durationMs);
31715
- const file2 = subagentLogPath(cwd, rec.parentID, rec.childID);
31716
- appendSubagentLog(file2, line, log9);
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 = subtaskHeartbeatServer;
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.18";
33420
+ const v = "0.8.19";
32761
33421
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
32762
33422
  return v;
32763
33423
  }
@@ -35137,11 +35797,11 @@ var HANDLERS = [
35137
35797
  { name: "codeforge-tools", init: handler7 },
35138
35798
  { name: "discover-spec-suggest", init: handler8 },
35139
35799
  { name: "memories-context", init: handler9 },
35140
- { name: "model-fallback", init: handler10 },
35141
- { name: "parallel-tool-nudge", init: handler11 },
35142
- { name: "pwsh-utf8", init: handler12 },
35143
- { name: "session-recovery", init: handler13 },
35144
- { name: "subtask-heartbeat", init: handler14 },
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 },
35145
35805
  { name: "subtasks", init: handler15 },
35146
35806
  { name: "terminal-monitor", init: handler16 },
35147
35807
  { name: "token-manager", init: handler17 },