engine7 7.1.31 → 7.1.33

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.
@@ -711,7 +711,10 @@ async function executeUserPromptSubmitHooks(prompt, ctx, signal) {
711
711
  session_id: ctx.sessionId,
712
712
  transcript_path: "",
713
713
  cwd: ctx.cwd,
714
- prompt
714
+ prompt,
715
+ sender_id: ctx.senderId || "",
716
+ channel_type: ctx.channelType || "",
717
+ source: ctx.source || ""
715
718
  };
716
719
  return executeHooks("UserPromptSubmit", hookInput, ctx, signal);
717
720
  }
@@ -728,6 +731,8 @@ async function executeStopHooks(ctx, signal, lastAssistantMessage) {
728
731
  last_assistant_message: lastAssistantMessage,
729
732
  channel: ctx.channel,
730
733
  // 让 callback hook 能判断来源
734
+ channelType: ctx.channelType || "",
735
+ // 'group' | 'dm' — Stop hook 用来判断是否跳过 group
731
736
  source: ctx.source || ""
732
737
  // 消息来源(heartbeat/cron/system 等注入 turn 可据此跳过)
733
738
  };
@@ -3108,6 +3113,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3108
3113
  sessionId: context?.sessionId || "default",
3109
3114
  workspace: context?.workspace || "",
3110
3115
  channel: context?.channel || "",
3116
+ channelType: context?.channelType || "",
3117
+ // 'group' | 'dm',Stop hook 用来判断是否跳过 group
3111
3118
  cwd: context?.workspace || "",
3112
3119
  source: typeof context?.source === "string" ? context.source : ""
3113
3120
  };
