@code-yeongyu/senpi 2026.11.10-3 → 2026.11.11

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bundle/chunks/{chunk-Z2C5N2LM.js → chunk-5F2XC6UC.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-H3VJVKUK.js → chunk-5PIZWSNG.js} +2 -2
  4. package/dist/bundle/chunks/{chunk-DEQLHN2J.js → chunk-BPEA5SWW.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-DIHIEJFV.js → chunk-BUDJLVMF.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-47T6YMI2.js → chunk-IBDAV2AL.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-O5WJVIEU.js → chunk-J4ADZ2OK.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-T2SMQPRE.js → chunk-K3GAGIPX.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-HZDKQOWB.js → chunk-L6DF5G6K.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-E266ORGA.js → chunk-LJFQYCFH.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-DHAAWL2H.js → chunk-NRZAJUUO.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-R2Z7CX77.js → chunk-OSNR3IRD.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-AC6MDRAE.js → chunk-PUZDKFRT.js} +81 -81
  14. package/dist/bundle/chunks/{chunk-6Q7Y2G5A.js → chunk-SANWO37B.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-NN7NESW2.js → chunk-SS5VTBAV.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-JSD6O3RJ.js → chunk-XUDKT4UH.js} +1 -1
  17. package/dist/bundle/chunks/{cli-main-ZTT3N4DO.js → cli-main-7II7GCIR.js} +1 -1
  18. package/dist/bundle/chunks/{credential-accounts-5KHVGSCY.js → credential-accounts-N3B5YJS7.js} +1 -1
  19. package/dist/bundle/chunks/github-copilot.js +1 -1
  20. package/dist/bundle/chunks/{hand-entry-UV5XPMBF.js → hand-entry-ONOLRMVB.js} +1 -1
  21. package/dist/bundle/chunks/{host-command-BZN7WYYV.js → host-command-KWMJLPBS.js} +1 -1
  22. package/dist/bundle/chunks/{host-lifecycle-TNHASSFJ.js → host-lifecycle-4WQDCN3Y.js} +1 -1
  23. package/dist/bundle/chunks/host-lifecycle.js +4 -4
  24. package/dist/bundle/chunks/{host-session-command-O5WYHZXH.js → host-session-command-ZDYEKCUB.js} +1 -1
  25. package/dist/bundle/chunks/{interactive-mode-OCU4NDPC.js → interactive-mode-EPE4M32G.js} +1 -1
  26. package/dist/bundle/chunks/{main-CNDP6HCF.js → main-OM7BKVZG.js} +1 -1
  27. package/dist/bundle/chunks/{multi-session-host-ODGJAGJP.js → multi-session-host-DB6XMY53.js} +2 -2
  28. package/dist/bundle/chunks/{package-manager-cli-HSPUG4RJ.js → package-manager-cli-GLU5Q2KV.js} +1 -1
  29. package/dist/bundle/chunks/{rpc-mode-KOZPXU3U.js → rpc-mode-SDJVBT36.js} +1 -1
  30. package/dist/bundle/chunks/{schedule-command-SG44FE23.js → schedule-command-5SQD7M5N.js} +1 -1
  31. package/dist/bundle/chunks/{session-control-endpoint-5A24JYPE.js → session-control-endpoint-R25A2QGS.js} +1 -1
  32. package/dist/bundle/chunks/{session-picker-73YRS3GE.js → session-picker-GZM5CVQL.js} +1 -1
  33. package/dist/bundle/chunks/session-worker.js +88 -88
  34. package/dist/bundle/cli.js +2 -2
  35. package/dist/bundle/host-supervisor.js +4 -4
  36. package/dist/bundle/index.js +1 -1
  37. package/dist/bundle/rpc-entry.js +1 -1
  38. package/dist/bundle/runtime-manifest.json +1 -1
  39. package/dist/core/agent-session.d.ts +2 -0
  40. package/dist/core/credential-accounts.d.ts +2 -0
  41. package/dist/core/credential-accounts.js +10 -1
  42. package/dist/core/dynamic-prompt/intent-gate.js +2 -2
  43. package/dist/core/dynamic-prompt/style.js +2 -2
  44. package/dist/core/dynamic-prompt/verification.js +2 -2
  45. package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
  46. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-account-test.js +3 -1
  47. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-connection-test.d.ts +3 -1
  48. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-connection-test.js +47 -16
  49. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-login-slots.d.ts +1 -1
  50. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-login-slots.js +4 -2
  51. package/dist/core/extensions/builtin/anthropic-subscription/errors.d.ts +1 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/errors.js +14 -5
  53. package/dist/core/extensions/builtin/anthropic-subscription/oauth-login.js +3 -3
  54. package/dist/core/extensions/builtin/goal/continuation.d.ts +6 -0
  55. package/dist/core/extensions/builtin/goal/continuation.js +3 -2
  56. package/dist/core/extensions/builtin/goal/index.js +10 -26
  57. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +0 -6
  58. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +3 -37
  59. package/dist/core/extensions/builtin/goal/monitor-continuation.js +66 -7
  60. package/dist/core/extensions/builtin/goal/provider-retry.d.ts +9 -0
  61. package/dist/core/extensions/builtin/goal/provider-retry.js +16 -0
  62. package/dist/core/extensions/builtin/goal/reload-reengagement.d.ts +3 -2
  63. package/dist/core/extensions/builtin/goal/reload-reengagement.js +4 -4
  64. package/dist/core/extensions/builtin/goal/session-start-continuation.d.ts +28 -0
  65. package/dist/core/extensions/builtin/goal/session-start-continuation.js +75 -0
  66. package/dist/core/extensions/builtin/goal/store.d.ts +1 -0
  67. package/dist/core/extensions/builtin/goal/store.js +1 -1
  68. package/dist/core/extensions/builtin/handoff-hint/index.js +15 -11
  69. package/dist/core/extensions/builtin/handoff-hint/replacement-relay.d.ts +16 -0
  70. package/dist/core/extensions/builtin/handoff-hint/replacement-relay.js +17 -0
  71. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +3 -3
  72. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +5 -4
  73. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +3 -3
  74. package/dist/core/extensions/builtin/prompt-preset/claude-haiku-5-5.js +3 -3
  75. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +3 -3
  76. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
  77. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +3 -3
  78. package/dist/core/extensions/builtin/prompt-preset/deepseek-v4-1-flash.js +3 -3
  79. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
  80. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +2 -2
  81. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +3 -3
  82. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +1 -1
  83. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
  84. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +5 -5
  85. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +4 -4
  86. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +7 -6
  87. package/dist/core/resource-loader.js +19 -3
  88. package/dist/core/retry-fallback/controller-types.d.ts +2 -0
  89. package/dist/core/retry-fallback/controller.js +9 -2
  90. package/dist/core/retry-fallback/cooldown.d.ts +7 -0
  91. package/dist/core/retry-fallback/cooldown.js +13 -1
  92. package/dist/core/retry-fallback/credential-recovery.d.ts +7 -0
  93. package/dist/core/retry-fallback/credential-recovery.js +24 -0
  94. package/dist/core/retry-fallback/settings.d.ts +8 -5
  95. package/dist/core/retry-fallback/settings.js +9 -6
  96. package/dist/modes/rpc/delegation-declaration.js +1 -1
  97. package/dist/modes/rpc/host-gc-evidence.d.ts +1 -1
  98. package/dist/modes/rpc/host-gc-evidence.js +9 -3
  99. package/dist/modes/rpc/host-gc.js +4 -1
  100. package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -0
  101. package/dist/modes/rpc/session-attachment-gate.js +3 -0
  102. package/dist/private-fd-startup-fillers.d.ts +2 -2
  103. package/dist/private-fd-startup-fillers.js +34 -4
  104. package/docs/rpc.md +2 -1
  105. package/docs/settings.md +1 -1
  106. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  107. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  108. package/package.json +6 -6