@@ -18813,7 +18820,10 @@ ${text}` : text });
18813
18820
  sessionId,
18814
18821
  workspace,
18815
18822
  channel: channelName === "cli" ? "console" : channelName,
18816
- cwd: workspace
18823
+ cwd: workspace,
18824
+ senderId: inboundMeta?.from || "",
18825
+ channelType: inboundMeta?.channelType || "",
18826
+ source
18817
18827
  });
18818
18828
  if (hookResult.additionalContexts && hookResult.additionalContexts.length > 0) {
18819
18829
  hookAdditionalContexts = hookResult.additionalContexts;
@@ -18872,6 +18882,8 @@ ${text}` : text });
18872
18882
  const toolContext = {
18873
18883
  sessionId,
18874
18884
  channel: channelName === "cli" ? "console" : channelName,
18885
+ channelType: inboundMeta?.channelType || "",
18886
+ // 'group' | 'dm',Stop hook 用来判断是否跳过 group(注入消息无 inboundMeta,必须 ?.)
18875
18887
  source: source || "",
18876
18888
  // 消息来源(user/inbox/heartbeat/cron/system/inner-voice),Stop hook 用来区分注入 turn
18877
18889
  workspace,
@@ -20295,160 +20307,192 @@ var NudgePlugin = class {
20295
20307
  registerCallbackHook("Stop", {
20296
20308
  type: "callback",
20297
20309
  callback: async (input, _toolUseID, _signal) => {
20298
- console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
20299
- const lastMsg = input?.last_assistant_message || "";
20300
- const sessionId = input?.session_id || "";
20301
- console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
20302
- const repliedIds = this.extractWakeReplyIds(lastMsg);
20303
- if (repliedIds.length > 0) {
20304
- this.removeNotificationsById(repliedIds);
20305
- }
20306
- const msgChannel = input?.channel || "";
20307
- if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
20308
- console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
20309
- return { outcome: { outcome: "success" } };
20310
- }
20311
- const msgSource = input?.source || "";
20312
- if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
20313
- console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
20314
- return { outcome: { outcome: "success" } };
20315
- }
20316
- if (!lastMsg) {
20317
- return { outcome: { outcome: "success" } };
20318
- }
20319
- let contextStr = "";
20320
- try {
20321
- const recent = recentMessages(sessions, 0.5, 6);
20322
- contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
20323
- console.log(`[stop-hook] ====== JUDGE INPUT ======`);
20324
- console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
20325
- recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
20326
- console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
20327
- console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
20328
- } catch (e) {
20329
- console.warn(`[stop-hook] recentMessages error: ${e.message}`);
20330
- }
20331
- const judgeInput = `${contextStr}
20332
- \u4F60: ${lastMsg.slice(0, 300)}`;
20333
- let isWaiting = false;
20334
- let waitDesc = "";
20335
- let pushedDecision = false;
20310
+ const timeoutMs = this.cfg.timeoutMs ?? 15e3;
20311
+ let judgeTimer;
20312
+ const judgeTimeout = new Promise((_, reject) => {
20313
+ judgeTimer = setTimeout(() => reject(new Error(`stop-hook judge ${timeoutMs}ms timeout`)), timeoutMs);
20314
+ });
20336
20315
  try {
20337
- const excludeCases = this.cfg.stopHookExcludeCases || [];
20338
- const excludeSection = excludeCases.length > 0 ? [
20339
- "",
20340
- "\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
20341
- ...excludeCases.map((c, i) => `- ${c}`)
20342
- ].join("\n") : "";
20343
- const stream = this.provider.streamChat({
20344
- model: this.model,
20345
- systemPrompt: [
20346
- "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
20347
- "",
20348
- "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20349
- '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20350
- "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20351
- "",
20352
- "\u5173\u952E\u533A\u5206\uFF1A",
20353
- '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20354
- '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20355
- "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20356
- excludeSection,
20357
- "",
20358
- "\u53EA\u56DE\u590D JSON\uFF1A",
20359
- '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20360
- '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20361
- '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20362
- ].join("\n"),
20363
- messages: [{ role: "user", content: judgeInput }],
20364
- maxTokens: 128,
20365
- temperature: 0.1,
20366
- disableThinking: true
20367
- });
20368
- let llmReply = "";
20369
- for await (const chunk of stream) {
20370
- if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
20371
- if (chunk.type === "error") break;
20372
- }
20373
- console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
20374
- const match = llmReply.match(/\{[\s\S]*\}/);
20375
- if (match) {
20376
- const parsed = JSON.parse(match[0]);
20377
- isWaiting = !!parsed.waiting;
20378
- waitDesc = parsed.desc || "";
20379
- pushedDecision = !!parsed.pushedDecision;
20380
- }
20381
- console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20316
+ await Promise.race([
20317
+ this.runStopHookJudge(input, sessions),
20318
+ judgeTimeout
20319
+ ]);
20382
20320
  } catch (e) {
20383
- console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20384
- }
20385
- if (pushedDecision && waitDesc) {
20386
- console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
20387
- try {
20388
- const correctiveMsg = buildNudgeNotification("prompt", [
20389
- "[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
20390
- "",
20391
- `\u8BCA\u65AD\uFF1A${waitDesc}`,
20392
- "",
20393
- "\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
20394
- "\u95EE = \u5361\u4F4F\u3002",
20395
- "",
20396
- "\u8BF7\u7ACB\u523B\uFF1A",
20397
- "1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
20398
- "2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
20399
- '3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
20400
- "",
20401
- "\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
20402
- ].join("\n"));
20403
- const route = this.getRoute(sessions);
20404
- if (route) {
20405
- enqueueNotification(correctiveMsg, route);
20406
- }
20407
- } catch (e) {
20408
- console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20321
+ if (/judge \d+ms timeout/i.test(e?.message || "")) {
20322
+ console.warn(`[stop-hook] judge timed out after ${timeoutMs}ms \u2014 abandoning this turn (no corrective, no wake-up; next stop-hook in ~5min)`);
20323
+ } else {
20324
+ console.warn(`[stop-hook] judge error: ${e.message}`);
20409
20325
  }
20326
+ } finally {
20327
+ if (judgeTimer) clearTimeout(judgeTimer);
20410
20328
  }
20411
- if (!isWaiting) {
20412
- return { outcome: { outcome: "success" } };
20329
+ return { outcome: { outcome: "success" } };
20330
+ }
20331
+ });
20332
+ console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up, hard timeout via Promise.race)");
20333
+ }
20334
+ /** Judge 完整逻辑(被 callback 用 Promise.race 调用,可被 timeout 截断) */
20335
+ async runStopHookJudge(input, sessions) {
20336
+ console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
20337
+ const lastMsg = input?.last_assistant_message || "";
20338
+ const sessionId = input?.session_id || "";
20339
+ const inputChannel = input?.channel || "";
20340
+ const inputChannelType = input?.channelType || "";
20341
+ console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
20342
+ if (inputChannelType === "group") {
20343
+ const channelCfg = this.cfg.channels?.[inputChannel];
20344
+ const stopHookEnabled = channelCfg?.group?.stopHookEnabled;
20345
+ if (stopHookEnabled !== true) {
20346
+ console.log(`[stop-hook] skipping ${inputChannel} group (stopHookEnabled=${stopHookEnabled ?? "undefined"}, default false)`);
20347
+ return;
20348
+ }
20349
+ }
20350
+ const repliedIds = this.extractWakeReplyIds(lastMsg);
20351
+ if (repliedIds.length > 0) {
20352
+ this.removeNotificationsById(repliedIds);
20353
+ }
20354
+ const msgChannel = inputChannel;
20355
+ if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
20356
+ console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
20357
+ return;
20358
+ }
20359
+ const msgSource = input?.source || "";
20360
+ if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
20361
+ console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
20362
+ return;
20363
+ }
20364
+ if (!lastMsg) {
20365
+ return;
20366
+ }
20367
+ let contextStr = "";
20368
+ try {
20369
+ const recent = recentMessages(sessions, 0.5, 6);
20370
+ contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
20371
+ console.log(`[stop-hook] ====== JUDGE INPUT ======`);
20372
+ console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
20373
+ recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
20374
+ console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
20375
+ console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
20376
+ } catch (e) {
20377
+ console.warn(`[stop-hook] recentMessages error: ${e.message}`);
20378
+ }
20379
+ const judgeInput = `${contextStr}
20380
+ \u4F60: ${lastMsg.slice(0, 300)}`;
20381
+ let isWaiting = false;
20382
+ let waitDesc = "";
20383
+ let pushedDecision = false;
20384
+ try {
20385
+ const excludeCases = this.cfg.stopHookExcludeCases || [];
20386
+ const excludeSection = excludeCases.length > 0 ? [
20387
+ "",
20388
+ "\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
20389
+ ...excludeCases.map((c, i) => `- ${c}`)
20390
+ ].join("\n") : "";
20391
+ const stream = this.provider.streamChat({
20392
+ model: this.model,
20393
+ systemPrompt: [
20394
+ "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
20395
+ "",
20396
+ "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20397
+ '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20398
+ "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20399
+ "",
20400
+ "\u5173\u952E\u533A\u5206\uFF1A",
20401
+ '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20402
+ '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20403
+ "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20404
+ excludeSection,
20405
+ "",
20406
+ "\u53EA\u56DE\u590D JSON\uFF1A",
20407
+ '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20408
+ '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20409
+ '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20410
+ ].join("\n"),
20411
+ messages: [{ role: "user", content: judgeInput }],
20412
+ maxTokens: 128,
20413
+ temperature: 0.1,
20414
+ disableThinking: true
20415
+ });
20416
+ let llmReply = "";
20417
+ for await (const chunk of stream) {
20418
+ if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
20419
+ if (chunk.type === "error") break;
20420
+ }
20421
+ console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
20422
+ const match = llmReply.match(/\{[\s\S]*\}/);
20423
+ if (match) {
20424
+ const parsed = JSON.parse(match[0]);
20425
+ isWaiting = !!parsed.waiting;
20426
+ waitDesc = parsed.desc || "";
20427
+ pushedDecision = !!parsed.pushedDecision;
20428
+ }
20429
+ console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20430
+ } catch (e) {
20431
+ console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20432
+ }
20433
+ if (pushedDecision && waitDesc) {
20434
+ console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
20435
+ try {
20436
+ const correctiveMsg = buildNudgeNotification("prompt", [
20437
+ "[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
20438
+ "",
20439
+ `\u8BCA\u65AD\uFF1A${waitDesc}`,
20440
+ "",
20441
+ "\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
20442
+ "\u95EE = \u5361\u4F4F\u3002",
20443
+ "",
20444
+ "\u8BF7\u7ACB\u523B\uFF1A",
20445
+ "1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
20446
+ "2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
20447
+ '3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
20448
+ "",
20449
+ "\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
20450
+ ].join("\n"));
20451
+ const route = this.getRoute(sessions);
20452
+ if (route) {
20453
+ enqueueNotification(correctiveMsg, route);
20413
20454
  }
20414
- const nudgeDir = path18.join(this.workspace, ".nudge");
20415
- const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
20416
- try {
20417
- if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
20418
- let notifs = [];
20419
- if (fs17.existsSync(notifPath)) {
20420
- notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
20421
- const now = Date.now();
20422
- const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
20423
- if (dup) {
20424
- dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
20425
- fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20426
- console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
20427
- return { outcome: { outcome: "success" } };
20428
- }
20429
- const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
20430
- if (recentReg) {
20431
- console.log(`[stop-hook] Skip (recent registration within 3min)`);
20432
- return { outcome: { outcome: "success" } };
20433
- }
20434
- }
20435
- const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
20436
- const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
20437
- notifs.push({
20438
- id: notifId,
20439
- description: waitDesc || lastMsg.slice(0, 200),
20440
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20441
- wakeAt
20442
- });
20455
+ } catch (e) {
20456
+ console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20457
+ }
20458
+ }
20459
+ if (!isWaiting) {
20460
+ return;
20461
+ }
20462
+ const nudgeDir = path18.join(this.workspace, ".nudge");
20463
+ const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
20464
+ try {
20465
+ if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
20466
+ let notifs = [];
20467
+ if (fs17.existsSync(notifPath)) {
20468
+ notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
20469
+ const now = Date.now();
20470
+ const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
20471
+ if (dup) {
20472
+ dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
20443
20473
  fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20444
- console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
20445
- } catch (e) {
20446
- console.warn(`[stop-hook] Failed to register: ${e.message}`);
20474
+ console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
20475
+ return;
20476
+ }
20477
+ const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
20478
+ if (recentReg) {
20479
+ console.log(`[stop-hook] Skip (recent registration within 3min)`);
20480
+ return;
20447
20481
  }
20448
- return { outcome: { outcome: "success" } };
20449
20482
  }
20450
- });
20451
- console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up)");
20483
+ const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
20484
+ const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
20485
+ notifs.push({
20486
+ id: notifId,
20487
+ description: waitDesc || lastMsg.slice(0, 200),
20488
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20489
+ wakeAt
20490
+ });
20491
+ fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20492
+ console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
20493
+ } catch (e) {
20494
+ console.warn(`[stop-hook] Failed to register: ${e.message}`);
20495
+ }
20452
20496
  }