@@ -6,7 +6,6 @@ import { isRecord } from "../../../work-custody-record.js";
6
6
  import { getLatestPhasesFromBranchEntries } from "../todotools/state.js";
7
7
  import { buildGoalContinuationSignature, evaluateGoalContinuation, hashAssistantText, } from "./continuation.js";
8
8
  import { CONTEXT_OVERFLOW_BLOCKED_REASON, CONTINUATION_CAP_BLOCKED_REASON, continuationCapRecoveryHint, LENGTH_EXHAUSTED_BLOCKED_REASON, NO_PROGRESS_BLOCKED_REASON, REPETITION_BLOCKED_REASON, } from "./continuation-recovery.js";
9
- import { buildContinuationPrompt } from "./prompt.js";
10
9
  import { recordContinuationDelivered, recordGoalContinuationStopped } from "./store.js";
11
10
  import { goalStoreRef } from "./store-ref.js";
12
11
  import { openTodoTaskContents } from "./todo-gate.js";
@@ -42,36 +41,15 @@ export async function admitAndQueueGoalContinuation(pi, ctx, goal, options) {
42
41
  if (options.input.currentSignature === undefined) {
43
42
  throw new Error("Cannot queue a goal continuation without a progress signature");
44
43
  }
45
- const recordedGoal = await recordContinuationDelivered(goalStoreRef(ctx.sessionManager, ctx.cwd), options.input.currentSignature, goal.id, { countUnattended: options.input.path !== "monitorDelayed" });
44
+ const recordedGoal = await recordContinuationDelivered(goalStoreRef(ctx.sessionManager, ctx.cwd), options.input.currentSignature, goal.id, options.input.limitWait === true
45
+ ? { countUnattended: false, countStreak: false }
46
+ : { countUnattended: options.input.path !== "monitorDelayed" });
46
47
  if (recordedGoal === null)
47
48
  return null;
48
49
  options.markContinuationPending();
49
50
  queueHiddenGoalPrompt(pi, options.content(verdict));
50
51
  return recordedGoal;
51
52
  }
52
- /** Routes startup and resume continuations through the same verdict and delivery accounting as agent-end paths. */
53
- export async function queueGoalContinuation(pi, ctx, goal, options) {
54
- const signature = buildCurrentGoalContinuationSignature(ctx, goal, lastAssistantTextFromEntries(ctx.sessionManager.getBranch()));
55
- return admitAndQueueGoalContinuation(pi, ctx, goal, {
56
- input: {
57
- isIdle: ctx.isIdle(),
58
- hasPendingMessages: ctx.hasPendingMessages(),
59
- path: "sessionStart",
60
- lastStopReason: undefined,
61
- lastTurnWasMalformedToolUse: false,
62
- consecutiveContinuations: goal.consecutiveContinuations ?? 0,
63
- lastContinuationSignature: goal.lastContinuationSignature,
64
- currentSignature: signature,
65
- consecutiveLengthRecoveries: 0,
66
- recentNormalizedOutputHashes: [],
67
- toollessContinuationStreak: 0,
68
- continuationPending: options.continuationPending,
69
- lastTurnStuckOnContextOverflow: isLastTurnStuckOnContextOverflow(ctx, lastAssistantFromEntries(ctx.sessionManager.getBranch())),
70
- },
71
- content: () => buildContinuationPrompt(goal, { modelId: ctx.model?.id }),
72
- markContinuationPending: options.markContinuationPending,
73
- });
74
- }
75
53
  export function buildCurrentGoalContinuationSignature(ctx, goal, lastAssistantText) {
76
54
  const entries = ctx.sessionManager.getBranch();
77
55
  const openTodos = openTodoTaskContents(entries).length;
@@ -86,18 +64,6 @@ export function lastAssistantText(messages) {
86
64
  }
87
65
  return "";
88
66
  }
89
- function lastAssistantTextFromEntries(entries) {
90
- const assistant = lastAssistantFromEntries(entries);
91
- return assistant === undefined ? "" : textContent(assistant);
92
- }
93
- function lastAssistantFromEntries(entries) {
94
- for (let index = entries.length - 1; index >= 0; index--) {
95
- const entry = entries[index];
96
- if (entry?.type === "message" && entry.message.role === "assistant")
97
- return entry.message;
98
- }
99
- return undefined;
100
- }
101
67
  export function isLastTurnStuckOnContextOverflow(ctx, lastAssistant) {
102
68
  return lastAssistant !== undefined && isTurnStuckOnContextOverflow(lastAssistant, ctx.model?.contextWindow ?? 0);
103
69
  }
@@ -1,4 +1,5 @@
1
1
  import { deadlineStopPendingSinceLastTurn } from "../../../turn-deadline.js";
2
+ import { subscribeProviderAccountEvents } from "../anthropic-subscription/account-events.js";
2
3
  import { isHumanInputMonitorNotification } from "../monitor-state-event.js";
3
4
  import { createGoalCacheWarmScheduleData, estimateCacheWarmMetrics, GOAL_CACHE_WARMUP_ENTRY_TYPE, GOAL_MONITOR_CONTINUATION_FALLBACK_DELAY_MS, resolveGoalBackstopMaxSecondsForCache, resolveGoalMonitorContinuationDelayMs, resolveGoalPromptCacheLifetime, } from "./cache-warm.js";
4
5
  import { subscribeGoalChannelState } from "./channel-state-subscriptions.js";
@@ -9,7 +10,7 @@ import { lastAssistantMessage } from "./last-assistant-message.js";
9
10
  import { admitAndQueueGoalContinuation, buildCurrentGoalContinuationSignature, isLastTurnStuckOnContextOverflow, lastAssistantText, } from "./lifecycle-helpers.js";
10
11
  import { findParkedGoalWait } from "./parked-wait.js";
11
12
  import { buildContinuationPrompt, buildGoalStallNotice, buildTruncationRecoveryPrompt } from "./prompt.js";
12
- import { exhaustedLadderResumeDelayMs, PROVIDER_RETRY_BASE_MS, providerRetryAt, providerRetryDelayMs, } from "./provider-retry.js";
13
+ import { exhaustedLadderResumeDelayMs, GOAL_LIMIT_WAIT_WAKE_LIMIT, isLimitRefusal, limitWaitFloorMs, PROVIDER_RETRY_BASE_MS, providerRetryAt, providerRetryDelayMs, } from "./provider-retry.js";
13
14
  import { isStaleExtensionContextError } from "./stale-context.js";
14
15
  import { resetContinuationStreak } from "./store.js";
15
16
  import { goalStoreRef } from "./store-ref.js";
@@ -22,6 +23,8 @@ export const GOAL_CONTINUATION_SCHEDULED_EVENT = "goal_continuation_scheduled";
22
23
  export const GOAL_CONTINUATION_RESUMED_EVENT = "goal_continuation_resumed";
23
24
  export const GOAL_CONTINUATION_TIMER_STATE_EVENT = "goal_continuation_timer_state";
24
25
  export const GOAL_MONITOR_STALL_EVENT = "goal_monitor_continuation_stall";
26
+ /** A limit outlasted GOAL_LIMIT_WAIT_WAKE_LIMIT re-checks: the goal waits for a sign-in, a block reset or input. */
27
+ const GOAL_LIMIT_WAIT_PARKED_EVENT = "goal_limit_wait_parked";
25
28
  /** The one notice a provider-limit park shows: when the goal resumes, in local time and as the exact instant. */
26
29
  function providerPauseNotice(retryAt) {
27
30
  const local = new Date(retryAt).toLocaleString("en-US", {
@@ -68,12 +71,51 @@ export class MonitorAwareGoalContinuation {
68
71
  #pendingProviderRecovery;
69
72
  #providerRetryAttempts = 0;
70
73
  #providerRetryDelayMs;
74
+ /** The armed provider retry waits out a usage or rate limit, so its wake spends no continuation budget (senpi#4085). */
75
+ #limitWait = false;
76
+ #limitWakes = 0;
77
+ #limitParked = false;
71
78
  constructor(pi, isContinuationPending = () => false, markContinuationPending = () => { }, waitTicker) {
72
79
  this.#pi = pi;
73
80
  this.#isContinuationPending = isContinuationPending;
74
81
  this.#markContinuationPending = markContinuationPending;
75
82
  this.#waitTicker = waitTicker;
76
83
  this.#subscribeToChannelState();
84
+ // A login or an explicit block reset can end the wait early: re-check the provider now (senpi#4085).
85
+ this.#channelStateUnsubscribers.push(subscribeProviderAccountEvents((event) => {
86
+ if (event.type === "accounts_changed" && event.remedy !== undefined)
87
+ this.#wakeLimitWait();
88
+ }));
89
+ }
90
+ #wakeLimitWait() {
91
+ const goal = this.#goal;
92
+ const waiting = this.#scheduledContinuationKind === "providerRetry" || this.#limitParked;
93
+ if (!this.#limitWait || !waiting || goal === null)
94
+ return;
95
+ this.#limitWakes = 0;
96
+ this.#limitParked = false;
97
+ this.#cancelTimer();
98
+ this.#providerRetryDelayMs = 0;
99
+ this.#schedule(goal, "providerRetry");
100
+ }
101
+ /**
102
+ * Arms the next re-check of a limit wait, never sooner than the provider backoff. A limit that outlasts
103
+ * GOAL_LIMIT_WAIT_WAKE_LIMIT re-checks parks the goal, still active, until a sign-in, a block reset or input.
104
+ */
105
+ #armLimitWait(goal, delayMs) {
106
+ this.#limitWait = true;
107
+ this.#limitWakes += 1;
108
+ if (this.#limitWakes > GOAL_LIMIT_WAIT_WAKE_LIMIT) {
109
+ this.#limitParked = true;
110
+ this.#publish(GOAL_LIMIT_WAIT_PARKED_EVENT, { goalId: goal.id, wakes: GOAL_LIMIT_WAIT_WAKE_LIMIT });
111
+ if (this.#ctxHasUI()) {
112
+ this.#ctx?.ui.notify(`Goal paused: the provider kept refusing for a usage limit through ${GOAL_LIMIT_WAIT_WAKE_LIMIT} re-checks. It resumes after a sign-in, an account block reset, or your next message.`, "warning");
113
+ }
114
+ return false;
115
+ }
116
+ this.#providerRetryDelayMs = Math.max(delayMs, limitWaitFloorMs(this.#providerRetryAttempts));
117
+ this.#schedule(goal, "providerRetry");
118
+ return true;
77
119
  }