20453
20497
  stop() {
20454
20498
  if (this.timer) {
@@ -28056,9 +28100,11 @@ ${notifications}
28056
28100
  const modelChoices = [{ name: "\u{1F504} auto\uFF08\u6062\u590D\u9ED8\u8BA4\uFF09", value: "auto" }];
28057
28101
  if (config.providers) {
28058
28102
  for (const [pid, pCfg] of Object.entries(config.providers)) {
28103
+ if (modelChoices.length >= 25) break;
28059
28104
  const models = pCfg.models;
28060
28105
  if (!models) continue;
28061
28106
  for (const m of models) {
28107
+ if (modelChoices.length >= 25) break;
28062
28108
  const ref = `${pid}/${m.id}`;
28063
28109
  const alias = aliasByRef.get(ref);
28064
28110
  const label = alias ? `${alias} \u2014 ${m.name || m.id}` : `${m.name || m.id} (${pid})`;
package/dist/main.mjs CHANGED
@@ -711,7 +711,10 @@ async function executeUserPromptSubmitHooks(prompt, ctx, signal) {
711
711
  session_id: ctx.sessionId,
712
712
  transcript_path: "",
713
713
  cwd: ctx.cwd,
714
- prompt
714
+ prompt,
715
+ sender_id: ctx.senderId || "",
716
+ channel_type: ctx.channelType || "",
717
+ source: ctx.source || ""
715
718
  };
716
719
  return executeHooks("UserPromptSubmit", hookInput, ctx, signal);
717
720
  }
@@ -728,6 +731,8 @@ async function executeStopHooks(ctx, signal, lastAssistantMessage) {
728
731
  last_assistant_message: lastAssistantMessage,
729
732
  channel: ctx.channel,
730
733
  // 让 callback hook 能判断来源
734
+ channelType: ctx.channelType || "",
735
+ // 'group' | 'dm' — Stop hook 用来判断是否跳过 group
731
736
  source: ctx.source || ""
732
737
  // 消息来源(heartbeat/cron/system 等注入 turn 可据此跳过)
733
738
  };
@@ -3108,6 +3113,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3108
3113
  sessionId: context?.sessionId || "default",
3109
3114
  workspace: context?.workspace || "",
3110
3115
  channel: context?.channel || "",
3116
+ channelType: context?.channelType || "",
3117
+ // 'group' | 'dm',Stop hook 用来判断是否跳过 group
3111
3118
  cwd: context?.workspace || "",
3112
3119
  source: typeof context?.source === "string" ? context.source : ""
3113
3120
  };
@@ -19187,7 +19194,10 @@ ${text}` : text });
19187
19194
  sessionId,
19188
19195
  workspace,
19189
19196
  channel: channelName === "cli" ? "console" : channelName,
19190
- cwd: workspace
19197
+ cwd: workspace,
19198
+ senderId: inboundMeta?.from || "",
19199
+ channelType: inboundMeta?.channelType || "",
19200
+ source
19191
19201
  });
19192
19202
  if (hookResult.additionalContexts && hookResult.additionalContexts.length > 0) {
19193
19203
  hookAdditionalContexts = hookResult.additionalContexts;
@@ -19246,6 +19256,8 @@ ${text}` : text });
19246
19256
  const toolContext = {
19247
19257
  sessionId,
19248
19258
  channel: channelName === "cli" ? "console" : channelName,
19259
+ channelType: inboundMeta?.channelType || "",
19260
+ // 'group' | 'dm',Stop hook 用来判断是否跳过 group(注入消息无 inboundMeta,必须 ?.)
19249
19261
  source: source || "",
19250
19262
  // 消息来源(user/inbox/heartbeat/cron/system/inner-voice),Stop hook 用来区分注入 turn
19251
19263
  workspace,
@@ -20669,160 +20681,192 @@ var NudgePlugin = class {
20669
20681
  registerCallbackHook("Stop", {
20670
20682
  type: "callback",
20671
20683
  callback: async (input, _toolUseID, _signal) => {
20672
- console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
20673
- const lastMsg = input?.last_assistant_message || "";
20674
- const sessionId = input?.session_id || "";
20675
- console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
20676
- const repliedIds = this.extractWakeReplyIds(lastMsg);
20677
- if (repliedIds.length > 0) {
20678
- this.removeNotificationsById(repliedIds);
20679
- }
20680
- const msgChannel = input?.channel || "";
20681
- if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
20682
- console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
20683
- return { outcome: { outcome: "success" } };
20684
- }
20685
- const msgSource = input?.source || "";
20686
- if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
20687
- console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
20688
- return { outcome: { outcome: "success" } };
20689
- }
20690
- if (!lastMsg) {
20691
- return { outcome: { outcome: "success" } };
20692
- }
20693
- let contextStr = "";
20694
- try {
20695
- const recent = recentMessages(sessions, 0.5, 6);
20696
- contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
20697
- console.log(`[stop-hook] ====== JUDGE INPUT ======`);
20698
- console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
20699
- recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
20700
- console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
20701
- console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
20702
- } catch (e) {
20703
- console.warn(`[stop-hook] recentMessages error: ${e.message}`);
20704
- }
20705
- const judgeInput = `${contextStr}
20706
- \u4F60: ${lastMsg.slice(0, 300)}`;
20707
- let isWaiting = false;
20708
- let waitDesc = "";
20709
- let pushedDecision = false;
20684
+ const timeoutMs = this.cfg.timeoutMs ?? 15e3;
20685
+ let judgeTimer;
20686
+ const judgeTimeout = new Promise((_, reject) => {
20687
+ judgeTimer = setTimeout(() => reject(new Error(`stop-hook judge ${timeoutMs}ms timeout`)), timeoutMs);
20688
+ });
20710
20689
  try {
20711
- const excludeCases = this.cfg.stopHookExcludeCases || [];
20712
- const excludeSection = excludeCases.length > 0 ? [
20713
- "",
20714
- "\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
20715
- ...excludeCases.map((c, i) => `- ${c}`)
20716
- ].join("\n") : "";
20717
- const stream = this.provider.streamChat({
20718
- model: this.model,
20719
- systemPrompt: [
20720
- "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
20721
- "",
20722
- "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20723
- '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20724
- "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20725
- "",
20726
- "\u5173\u952E\u533A\u5206\uFF1A",
20727
- '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20728
- '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20729
- "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20730
- excludeSection,
20731
- "",
20732
- "\u53EA\u56DE\u590D JSON\uFF1A",
20733
- '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20734
- '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20735
- '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20736
- ].join("\n"),
20737
- messages: [{ role: "user", content: judgeInput }],
20738
- maxTokens: 128,
20739
- temperature: 0.1,
20740
- disableThinking: true
20741
- });
20742
- let llmReply = "";
20743
- for await (const chunk of stream) {
20744
- if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
20745
- if (chunk.type === "error") break;
20746
- }
20747
- console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
20748
- const match = llmReply.match(/\{[\s\S]*\}/);
20749
- if (match) {
20750
- const parsed = JSON.parse(match[0]);
20751
- isWaiting = !!parsed.waiting;
20752
- waitDesc = parsed.desc || "";
20753
- pushedDecision = !!parsed.pushedDecision;
20754
- }
20755
- console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20690
+ await Promise.race([
20691
+ this.runStopHookJudge(input, sessions),
20692
+ judgeTimeout
20693
+ ]);
20756
20694
  } catch (e) {
20757
- console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20758
- }
20759
- if (pushedDecision && waitDesc) {
20760
- console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
20761
- try {
20762
- const correctiveMsg = buildNudgeNotification("prompt", [
20763
- "[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
20764
- "",
20765
- `\u8BCA\u65AD\uFF1A${waitDesc}`,
20766
- "",
20767
- "\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
20768
- "\u95EE = \u5361\u4F4F\u3002",
20769
- "",
20770
- "\u8BF7\u7ACB\u523B\uFF1A",
20771
- "1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
20772
- "2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
20773
- '3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
20774
- "",
20775
- "\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
20776
- ].join("\n"));
20777
- const route = this.getRoute(sessions);
20778
- if (route) {
20779
- enqueueNotification(correctiveMsg, route);
20780
- }
20781
- } catch (e) {
20782
- console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20695
+ if (/judge \d+ms timeout/i.test(e?.message || "")) {
20696
+ console.warn(`[stop-hook] judge timed out after ${timeoutMs}ms \u2014 abandoning this turn (no corrective, no wake-up; next stop-hook in ~5min)`);
20697
+ } else {
20698
+ console.warn(`[stop-hook] judge error: ${e.message}`);
20783
20699
  }
20700
+ } finally {
20701
+ if (judgeTimer) clearTimeout(judgeTimer);
20784
20702
  }
20785
- if (!isWaiting) {
20786
- return { outcome: { outcome: "success" } };
20703
+ return { outcome: { outcome: "success" } };
20704
+ }
20705
+ });
20706
+ console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up, hard timeout via Promise.race)");
20707
+ }
20708
+ /** Judge 完整逻辑(被 callback 用 Promise.race 调用,可被 timeout 截断) */
20709
+ async runStopHookJudge(input, sessions) {
20710
+ console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
20711
+ const lastMsg = input?.last_assistant_message || "";
20712
+ const sessionId = input?.session_id || "";
20713
+ const inputChannel = input?.channel || "";
20714
+ const inputChannelType = input?.channelType || "";
20715
+ console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
20716
+ if (inputChannelType === "group") {
20717
+ const channelCfg = this.cfg.channels?.[inputChannel];
20718
+ const stopHookEnabled = channelCfg?.group?.stopHookEnabled;
20719
+ if (stopHookEnabled !== true) {
20720
+ console.log(`[stop-hook] skipping ${inputChannel} group (stopHookEnabled=${stopHookEnabled ?? "undefined"}, default false)`);
20721
+ return;
20722
+ }
20723
+ }
20724
+ const repliedIds = this.extractWakeReplyIds(lastMsg);
20725
+ if (repliedIds.length > 0) {
20726
+ this.removeNotificationsById(repliedIds);
20727
+ }
20728
+ const msgChannel = inputChannel;
20729
+ if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
20730
+ console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
20731
+ return;
20732
+ }
20733
+ const msgSource = input?.source || "";
20734
+ if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
20735
+ console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
20736
+ return;
20737
+ }
20738
+ if (!lastMsg) {
20739
+ return;
20740
+ }
20741
+ let contextStr = "";
20742
+ try {
20743
+ const recent = recentMessages(sessions, 0.5, 6);
20744
+ contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
20745
+ console.log(`[stop-hook] ====== JUDGE INPUT ======`);
20746
+ console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
20747
+ recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
20748
+ console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
20749
+ console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
20750
+ } catch (e) {
20751
+ console.warn(`[stop-hook] recentMessages error: ${e.message}`);
20752
+ }
20753
+ const judgeInput = `${contextStr}
20754
+ \u4F60: ${lastMsg.slice(0, 300)}`;
20755
+ let isWaiting = false;
20756
+ let waitDesc = "";
20757
+ let pushedDecision = false;
20758
+ try {
20759
+ const excludeCases = this.cfg.stopHookExcludeCases || [];
20760
+ const excludeSection = excludeCases.length > 0 ? [
20761
+ "",
20762
+ "\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
20763
+ ...excludeCases.map((c, i) => `- ${c}`)
20764
+ ].join("\n") : "";
20765
+ const stream = this.provider.streamChat({
20766
+ model: this.model,
20767
+ systemPrompt: [
20768
+ "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
20769
+ "",
20770
+ "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20771
+ '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20772
+ "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20773
+ "",
20774
+ "\u5173\u952E\u533A\u5206\uFF1A",
20775
+ '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20776
+ '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20777
+ "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20778
+ excludeSection,
20779
+ "",
20780
+ "\u53EA\u56DE\u590D JSON\uFF1A",
20781
+ '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20782
+ '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20783
+ '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20784
+ ].join("\n"),
20785
+ messages: [{ role: "user", content: judgeInput }],
20786
+ maxTokens: 128,
20787
+ temperature: 0.1,
20788
+ disableThinking: true
20789
+ });
20790
+ let llmReply = "";
20791
+ for await (const chunk of stream) {
20792
+ if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
20793
+ if (chunk.type === "error") break;
20794
+ }
20795
+ console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
20796
+ const match = llmReply.match(/\{[\s\S]*\}/);
20797
+ if (match) {
20798
+ const parsed = JSON.parse(match[0]);
20799
+ isWaiting = !!parsed.waiting;
20800
+ waitDesc = parsed.desc || "";
20801
+ pushedDecision = !!parsed.pushedDecision;
20802
+ }
20803
+ console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20804
+ } catch (e) {
20805
+ console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20806
+ }
20807
+ if (pushedDecision && waitDesc) {
20808
+ console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
20809
+ try {
20810
+ const correctiveMsg = buildNudgeNotification("prompt", [
20811
+ "[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
20812
+ "",
20813
+ `\u8BCA\u65AD\uFF1A${waitDesc}`,
20814
+ "",
20815
+ "\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
20816
+ "\u95EE = \u5361\u4F4F\u3002",
20817
+ "",
20818
+ "\u8BF7\u7ACB\u523B\uFF1A",
20819
+ "1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
20820
+ "2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
20821
+ '3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
20822
+ "",
20823
+ "\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
20824
+ ].join("\n"));
20825
+ const route = this.getRoute(sessions);
20826
+ if (route) {
20827
+ enqueueNotification(correctiveMsg, route);
20787
20828
  }
20788
- const nudgeDir = path18.join(this.workspace, ".nudge");
20789
- const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
20790
- try {
20791
- if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
20792
- let notifs = [];
20793
- if (fs17.existsSync(notifPath)) {
20794
- notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
20795
- const now = Date.now();
20796
- const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
20797
- if (dup) {
20798
- dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
20799
- fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20800
- console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
20801
- return { outcome: { outcome: "success" } };
20802
- }
20803
- const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
20804
- if (recentReg) {
20805
- console.log(`[stop-hook] Skip (recent registration within 3min)`);
20806
- return { outcome: { outcome: "success" } };
20807
- }
20808
- }
20809
- const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
20810
- const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
20811
- notifs.push({
20812
- id: notifId,
20813
- description: waitDesc || lastMsg.slice(0, 200),
20814
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20815
- wakeAt
20816
- });
20829
+ } catch (e) {
20830
+ console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20831
+ }
20832
+ }
20833
+ if (!isWaiting) {
20834
+ return;
20835
+ }
20836
+ const nudgeDir = path18.join(this.workspace, ".nudge");
20837
+ const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
20838
+ try {
20839
+ if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
20840
+ let notifs = [];
20841
+ if (fs17.existsSync(notifPath)) {
20842
+ notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
20843
+ const now = Date.now();
20844
+ const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
20845
+ if (dup) {
20846
+ dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
20817
20847
  fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20818
- console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
20819
- } catch (e) {
20820
- console.warn(`[stop-hook] Failed to register: ${e.message}`);
20848
+ console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
20849
+ return;
20850
+ }
20851
+ const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
20852
+ if (recentReg) {
20853
+ console.log(`[stop-hook] Skip (recent registration within 3min)`);
20854
+ return;
20821
20855
  }
20822
- return { outcome: { outcome: "success" } };
20823
20856
  }
20824
- });
20825
- console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up)");
20857
+ const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
20858
+ const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
20859
+ notifs.push({
20860
+ id: notifId,
20861
+ description: waitDesc || lastMsg.slice(0, 200),
20862
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20863
+ wakeAt
20864
+ });
20865
+ fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
20866
+ console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
20867
+ } catch (e) {
20868
+ console.warn(`[stop-hook] Failed to register: ${e.message}`);
20869
+ }
20826
20870
  }
20827
20871
  stop() {
20828
20872
  if (this.timer) {
@@ -28140,9 +28184,11 @@ ${notifications}
28140
28184
  const modelChoices = [{ name: "\u{1F504} auto\uFF08\u6062\u590D\u9ED8\u8BA4\uFF09", value: "auto" }];
28141
28185
  if (config2.providers) {
28142
28186
  for (const [pid, pCfg] of Object.entries(config2.providers)) {
28187
+ if (modelChoices.length >= 25) break;
28143
28188
  const models = pCfg.models;
28144
28189
  if (!models) continue;
28145
28190
  for (const m of models) {
28191
+ if (modelChoices.length >= 25) break;
28146
28192
  const ref = `${pid}/${m.id}`;
28147
28193
  const alias = aliasByRef.get(ref);
28148
28194
  const label = alias ? `${alias} \u2014 ${m.name || m.id}` : `${m.name || m.id} (${pid})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engine7",
3
- "version": "7.1.31",
3
+ "version": "7.1.33",
4
4
  "type": "module",
5
5
  "description": "Engine 7 — Self-hosted AI agent engine",
6
6
  "main": "dist/main.mjs",