78
120
  start(ctx) {
79
121
  this.#cancelTimer();
@@ -91,6 +133,9 @@ export class MonitorAwareGoalContinuation {
91
133
  if (options.goal?.id !== this.#goal?.id)
92
134
  this.#resetContinuationState();
93
135
  this.#providerRetryAttempts = 0;
136
+ this.#limitWait = false;
137
+ this.#limitWakes = 0;
138
+ this.#limitParked = false;
94
139
  this.#ctx = options.ctx;
95
140
  this.#goal = options.goal;
96
141
  this.#lastAgentEndMessages = options.messages;
@@ -214,9 +259,18 @@ export class MonitorAwareGoalContinuation {
214
259
  return undefined;
215
260
  }
216
261
  this.#providerRetryAttempts += 1;
262
+ this.#limitWait = isLimitRefusal(pendingProvider.messages);
217
263
  const now = Date.now();
218
264
  const retryAt = providerRetryAt(pendingProvider.messages, now);
219
265
  const delayMs = providerRetryDelayMs(this.#providerRetryAttempts, retryAt, now);
266
+ if (this.#limitWait) {
267
+ this.#cancelTimer();
268
+ const armed = this.#armLimitWait(pendingProvider.goal, delayMs);
269
+ if (armed && retryAt !== undefined && this.#ctxHasUI(pendingProvider.ctx)) {
270
+ pendingProvider.ctx.ui.notify(providerPauseNotice(retryAt), "info");
271
+ }
272
+ return pendingProvider.goal;
273
+ }
220
274
  if (delayMs > 0) {
221
275
  this.#cancelTimer();
222
276
  this.#providerRetryDelayMs = delayMs;
@@ -230,17 +284,16 @@ export class MonitorAwareGoalContinuation {
230
284
  }
231
285
  /**
232
286
  * senpi#3709: every fallback rung failed, so a continuation now would hit the same limits. Park on
233
- * the soonest reset the run named and resume once; with no known reset, stay parked until input.
287
+ * the soonest reset the run named; with no known reset, re-check on the provider backoff (senpi#4085).
234
288
  */
235
289
  #parkUntilLadderReset(resumeAt) {
236
290
  const goal = this.#goal;
237
291
  if (goal?.status !== "active")
238
292
  return undefined;
239
293
  this.#cancelTimer();
240
- if (resumeAt === undefined)
241
- return goal;
242
- this.#providerRetryDelayMs = exhaustedLadderResumeDelayMs(resumeAt, Date.now());
243
- this.#schedule(goal, "providerRetry");
294
+ this.#providerRetryAttempts += 1;
295
+ // No failure named a reset: re-check on the provider backoff (15 s doubling to 30 min) rather than wait for input.
296
+ this.#armLimitWait(goal, resumeAt === undefined ? 0 : exhaustedLadderResumeDelayMs(resumeAt, Date.now()));
244
297
  return goal;
245
298
  }
246
299
  syncGoal(goal) {
@@ -521,7 +574,10 @@ export class MonitorAwareGoalContinuation {
521
574
  this.#resetCacheWarmIteration();
522
575
  }
523
576
  async #admitAndQueue(ctx, goal, path, messages) {
524
- const input = this.#buildVerdictInput(ctx, goal, path, messages);
577
+ const input = {
578
+ ...this.#buildVerdictInput(ctx, goal, path, messages),
579
+ ...(path === "providerRecovery" && this.#limitWait ? { limitWait: true } : {}),
580
+ };
525
581
  const verdict = evaluateGoalContinuation({ goal, ...input });
526
582
  const admittedGoal = await admitAndQueueGoalContinuation(this.#pi, ctx, goal, {
527
583
  input,
@@ -747,6 +803,9 @@ export class MonitorAwareGoalContinuation {
747
803
  this.#pendingSystemRecovery = undefined;
748
804
  this.#pendingProviderRecovery = undefined;
749
805
  this.#providerRetryAttempts = 0;
806
+ this.#limitWait = false;
807
+ this.#limitWakes = 0;
808
+ this.#limitParked = false;
750
809
  this.#consecutiveLengthRecoveries.clear();
751
810
  this.#recentNormalizedOutputHashes = [];
752
811
  this.#recentToolEffects = [];
@@ -7,6 +7,15 @@ export declare const PROVIDER_RETRY_WAKE_CAP_MS: number;
7
7
  * pool guidance falls back to the earliest future ISO instant in the error only.
8
8
  */
9
9
  export declare function providerRetryAt(messages: readonly AgentMessage[], now: number): number | undefined;
10
+ /** Re-checks of a limit that never clears before the goal parks until a sign-in, a block reset or input (senpi#4085). */
11
+ export declare const GOAL_LIMIT_WAIT_WAKE_LIMIT = 48;
12
+ /** The shortest wait before the `attempt`-th limit re-check: the provider backoff, 15 s doubling to 30 min. */
13
+ export declare function limitWaitFloorMs(attempt: number): number;
14
+ /**
15
+ * The turn ended on a provider refusing for a usage or rate limit, not on an outage (senpi#4085). An OpenAI-compatible
16
+ * adapter reports a bare 429 as a status-only diagnostic (`unknown` + 429); that status is still a limit.
17
+ */
18
+ export declare function isLimitRefusal(messages: readonly AgentMessage[]): boolean;
10
19
  /**
11
20
  * How long to wait before the `attempt`-th consecutive provider-recovery
12
21
  * continuation (senpi#3264). A stated unblock time is the wake; otherwise the
@@ -31,6 +31,22 @@ export function providerRetryAt(messages, now) {
31
31
  .filter((at) => Number.isFinite(at) && at > now);
32
32
  return instants.length === 0 ? undefined : Math.min(...instants);
33
33
  }
34
+ /** Re-checks of a limit that never clears before the goal parks until a sign-in, a block reset or input (senpi#4085). */
35
+ export const GOAL_LIMIT_WAIT_WAKE_LIMIT = 48;
36
+ /** The shortest wait before the `attempt`-th limit re-check: the provider backoff, 15 s doubling to 30 min. */
37
+ export function limitWaitFloorMs(attempt) {
38
+ return Math.min(PROVIDER_RETRY_CAP_MS, PROVIDER_RETRY_BASE_MS * 2 ** (Math.max(1, attempt) - 1));
39
+ }
40
+ /**
41
+ * The turn ended on a provider refusing for a usage or rate limit, not on an outage (senpi#4085). An OpenAI-compatible
42
+ * adapter reports a bare 429 as a status-only diagnostic (`unknown` + 429); that status is still a limit.
43
+ */
44
+ export function isLimitRefusal(messages) {
45
+ const diagnostic = lastAssistantMessage(messages)?.providerDiagnostic;
46
+ return (diagnostic?.category === "rate_limit" ||
47
+ diagnostic?.category === "quota" ||
48
+ (diagnostic?.category === "unknown" && diagnostic.httpStatus === 429));
49
+ }
34
50
  /**
35
51
  * How long to wait before the `attempt`-th consecutive provider-recovery
36
52
  * continuation (senpi#3264). A stated unblock time is the wake; otherwise the
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "../../types.ts";
2
2
  import type { MonitorAwareGoalContinuation } from "./monitor-continuation.ts";
3
+ import type { TrailingGoalContinuations } from "./session-start-continuation.ts";
3
4
  import type { Goal } from "./types.ts";
4
5
  export type ReloadReengagementOutcome = "skipped-inactive" | "backstop-rearmed" | "suppressed-flood" | "continuation-queued";
5
6
  /**
@@ -24,7 +25,7 @@ export declare function reengageGoalAfterReload(options: {
24
25
  readonly ctx: ExtensionContext;
25
26
  readonly goal: Goal;
26
27
  readonly monitor: MonitorAwareGoalContinuation;
27
- readonly countTrailingContinuations: () => number;
28
- readonly queueContinuation: () => Promise<void>;
28
+ readonly readTrailingContinuations: () => TrailingGoalContinuations;
29
+ readonly queueContinuation: (limitWait: boolean) => Promise<void>;
29
30
  }): Promise<ReloadReengagementOutcome>;
30
31
  //# sourceMappingURL=reload-reengagement.d.ts.map
@@ -30,12 +30,12 @@ export async function reengageGoalAfterReload(options) {
30
30
  monitor.rearmMonitorBackstop(goal);
31
31
  return "backstop-rearmed";
32
32
  }
33
- const trailingContinuations = options.countTrailingContinuations();
34
- if (trailingContinuations >= GOAL_CONTINUATION_CAP) {
35
- ctx.ui.notify(`Goal auto-continuation suppressed for this resumed session (${trailingContinuations} historical continuations). Send a message to resume.`, "info");
33
+ const trailing = options.readTrailingContinuations();
34
+ if (trailing.counted >= GOAL_CONTINUATION_CAP) {
35
+ ctx.ui.notify(`Goal auto-continuation suppressed for this resumed session (${trailing.counted} historical continuations). Send a message to resume.`, "info");
36
36
  return "suppressed-flood";
37
37
  }
38
- await options.queueContinuation();
38
+ await options.queueContinuation(trailing.waitingOutLimit);
39
39
  return "continuation-queued";
40
40
  }
41
41
  //# sourceMappingURL=reload-reengagement.js.map
@@ -0,0 +1,28 @@
1
+ import type { SessionEntry } from "../../../session-manager.ts";
2
+ import type { ExtensionAPI, ExtensionContext } from "../../types.ts";
3
+ import type { Goal } from "./types.ts";
4
+ export type TrailingGoalContinuations = {
5
+ /** Continuations whose turn ran: the unattended loop the load-time flood guard bounds. */
6
+ readonly counted: number;
7
+ /** The newest continuation was refused for a usage or rate limit, so the goal was waiting that limit out. */
8
+ readonly waitingOutLimit: boolean;
9
+ };
10
+ export type SessionStartContinuationOptions = {
11
+ readonly continuationPending: boolean;
12
+ readonly markContinuationPending: () => void;
13
+ /** The session opened in the middle of a limit wait, so this wake re-checks that limit (senpi#4085). */
14
+ readonly limitWait?: boolean;
15
+ };
16
+ /**
17
+ * Reads the goal continuations queued since the most recent real user message in the branch (mirrors the
18
+ * todo-gate / todo-bridge backward branch read). A real user message is the only reset: the assistant turns in
19
+ * between are exactly the unattended loop the load-time admission guard exists to stop.
20
+ *
21
+ * A continuation the provider refused for a usage or rate limit ran no model turn, so it is not part of that
22
+ * loop (senpi#4085). A goal that waited a limit out for more wakes than the cap is therefore not a flood, and it
23
+ * resumes after a restart or a config reload.
24
+ */
25
+ export declare function readTrailingGoalContinuations(entries: readonly SessionEntry[]): TrailingGoalContinuations;
26
+ /** Routes startup and resume continuations through the same verdict and delivery accounting as agent-end paths. */
27
+ export declare function queueGoalContinuation(pi: ExtensionAPI, ctx: ExtensionContext, goal: Goal, options: SessionStartContinuationOptions): Promise<Goal | null>;
28
+ //# sourceMappingURL=session-start-continuation.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { GOAL_CONTINUATION_MESSAGE_TYPE } from "../../../messages.js";
2
+ import { admitAndQueueGoalContinuation, buildCurrentGoalContinuationSignature, isLastTurnStuckOnContextOverflow, lastAssistantText, } from "./lifecycle-helpers.js";
3
+ import { buildContinuationPrompt } from "./prompt.js";
4
+ import { isLimitRefusal } from "./provider-retry.js";
5
+ /**
6
+ * Reads the goal continuations queued since the most recent real user message in the branch (mirrors the
7
+ * todo-gate / todo-bridge backward branch read). A real user message is the only reset: the assistant turns in
8
+ * between are exactly the unattended loop the load-time admission guard exists to stop.
9
+ *
10
+ * A continuation the provider refused for a usage or rate limit ran no model turn, so it is not part of that
11
+ * loop (senpi#4085). A goal that waited a limit out for more wakes than the cap is therefore not a flood, and it
12
+ * resumes after a restart or a config reload.
13
+ */
14
+ export function readTrailingGoalContinuations(entries) {
15
+ let counted = 0;
16
+ let waitingOutLimit;
17
+ let turn = [];
18
+ for (let index = entries.length - 1; index >= 0; index--) {
19
+ const entry = entries[index];
20
+ if (entry?.type === "message") {
21
+ if (entry.message.role === "user")
22
+ break;
23
+ if (entry.message.role === "assistant")
24
+ turn.push(entry.message);
25
+ continue;
26
+ }
27
+ if (entry?.type !== "custom_message" || entry.customType !== GOAL_CONTINUATION_MESSAGE_TYPE)
28
+ continue;
29
+ const refused = wasRefusedForLimit(turn);
30
+ waitingOutLimit ??= refused;
31
+ if (!refused)
32
+ counted += 1;
33
+ turn = [];
34
+ }
35
+ return { counted, waitingOutLimit: waitingOutLimit ?? false };
36
+ }
37
+ function wasRefusedForLimit(turn) {
38
+ return (turn.length > 0 &&
39
+ turn.every((message) => message.stopReason === "error") &&
40
+ turn.some((message) => isLimitRefusal([message])));
41
+ }
42
+ /** Routes startup and resume continuations through the same verdict and delivery accounting as agent-end paths. */
43
+ export async function queueGoalContinuation(pi, ctx, goal, options) {
44
+ const lastAssistant = lastAssistantFromEntries(ctx.sessionManager.getBranch());
45
+ const signature = buildCurrentGoalContinuationSignature(ctx, goal, lastAssistant === undefined ? "" : lastAssistantText([lastAssistant]));
46
+ return admitAndQueueGoalContinuation(pi, ctx, goal, {
47
+ input: {
48
+ isIdle: ctx.isIdle(),
49
+ hasPendingMessages: ctx.hasPendingMessages(),
50
+ path: "sessionStart",
51
+ lastStopReason: undefined,
52
+ lastTurnWasMalformedToolUse: false,
53
+ consecutiveContinuations: goal.consecutiveContinuations ?? 0,
54
+ lastContinuationSignature: goal.lastContinuationSignature,
55
+ currentSignature: signature,
56
+ consecutiveLengthRecoveries: 0,
57
+ recentNormalizedOutputHashes: [],
58
+ toollessContinuationStreak: 0,
59
+ continuationPending: options.continuationPending,
60
+ lastTurnStuckOnContextOverflow: isLastTurnStuckOnContextOverflow(ctx, lastAssistant),
61
+ ...(options.limitWait === true ? { limitWait: true } : {}),
62
+ },
63
+ content: () => buildContinuationPrompt(goal, { modelId: ctx.model?.id }),
64
+ markContinuationPending: options.markContinuationPending,
65
+ });
66
+ }
67
+ function lastAssistantFromEntries(entries) {
68
+ for (let index = entries.length - 1; index >= 0; index--) {
69
+ const entry = entries[index];
70
+ if (entry?.type === "message" && entry.message.role === "assistant")
71
+ return entry.message;
72
+ }
73
+ return undefined;
74
+ }
75
+ //# sourceMappingURL=session-start-continuation.js.map
@@ -15,6 +15,7 @@ export declare function clearGoal(ref: GoalStoreRef): Promise<boolean>;
15
15
  export declare function accountGoalUsage(ref: GoalStoreRef, usage: TokenUsageSnapshot, elapsedSeconds: number, mode?: GoalAccountingMode, expectedGoalId?: string): Promise<Goal | null>;
16
16
  export declare function recordContinuationDelivered(ref: GoalStoreRef, signature: string, expectedGoalId?: string, options?: {
17
17
  countUnattended?: boolean;
18
+ countStreak?: boolean;
18
19
  }): Promise<Goal | null>;
19
20
  export declare function resetContinuationStreak(ref: GoalStoreRef, options?: {
20
21
  unattended?: boolean;
@@ -177,7 +177,7 @@ export async function recordContinuationDelivered(ref, signature, expectedGoalId
177
177
  const counted = options.countUnattended === false ? 0 : 1;
178
178
  const next = {
179
179
  ...goal,
180
- consecutiveContinuations: (goal.consecutiveContinuations ?? 0) + 1,
180
+ consecutiveContinuations: (goal.consecutiveContinuations ?? 0) + (options.countStreak === false ? 0 : 1),
181
181
  unattendedContinuations: (goal.unattendedContinuations ?? 0) + counted,
182
182
  noProgressContinuations: (goal.noProgressContinuations ?? 0) + counted,
183
183
  lastContinuationSignature: signature,
@@ -5,6 +5,7 @@ import { readGoal } from "../goal/store.js";
5
5
  import { goalStoreRef } from "../goal/store-ref.js";
6
6
  import { buildHandoffBrief } from "./brief.js";
7
7
  import { estimateHandoff, hintText } from "./estimate.js";
8
+ import { createReplacementRelay } from "./replacement-relay.js";
8
9
  export const HANDOFF_HINT_ENTRY_TYPE = "handoff-hint";
9
10
  export const HANDOFF_BRIEF_MESSAGE_TYPE = "handoff-brief";
10
11
  export const HANDOFF_HINT_EVENT = "handoff_hint";
@@ -51,6 +52,7 @@ const readActiveGoal = (ctx) => readGoal(goalStoreRef(ctx.sessionManager, ctx.cw
51
52
  export function createHandoffHintExtension(dependencies = {}) {
52
53
  const now = dependencies.now ?? Date.now;
53
54
  const goalOf = dependencies.readGoal ?? readActiveGoal;
55
+ const acceptances = createReplacementRelay();
54
56
  return (pi) => {
55
57
  let ctx;
56
58
  let timer;
@@ -168,22 +170,21 @@ export function createHandoffHintExtension(dependencies = {}) {
168
170
  previousSessionFile: current.sessionManager.getSessionFile(),
169
171
  contextTokens: usage?.tokens ?? estimate?.contextTokens ?? 0,
170
172
  });
171
- const accepted = estimate === undefined ? undefined : hintEvent("accepted", current, estimate);
172
173
  const sessionId = current.sessionManager.getSessionId();
174
+ const previousSessionFile = current.sessionManager.getSessionFile();
175
+ // The replacement's session_start emits it through the new generation's pi (senpi#4051).
176
+ if (estimate !== undefined)
177
+ acceptances.hold(previousSessionFile, hintEvent("accepted", current, estimate));
173
178
  hide(current);
174
- const result = await current.newSession({
175
- parentSession: current.sessionManager.getSessionFile(),
179
+ const switching = current.newSession({
180
+ parentSession: previousSessionFile,
176
181
  withSession: async (next) => {
177
182
  await next.sendMessage({ customType: HANDOFF_BRIEF_MESSAGE_TYPE, content: brief, display: true }, { triggerTurn: false });
178
183
  },
179
184
  });
180
- if (result.cancelled) {
181
- if (estimate !== undefined && current.sessionManager.getSessionId() === sessionId)
182
- render(current, estimate);
183
- return;
184
- }
185
- if (accepted !== undefined)
186
- pi.events?.emit(HANDOFF_HINT_EVENT, accepted);
185
+ const result = await switching.finally(() => acceptances.release(previousSessionFile));
186
+ if (result.cancelled && estimate !== undefined && current.sessionManager.getSessionId() === sessionId)
187
+ render(current, estimate);
187
188
  }
188
189
  pi.registerCommand(HANDOFF_COMMAND, {
189
190
  description: "Start a fresh session seeded with a short handoff brief (`/handoff keep` hides the hint)",
@@ -207,8 +208,11 @@ export function createHandoffHintExtension(dependencies = {}) {
207
208
  await startFresh(current);
208
209
  },
209
210
  });
210
- pi.on("session_start", (_event, next) => {
211
+ pi.on("session_start", (event, next) => {
211
212
  ctx = next;
213
+ const accepted = event.reason === "new" ? acceptances.take(event.previousSessionFile) : undefined;
214
+ if (accepted !== undefined)
215
+ pi.events?.emit(HANDOFF_HINT_EVENT, accepted);
212
216
  disarm();
213
217
  visible = undefined;
214
218
  shownThisCrossing = false;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Carries a value from a session to the session that replaces it (senpi#4051).
3
+ *
4
+ * `newSession()` retires the old extension generation before the replacement starts, so a captured `pi` cannot emit
5
+ * after the switch, and emitting before it would report a switch that `session_before_switch` may still cancel. The
6
+ * builtin factory outlives both generations in one process, so the value waits here, keyed by the session being
7
+ * replaced, until the replacement's `session_start` takes it through its own live `pi`. The holder releases its key
8
+ * once `newSession()` settles, so a cancelled or failed switch delivers nothing.
9
+ */
10
+ export interface ReplacementRelay<T> {
11
+ hold(previousSessionFile: string | undefined, value: T): void;
12
+ take(previousSessionFile: string | undefined): T | undefined;
13
+ release(previousSessionFile: string | undefined): void;
14
+ }
15
+ export declare function createReplacementRelay<T>(): ReplacementRelay<T>;
16
+ //# sourceMappingURL=replacement-relay.d.ts.map
@@ -0,0 +1,17 @@
1
+ export function createReplacementRelay() {
2
+ const pending = new Map();
3
+ return {
4
+ hold: (previousSessionFile, value) => {
5
+ pending.set(previousSessionFile, value);
6
+ },
7
+ take: (previousSessionFile) => {
8
+ const value = pending.get(previousSessionFile);
9
+ pending.delete(previousSessionFile);
10
+ return value;
11
+ },
12
+ release: (previousSessionFile) => {
13
+ pending.delete(previousSessionFile);
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=replacement-relay.js.map
@@ -85,7 +85,7 @@ ${buildSearchLine(context)}
85
85
  Route by true intent, not surface form:
86
86
  - Information asks (explain, look into, investigate): read the code and report; no edits.
87
87
  - Judgment asks (what do you think, review) and open-ended changes (refactor, improve, clean up): assess and propose, then wait for confirmation.
88
- - Change asks (implement, add, fix this error): build, or diagnose and fix minimally.
88
+ - Change asks (implement, add, fix this error): build, or diagnose and fix at the cause.
89
89
 
90
90
  Derive intent from the latest user turn alone: a new direction drops the stale plan, and queued steering messages outrank earlier intent.
91
91
 
@@ -93,7 +93,7 @@ Derive intent from the latest user turn alone: a new direction drops the stale p
93
93
 
94
94
  The request sets the scope, and the scope is the deliverable: deliver all of it and only it. Make routine judgment calls yourself; ask only when different readings would lead to materially different work, and ask after doing everything that does not depend on the answer, through ask_user_question when it is available (waitForAnswer true when the next step depends on the answer). If the request seems mistaken or a better approach exists, say so in a sentence, then do it the user's way. If part of the task is blocked, finish every other part and say exactly what you left out and why.
95
95
 
96
- Smallest correct change wins: no refactors beside a focused fix, no helpers or abstractions for hypothetical needs, no defensive checks inside trusted code; validate only at system boundaries. A pre-existing bug or performance concern you notice is a follow-up for your summary, not a change in this diff. Scratch checks verify and get discarded; commit tests only where the task asks for them or the repository already keeps tests for that kind of change, sized like the neighboring test files. Prefer a surgical edit over rewriting a file when the result would be identical.
96
+ This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it. Change it the way its owner would: build on the code that already does the job instead of adding a parallel copy, fit new behavior into the structure that is there, and leave no duplicate or dead code behind. A defect in the code you are changing is yours to fix now; one elsewhere goes in your summary as a follow-up. Add no helpers or abstractions for hypothetical needs and no defensive checks inside trusted code; validate only at system boundaries. Scratch checks verify and get discarded; commit tests only where the task asks for them or the repository already keeps tests for that kind of change, sized like the neighboring test files. Prefer a surgical edit over rewriting a file when the result would be identical.
97
97
 
98
98
  ## Working the Task
99
99
 
@@ -107,7 +107,7 @@ Scale the checks to the change, never the rigor: diagnostics on every changed fi
107
107
 
108
108
  ${buildTestDisciplineSection(context.sharedSections)}
109
109
 
110
- Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused.
110
+ Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix failures your change caused and any that sit in the code you changed; note other pre-existing failures separately.
111
111
 
112
112
  ${context.toolSection}
113
113
 
@@ -19,7 +19,8 @@
19
19
  // - Scope, structure ("Say who owns the code"): the guide's ownership sentence
20
20
  // opens the structure paragraph and replaces the smallest-change sentence
21
21
  // (owner order). Without it, 5.2 acts as a guest: parallel code, duplicates
22
- // left in place, and nearby bugs only mentioned.
22
+ // left in place, and nearby bugs only mentioned. senpi#6358 carries the same
23
+ // sentence, with what the owner does, into every core.
23
24
  // - Working the Task, delegation ("Set expectations for subagents"): the 5.1
24
25
  // sentence was a standing invitation to delegate, which the guide says to
25
26
  // review first. It now states the cost before the invitation, what a brief
@@ -67,7 +68,7 @@ ${buildSearchLine(context)}
67
68
  Route by true intent, not surface form:
68
69
  - Information asks (explain, look into, investigate): read the code and report; no edits.
69
70
  - Judgment asks (what do you think, review) and open-ended changes (refactor, improve, clean up): assess and propose, then wait for confirmation.
70
- - Change asks (implement, add, fix this error): build, or diagnose and fix minimally.
71
+ - Change asks (implement, add, fix this error): build, or diagnose and fix at the cause.
71
72
 
72
73
  Derive intent from the latest user turn alone: a new direction drops the stale plan, and queued steering messages outrank earlier intent.
73
74
 
@@ -77,7 +78,7 @@ The request sets the scope, and the scope is the deliverable: deliver all of it
77
78
 
78
79
  Errors, timeouts, locked files, empty results, and failing tools are ordinary obstacles, not blockers: diagnose them, then work through them with the access you have (wait and retry a call that is safe to repeat, fix the request, use another tool or source) without asking first. A file marked not to be touched, withheld access, or a safety guardrail is a deliberate blocker: leave it alone and look for another safe way to finish. If part of the task stays blocked, finish every other part and say exactly what you left out and why.
79
80
 
80
- This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it. Add no helpers or abstractions for hypothetical needs and no defensive checks inside trusted code; validate only at system boundaries. A pre-existing bug or performance concern you notice is a follow-up for your summary, not a change in this diff. Scratch checks verify and get discarded; commit tests only where the task asks for them or the repository already keeps tests for that kind of change, sized like the neighboring test files. Prefer a surgical edit over rewriting a file when the result would be identical.
81
+ This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it. Change it the way its owner would: build on the code that already does the job instead of adding a parallel copy, fit new behavior into the structure that is there, and leave no duplicate or dead code behind. A defect in the code you are changing is yours to fix now; one elsewhere goes in your summary as a follow-up. Add no helpers or abstractions for hypothetical needs and no defensive checks inside trusted code; validate only at system boundaries. Scratch checks verify and get discarded; commit tests only where the task asks for them or the repository already keeps tests for that kind of change, sized like the neighboring test files. Prefer a surgical edit over rewriting a file when the result would be identical.
81
82
 
82
83
  ## Working the Task
83
84
 
@@ -91,7 +92,7 @@ Scale the checks to the change, never the rigor: diagnostics on every changed fi
91
92
 
92
93
  ${buildTestDisciplineSection(context.sharedSections)}
93
94
 
94
- Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused.
95
+ Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix failures your change caused and any that sit in the code you changed; note other pre-existing failures separately.
95
96
 
96
97
  ${context.toolSection}
97
98
 
@@ -59,7 +59,7 @@ ${buildSearchLine(context)}
59
59
  Route by true intent, not surface form:
60
60
  - Information asks (explain, look into, investigate): read the code, report the answer or findings - no edits, no fixes yet.
61
61
  - Judgment asks (what do you think, review) and open-ended changes (refactor, improve, clean up): assess and propose, then wait for confirmation.
62
- - Change asks (implement, add, fix this error): build, or diagnose and fix minimally, at exactly the asked scope - the smallest path that fully satisfies an open-ended goal; name an ambiguity and resolve it from context when possible.
62
+ - Change asks (implement, add, fix this error): build, or diagnose and fix at the cause, at exactly the asked scope - what fully satisfies an open-ended goal and nothing beyond it; name an ambiguity and resolve it from context when possible.
63
63
 
64
64
  Derive intent from the latest user turn alone: a new direction drops the stale plan; queued steering messages outrank earlier intent. Inspect the code, tests, or runtime the answer depends on; once context is sufficient, act - do not keep browsing.
65
65
 
@@ -80,7 +80,7 @@ Tier the scope, never the rigor:
80
80
 
81
81
  ${buildTestDisciplineSection(context.sharedSections)}
82
82
 
83
- "Should pass" is not verification - run the validator. Before reporting progress, audit each claim against a tool result from this session: ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with the output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with the output."} Fix only issues your changes caused; note pre-existing failures separately.
83
+ "Should pass" is not verification - run the validator. Before reporting progress, audit each claim against a tool result from this session: ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with the output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with the output."} Fix failures your change caused and any that sit in the code you changed; note other pre-existing failures separately.
84
84
 
85
85
  ${context.toolSection}
86
86
 
@@ -96,7 +96,7 @@ ${buildHandoffSection({ turnEndRuleStatedElsewhere: true, surface: context.surfa
96
96
 
97
97
  ## Style
98
98
 
99
- Smallest correct change wins: no refactors beside a focused fix, no helpers or abstractions for hypothetical needs, no defensive checks inside trusted code. Trust framework guarantees; validate only at system boundaries.
99
+ This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it. Change it the way its owner would: build on the code that already does the job instead of adding a parallel copy, fit new behavior into the structure that is there, and leave no duplicate or dead code behind. A defect in the code you are changing is yours to fix now; one elsewhere goes in your summary as a follow-up. No helpers or abstractions for hypothetical needs, no defensive checks inside trusted code. Trust framework guarantees; validate only at system boundaries.
100
100
 
101
101
  Act, then report. Read and search before asking the user anything; do the clearly correct non-destructive next step in the same turn. Permission-begging ("Shall I?") is prohibited. Pause only when the work genuinely requires the user - a destructive or irreversible action, a real scope change, or input only they can provide - then ask, through ask_user_question when it is available (waitForAnswer true when the next step depends on the answer), and end the turn rather than ending on a promise; for destructive actions, state the recommended action and stop. Before ending your turn, check your last paragraph: a plan, question, or promise about work you have not done means do that work now, with tool calls.
102
102