@gajae-code/coding-agent 0.11.10 → 0.12.0

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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -1,6 +1,6 @@
1
1
  import type { AgentMessage } from "@gajae-code/agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message } from "@gajae-code/ai";
3
- import { type Component, Spacer, Text, TruncatedText, type TUI, truncateToWidth } from "@gajae-code/tui";
3
+ import { type Component, Loader, Spacer, Text, TruncatedText, type TUI, truncateToWidth } from "@gajae-code/tui";
4
4
  import { settings } from "../../config/settings";
5
5
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
6
6
  import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
@@ -85,6 +85,99 @@ export function prepareTranscriptRebuild(ui: TUI, policy: TranscriptRebuildPolic
85
85
  if (policy === "replace-identity") ui.resetViewportAnchorIntent();
86
86
  else ui.prepareViewportAnchorForTranscriptRebuild();
87
87
  }
88
+
89
+ export const RESUME_PROGRESS_COMMIT_TIMEOUT_MS = 250;
90
+
91
+ export interface ResumeProgressLease {
92
+ readonly committed: Promise<boolean>;
93
+ clear(): void;
94
+ }
95
+
96
+ /**
97
+ * Mount a resume loader on the live status rail and wait for its render generation
98
+ * to commit before session I/O begins. The commit is advisory: a stopped or
99
+ * unavailable terminal resolves false and callers continue without blocking.
100
+ *
101
+ * Fail open (no-op lease, committed=false) when the status rail or UI lacks the
102
+ * child-mutation/render-commit surface required to mount progress. Headless and
103
+ * minimal controller contexts keep resume/migration semantics; full interactive
104
+ * TUI containers retain progress-before-switch.
105
+ */
106
+ export function acquireResumeProgressLease(
107
+ ctx: Pick<InteractiveModeContext, "ui" | "statusContainer">,
108
+ ): ResumeProgressLease {
109
+ if (!canMountResumeProgressLease(ctx)) {
110
+ return {
111
+ committed: Promise.resolve(false),
112
+ clear(): void {},
113
+ };
114
+ }
115
+
116
+ const statusContainer = ctx.statusContainer as ResumeProgressStatusSurface;
117
+ const ui = ctx.ui as ResumeProgressUiSurface;
118
+ const loader = new Loader(
119
+ ui,
120
+ spinner => theme?.fg?.("accent", spinner) ?? spinner,
121
+ message => theme?.fg?.("muted", message) ?? message,
122
+ "Resuming session…",
123
+ );
124
+ statusContainer.addChild(loader);
125
+ const generation = ui.requestRenderWithGeneration(false, "resume-progress");
126
+ let active = true;
127
+ const committed = ui.waitForRenderCommit(generation, RESUME_PROGRESS_COMMIT_TIMEOUT_MS).catch(() => false);
128
+ return {
129
+ committed,
130
+ clear(): void {
131
+ if (!active) return;
132
+ active = false;
133
+ if (statusContainer.children.includes(loader)) statusContainer.removeChild(loader);
134
+ else loader.stop();
135
+ ui.requestRender(false, "resume-progress-clear");
136
+ },
137
+ };
138
+ }
139
+
140
+ type ResumeProgressStatusSurface = {
141
+ addChild: (child: Component) => void;
142
+ removeChild: (child: Component) => void;
143
+ children: Component[];
144
+ };
145
+
146
+ type ResumeProgressUiSurface = InteractiveModeContext["ui"] & {
147
+ requestRenderWithGeneration: (force?: boolean, source?: string) => number;
148
+ waitForRenderCommit: (generation: number, timeoutMs?: number) => Promise<boolean>;
149
+ requestRender: (force?: boolean, source?: string) => void;
150
+ };
151
+
152
+ function canMountResumeProgressLease(ctx: Pick<InteractiveModeContext, "ui" | "statusContainer">): boolean {
153
+ const status = ctx.statusContainer as
154
+ | Partial<{
155
+ addChild: unknown;
156
+ removeChild: unknown;
157
+ children: unknown;
158
+ }>
159
+ | null
160
+ | undefined;
161
+ const ui = ctx.ui as
162
+ | Partial<{
163
+ requestRenderWithGeneration: unknown;
164
+ waitForRenderCommit: unknown;
165
+ requestRender: unknown;
166
+ }>
167
+ | null
168
+ | undefined;
169
+
170
+ return (
171
+ !!status &&
172
+ typeof status.addChild === "function" &&
173
+ typeof status.removeChild === "function" &&
174
+ Array.isArray(status.children) &&
175
+ !!ui &&
176
+ typeof ui.requestRenderWithGeneration === "function" &&
177
+ typeof ui.waitForRenderCommit === "function" &&
178
+ typeof ui.requestRender === "function"
179
+ );
180
+ }
88
181
  type TextBlock = { type: "text"; text: string };
89
182
  interface RenderInitialMessagesOptions {
90
183
  preserveExistingChat?: boolean;
@@ -37,11 +37,19 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
37
37
  {{restrictedBash}}
38
38
  - Never approve code or plans you have not grounded in inspected files.
39
39
  - Never give generic advice detached from this codebase.
40
- - Never approve CRITICAL or HIGH severity issues.
40
+ - Never approve carryover CRITICAL or HIGH severity issues (raised in a prior pass and still unresolved). A fresh CRITICAL/HIGH minted from pass 2 on previously-approved ground blocks only with an explicit why-not-visible-earlier justification (rule 2); without that justification, record it as a non-blocking caveat with its severity noted. On pass 1 every CRITICAL/HIGH blocks.
41
41
  - Do not skip spec compliance to jump to style nitpicks.
42
42
  - Be constructive: explain why an issue matters and how to fix it or strengthen the design.
43
43
  </constraints>
44
44
 
45
+ <re_review_ratchet>
46
+ - Rule 1 (delta-only): from pass 2, review only the delta against the prior pass plus the resolution of previously raised findings; do not re-litigate previously-approved ground. The prior pass is identified by the re-review context bundle: prior reviewed-plan path, prior same-lane review path, and the explicit run-level pass number supplied in the assignment.
47
+ - Rule 2 (novelty justification): a new blocker on previously-reviewed ground requires an explicit "why this was not visible in the prior pass" justification (e.g. revealed by a fix, new file evidence); without it, demote to a non-blocking caveat.
48
+ - Rule 3 (verdict monotonicity): once all blockers from the prior pass are resolved, neither Architectural Status (`CLEAR`/`WATCH`/`BLOCK`) nor Code Review Recommendation (`APPROVE`/`COMMENT`/`REQUEST CHANGES`) may worsen absent a rule-2-justified new blocker.
49
+ - Rule 4 (severity discipline): carryover CRITICAL or HIGH severity issues (raised in a prior pass and still unresolved) remain blocking regardless of pass number. A fresh CRITICAL/HIGH minted from pass 2 on previously-approved ground blocks only with an explicit why-not-visible-earlier justification (rule 2); without that justification, record it as a non-blocking caveat with its severity noted. On pass 1 every CRITICAL/HIGH blocks.
50
+ - Rule 5 (counter-review awareness): From pass 2 your output is counter-reviewed by Critic for over-engineering and unnecessary scope expansion; unjustified scope inflation is flagged as a review defect and does not force revision passes. On pass 2+, do not broaden scope, add options, or demand synthesis beyond what resolves prior findings; constructive synthesis (Stage 3) stays full-strength on pass 1 only.
51
+ </re_review_ratchet>
52
+
45
53
  <review_stages>
46
54
  1. Understand the request, spec, plan, or diff.
47
55
  2. Gather file-backed evidence.
@@ -32,6 +32,15 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
32
32
  - For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
33
33
  </constraints>
34
34
 
35
+ <re_review_ratchet>
36
+ - Rule 1 (delta-only): from pass 2, review only the delta against the prior pass plus the resolution of previously raised findings; do not re-litigate previously-approved ground. The prior pass is identified by the re-review context bundle (WI-3): prior reviewed-plan path, prior same-lane review path, and the explicit run-level pass number supplied in the assignment.
37
+ - Rule 2 (novelty justification): a new blocker on previously-reviewed ground requires an explicit "why this was not visible in the prior pass" justification (e.g. revealed by a fix, new file evidence); without it, demote to a non-blocking caveat.
38
+ - Rule 3 (verdict monotonicity): once all blockers from the prior pass are resolved, the verdict must not worsen (e.g. ITERATE -> REJECT) absent a rule-2-justified new blocker.
39
+ - Rule 4 (severity discipline): carryover blockers (raised in a prior pass, still unresolved) remain blocking regardless of pass number. A fresh high-severity concern minted from pass 2 on previously-approved ground follows rule 2: it blocks only with the why-not-visible-earlier justification, else it is recorded as a non-blocking caveat with severity noted.
40
+ - Rule 5 (counter-review duty): from pass 2, Critic also reviews the Architect output (routed via the context bundle) for over-engineering and unnecessary scope expansion; flag inflation as a review defect and do NOT convert unjustified Architect demands into ITERATE — inflating demands must not force revision passes.
41
+ - Enrichment lane ("spec too thin — expand") preserved verbatim but justification-gated from pass 2: expansion requests on already-reviewed ground need the rule-2 justification.
42
+ </re_review_ratchet>
43
+
35
44
  <execution_loop>
36
45
  1. Read the plan and referenced artifacts.
37
46
  2. Extract and verify file references.
@@ -34,7 +34,7 @@ Explore just enough context, implement the smallest correct change, and leave co
34
34
  {{#if ultragoalRedTeam}}
35
35
 
36
36
  <ultragoal_red_team_mode>
37
- This mode activates only when the assignment explicitly labels Executor as Ultragoal completion QA/red-team or asks for `executorQa` red-team evidence. Otherwise, preserve ordinary Executor behavior.
37
+ This mode activates only when the task sets typed `executionMode: "ultragoal-red-team"`, or — as a compatibility fallback — when the assignment text explicitly labels Executor as Ultragoal completion QA/red-team or asks for `executorQa` red-team evidence (not a bare `executorQa` field-name mention). Prefer the typed flag. Otherwise, preserve ordinary Executor behavior.
38
38
 
39
39
  When active:
40
40
  - Follow the exact `executorQa` contract provided in the assignment (matrix shape, row fields, artifact/replay rules); the runtime validates it strictly. If the assignment omits the contract, read the ultragoal SKILL's executor QA section before producing evidence.
@@ -24,7 +24,6 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
24
24
  - Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
25
25
  - Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
26
26
  - Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
27
- - Deep-interview state is runtime-owned: diagnose with `gjc deep-interview sanity-check`, inspect only the affected bounded selector, and repair through typed operations using CLI-generated/edited drafts (`draft create|edit|show|check|rebase|discard`) consumed with `--draft-id`. Inline JSON request flags are compatibility-only; normal flow must never reconstruct a payload or full envelope.
28
27
  - Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
29
28
  - Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
30
29
  - Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
@@ -9,12 +9,13 @@ Read files, directories, archives, SQLite databases, images, documents, internal
9
9
  ## Parameters
10
10
 
11
11
  - `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
12
+ - `truncation` — optional `head` | `last` | `both`; selects which end of an over-budget result to retain. Configured default: {{TRUNCATION_DEFAULT}} (factory default: `last`); non-file routes such as URLs, directories and converted documents default to `head`. A line-range selector still bounds the selection — this only picks which end of that selection survives the byte/line cap. SQLite row queries page via their own `limit`/`offset` and ignore it.
12
13
 
13
- ## Selectors
14
14
 
15
+ ## Selectors
15
16
  Append `:<sel>` to `path`. The bare path falls back to the default mode.
16
17
 
17
- - _(none)_ — parseable code → structural summary (signatures kept, bodies elided); other files → read from the start (up to {{DEFAULT_LIMIT}} lines).
18
+ - _(none)_ — parseable code → structural summary (signatures kept, bodies elided); a plain text file → a bounded receipt of about {{RECEIPT_LINES}} lines or {{RECEIPT_KIB}} KiB, whichever is smaller; the configured truncation direction is {{TRUNCATION_DEFAULT}} (factory default: last). Line+hash anchors keep their real file line numbers and a footer names the omitted range. Archive members use the larger {{DEFAULT_MAX_LINES}}-line / 50 KiB budget. Converted documents, notebooks, URLs and directory listings still start from the beginning.
18
19
  - `:50` / `:50-` — read from line 50 onward.
19
20
  - `:50-200` — lines 50–200 inclusive.
20
21
  - `:50+150` — 150 lines starting at line 50.
@@ -39,6 +40,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
39
40
  `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`
40
41
 
41
42
  If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.
43
+ - Directional windows identify the retained first/last lines and the omitted range; use the `re-read <path>:1-<total>` or `:raw` hint in the footer to recover the full content.
42
44
 
43
45
  # Documents & Notebooks
44
46
 
@@ -0,0 +1,66 @@
1
+ import type {
2
+ MemoryGuardDomainSnapshot,
3
+ MemoryGuardWorkerAccounting,
4
+ MemoryGuardWorkerSample,
5
+ } from "./memory-guard-contract";
6
+
7
+ export interface MemoryGuardDomainInput {
8
+ effectiveLimitBytes: number;
9
+ totalUsageBytes: number;
10
+ parentBytes: number;
11
+ parentReserveBytes: number;
12
+ workers: readonly MemoryGuardWorkerSample[];
13
+ }
14
+
15
+ function assertNonNegativeSafeInteger(name: string, value: number): number {
16
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`invalid_${name}`);
17
+ return value;
18
+ }
19
+
20
+ function sumWorkerBytes(workers: readonly MemoryGuardWorkerSample[]): number {
21
+ let total = 0;
22
+ for (const worker of workers) {
23
+ total += assertNonNegativeSafeInteger(`worker_bytes:${worker.workerId}`, worker.bytes);
24
+ if (!Number.isSafeInteger(total)) throw new Error("invalid_worker_total_bytes");
25
+ }
26
+ return total;
27
+ }
28
+
29
+ export function computeMemoryGuardDomain(input: MemoryGuardDomainInput): MemoryGuardDomainSnapshot {
30
+ const effectiveLimitBytes = assertNonNegativeSafeInteger("effective_limit_bytes", input.effectiveLimitBytes);
31
+ const totalUsageBytes = assertNonNegativeSafeInteger("total_usage_bytes", input.totalUsageBytes);
32
+ const parentBytes = assertNonNegativeSafeInteger("parent_bytes", input.parentBytes);
33
+ const parentReserveBytes = assertNonNegativeSafeInteger("parent_reserve_bytes", input.parentReserveBytes);
34
+ const totalWorkerBytes = sumWorkerBytes(input.workers);
35
+ const acceptedWorkers = input.workers.filter(worker => worker.accepted !== false);
36
+ const acceptedWorkerBytes = sumWorkerBytes(acceptedWorkers);
37
+ const acceptedWorkerCount = acceptedWorkers.length;
38
+ const unmanagedBytes = Math.max(0, totalUsageBytes - parentBytes - acceptedWorkerBytes);
39
+ const headroomBytes = Math.max(0, effectiveLimitBytes - totalUsageBytes);
40
+ const workerBudgetBytes = Math.max(0, effectiveLimitBytes - unmanagedBytes - parentReserveBytes);
41
+ const perWorkerAllowanceBytes = acceptedWorkerCount === 0 ? 0 : workerBudgetBytes / acceptedWorkerCount;
42
+ const workers: MemoryGuardWorkerAccounting[] = input.workers.map(worker => {
43
+ const accepted = worker.accepted !== false;
44
+ return {
45
+ workerId: worker.workerId,
46
+ bytes: worker.bytes,
47
+ accepted,
48
+ allowanceBytes: accepted ? perWorkerAllowanceBytes : 0,
49
+ excessBytes: accepted ? Math.max(0, worker.bytes - perWorkerAllowanceBytes) : 0,
50
+ };
51
+ });
52
+ return {
53
+ effectiveLimitBytes,
54
+ totalUsageBytes,
55
+ parentBytes,
56
+ parentReserveBytes,
57
+ totalWorkerBytes,
58
+ acceptedWorkerCount,
59
+ unmanagedBytes,
60
+ headroomBytes,
61
+ workerBudgetBytes,
62
+ perWorkerAllowanceBytes,
63
+ hostExcessBytes: Math.max(0, parentBytes - parentReserveBytes),
64
+ workers,
65
+ };
66
+ }
@@ -0,0 +1,73 @@
1
+ export interface MemoryGuardPolicy {
2
+ enabled: boolean;
3
+ checkIntervalMs: number;
4
+ gcThresholdRatio: number;
5
+ restartThresholdRatio: number;
6
+ restartThresholdWindowMs: number;
7
+ cooldownMs: number;
8
+ parentReserveBytes: number;
9
+ policyLimitBytes: number | null;
10
+ }
11
+
12
+ export interface MemoryGuardWorkerSample {
13
+ workerId: string;
14
+ bytes: number;
15
+ accepted?: boolean;
16
+ }
17
+
18
+ export interface MemoryGuardWorkerAccounting {
19
+ workerId: string;
20
+ bytes: number;
21
+ accepted: boolean;
22
+ allowanceBytes: number;
23
+ excessBytes: number;
24
+ }
25
+
26
+ export interface MemoryGuardDomainSnapshot {
27
+ effectiveLimitBytes: number;
28
+ totalUsageBytes: number;
29
+ parentBytes: number;
30
+ parentReserveBytes: number;
31
+ totalWorkerBytes: number;
32
+ acceptedWorkerCount: number;
33
+ unmanagedBytes: number;
34
+ headroomBytes: number;
35
+ workerBudgetBytes: number;
36
+ perWorkerAllowanceBytes: number;
37
+ hostExcessBytes: number;
38
+ workers: MemoryGuardWorkerAccounting[];
39
+ }
40
+
41
+ export interface MemoryGuardHostTarget {
42
+ kind: "host";
43
+ excessBytes: number;
44
+ }
45
+
46
+ export interface MemoryGuardWorkerTarget {
47
+ kind: "worker";
48
+ workerId: string;
49
+ excessBytes: number;
50
+ }
51
+
52
+ export type MemoryGuardActionTarget = MemoryGuardHostTarget | MemoryGuardWorkerTarget;
53
+
54
+ export type MemoryGuardNoopReason =
55
+ | "memory_guard_action_noop_unmanaged_or_within_allowance"
56
+ | "memory_guard_action_noop_unsupported";
57
+
58
+ export type MemoryGuardDecision =
59
+ | { kind: "execute"; target: MemoryGuardActionTarget }
60
+ | { kind: "noop"; reason: MemoryGuardNoopReason }
61
+ | { kind: "revalidated_out"; reason: "memory_guard_action_revalidated_out" };
62
+
63
+ export interface MemoryGuardSchedulerState {
64
+ timerActive: boolean;
65
+ registrationCount: number;
66
+ inProgress: boolean;
67
+ generation: number;
68
+ pendingDeadline: number | null;
69
+ pendingOwner: { generation: number; token: number } | null;
70
+ deferredDeadline: number | null;
71
+ deferredGeneration: number | null;
72
+ activeGeneration: number | null;
73
+ }
@@ -0,0 +1,292 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type {
3
+ MemoryGuardActionTarget,
4
+ MemoryGuardDecision,
5
+ MemoryGuardDomainSnapshot,
6
+ MemoryGuardPolicy,
7
+ MemoryGuardSchedulerState,
8
+ } from "./memory-guard-contract";
9
+
10
+ const BYTES_PER_MB = 1024 * 1024;
11
+ const DEFAULT_CHECK_INTERVAL_MS = 30_000;
12
+
13
+ type ScheduleOwner = { generation: number; token: number };
14
+ type DeferredSchedule = { generation: number; deadline: number };
15
+ type WorkOwner = { generation: number; source: "timer" | "external" };
16
+
17
+ export interface MemoryGuardHostRegistration {
18
+ ownerId: string;
19
+ intervalMs: number;
20
+ }
21
+
22
+ export interface MemoryGuardHostOptions {
23
+ run: () => Promise<void>;
24
+ logDebug?: (message: string, meta?: Record<string, unknown>) => void;
25
+ schedulerNow?: () => number;
26
+ }
27
+
28
+ function normalizePositiveIntervalMs(intervalMs: number): number {
29
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) throw new Error("invalid_interval_ms");
30
+ return intervalMs;
31
+ }
32
+
33
+ function toRatio(percent: number): number {
34
+ return percent / 100;
35
+ }
36
+
37
+ function mbToBytes(value: number): number {
38
+ return Math.round(value * BYTES_PER_MB);
39
+ }
40
+
41
+ function sortTargets(left: MemoryGuardActionTarget, right: MemoryGuardActionTarget): number {
42
+ if (left.excessBytes !== right.excessBytes) return right.excessBytes - left.excessBytes;
43
+ if (left.kind !== right.kind) return left.kind === "host" ? -1 : 1;
44
+ if (left.kind === "worker" && right.kind === "worker") return left.workerId.localeCompare(right.workerId);
45
+ return 0;
46
+ }
47
+
48
+ export function resolveMemoryGuardPolicy(settings: Settings): MemoryGuardPolicy {
49
+ const configuredPolicyLimitMb = settings.get("memoryGuard.policyLimitMb");
50
+ return {
51
+ enabled: settings.get("memoryGuard.enabled"),
52
+ checkIntervalMs: settings.get("memoryGuard.checkIntervalMs"),
53
+ gcThresholdRatio: toRatio(settings.get("memoryGuard.gcThresholdPercent")),
54
+ restartThresholdRatio: toRatio(settings.get("memoryGuard.restartThresholdPercent")),
55
+ restartThresholdWindowMs: settings.get("memoryGuard.restartThresholdWindowMs"),
56
+ cooldownMs: settings.get("memoryGuard.cooldownMs"),
57
+ parentReserveBytes: mbToBytes(settings.get("memoryGuard.parentReserveMb")),
58
+ policyLimitBytes: configuredPolicyLimitMb > 0 ? mbToBytes(configuredPolicyLimitMb) : null,
59
+ };
60
+ }
61
+
62
+ export function chooseMemoryGuardAction(input: {
63
+ domain: MemoryGuardDomainSnapshot;
64
+ hostSupported: boolean;
65
+ workerSupported: (workerId: string) => boolean;
66
+ }): MemoryGuardDecision {
67
+ const candidates: MemoryGuardActionTarget[] = [];
68
+ if (input.hostSupported && input.domain.hostExcessBytes > 0) {
69
+ candidates.push({ kind: "host", excessBytes: input.domain.hostExcessBytes });
70
+ }
71
+ for (const worker of input.domain.workers) {
72
+ if (!worker.accepted || worker.excessBytes <= 0 || !input.workerSupported(worker.workerId)) continue;
73
+ candidates.push({ kind: "worker", workerId: worker.workerId, excessBytes: worker.excessBytes });
74
+ }
75
+ if (candidates.length > 0) {
76
+ candidates.sort(sortTargets);
77
+ return { kind: "execute", target: candidates[0]! };
78
+ }
79
+ const unsupportedExcessExists =
80
+ input.domain.hostExcessBytes > 0 ||
81
+ input.domain.workers.some(worker => worker.accepted && worker.excessBytes > 0);
82
+ if (unsupportedExcessExists) return { kind: "noop", reason: "memory_guard_action_noop_unsupported" };
83
+ return { kind: "noop", reason: "memory_guard_action_noop_unmanaged_or_within_allowance" };
84
+ }
85
+
86
+ export function revalidateMemoryGuardAction(
87
+ previous: Extract<MemoryGuardDecision, { kind: "execute" }>,
88
+ revalidated: MemoryGuardDecision,
89
+ ): MemoryGuardDecision {
90
+ if (revalidated.kind !== "execute")
91
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
92
+ if (previous.target.kind !== revalidated.target.kind)
93
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
94
+ if (
95
+ previous.target.kind === "worker" &&
96
+ revalidated.target.kind === "worker" &&
97
+ previous.target.workerId !== revalidated.target.workerId
98
+ ) {
99
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
100
+ }
101
+ return revalidated;
102
+ }
103
+
104
+ export class MemoryGuardHost {
105
+ #run: () => Promise<void>;
106
+ #logDebug: (message: string, meta?: Record<string, unknown>) => void;
107
+ #defaultSchedulerNow: () => number;
108
+ #schedulerNow: () => number;
109
+ #registrations = new Map<string, { intervalMs: number; nextDueMs: number }>();
110
+ #pendingTimer: NodeJS.Timeout | null = null;
111
+ #pendingDeadline: number | null = null;
112
+ #pendingOwner: ScheduleOwner | null = null;
113
+ #deferredSchedule: DeferredSchedule | null = null;
114
+ #inProgressOwner: WorkOwner | null = null;
115
+ #stopped = false;
116
+ #generation = 0;
117
+ #nextTimerToken = 0;
118
+
119
+ constructor(options: MemoryGuardHostOptions) {
120
+ this.#run = options.run;
121
+ this.#logDebug = options.logDebug ?? (() => undefined);
122
+ this.#defaultSchedulerNow = options.schedulerNow ?? (() => performance.now());
123
+ this.#schedulerNow = this.#defaultSchedulerNow;
124
+ }
125
+
126
+ register(registration: MemoryGuardHostRegistration): () => void {
127
+ const intervalMs = normalizePositiveIntervalMs(registration.intervalMs);
128
+ const now = this.#schedulerNow();
129
+ const existing = this.#registrations.get(registration.ownerId);
130
+ const isNewRegistration = !existing;
131
+ const nextDueMs = existing ? Math.min(existing.nextDueMs, now + intervalMs) : now + intervalMs;
132
+ this.#registrations.set(registration.ownerId, { intervalMs, nextDueMs });
133
+ this.#stopped = false;
134
+ if (isNewRegistration) {
135
+ const deadline = nextDueMs;
136
+ if (
137
+ this.#inProgressOwner?.generation === this.#generation &&
138
+ (this.#pendingDeadline === null || this.#pendingDeadline <= deadline)
139
+ ) {
140
+ this.#deferSchedule(this.#generation, deadline);
141
+ } else {
142
+ this.#requestSchedule(deadline);
143
+ }
144
+ }
145
+ let unregistered = false;
146
+ return () => {
147
+ if (unregistered) return;
148
+ unregistered = true;
149
+ this.#registrations.delete(registration.ownerId);
150
+ if (this.#registrations.size === 0) this.#stop();
151
+ };
152
+ }
153
+
154
+ updateInterval(ownerId: string, intervalMs: number): void {
155
+ const existing = this.#registrations.get(ownerId);
156
+ if (!existing) return;
157
+ const normalized = normalizePositiveIntervalMs(intervalMs);
158
+ if (existing.intervalMs === normalized) return;
159
+ const now = this.#schedulerNow();
160
+ const nextDueMs = now + normalized;
161
+ this.#registrations.set(ownerId, { intervalMs: normalized, nextDueMs });
162
+ if (this.#inProgressOwner) return;
163
+ const nextDeadline = this.#nextDeadlineMs();
164
+ if (this.#pendingDeadline !== null && nextDeadline > this.#pendingDeadline) {
165
+ this.#clearPendingSchedule();
166
+ }
167
+ this.#requestSchedule(nextDeadline);
168
+ }
169
+
170
+ async runTick(generation = this.#generation, source: WorkOwner["source"] = "external"): Promise<void> {
171
+ if (this.#inProgressOwner || this.#registrations.size === 0) return;
172
+ const owner: WorkOwner = { generation, source };
173
+ this.#inProgressOwner = owner;
174
+ try {
175
+ await this.#run();
176
+ } catch (error) {
177
+ this.#logDebug("memory guard tick failed", { error: error instanceof Error ? error.message : String(error) });
178
+ } finally {
179
+ if (this.#inProgressOwner === owner) this.#inProgressOwner = null;
180
+ this.#advanceDueTimes();
181
+ this.#reconcileCurrentSchedule();
182
+ }
183
+ }
184
+
185
+ async runTimerCallbackForTest(owner: { generation: number; token: number }, deadline: number): Promise<void> {
186
+ await this.#handleTimerCallback(owner, deadline);
187
+ }
188
+
189
+ setSchedulerNowForTest(now: () => number): void {
190
+ this.#schedulerNow = now;
191
+ }
192
+
193
+ getStateForTest(): MemoryGuardSchedulerState {
194
+ return {
195
+ timerActive: this.#pendingTimer !== null,
196
+ registrationCount: this.#registrations.size,
197
+ inProgress: this.#inProgressOwner !== null,
198
+ generation: this.#generation,
199
+ pendingDeadline: this.#pendingDeadline,
200
+ pendingOwner: this.#pendingOwner ? { ...this.#pendingOwner } : null,
201
+ deferredDeadline: this.#deferredSchedule?.deadline ?? null,
202
+ deferredGeneration: this.#deferredSchedule?.generation ?? null,
203
+ activeGeneration: this.#inProgressOwner?.generation ?? null,
204
+ };
205
+ }
206
+
207
+ resetForTest(): void {
208
+ this.#stop();
209
+ this.#registrations.clear();
210
+ this.#inProgressOwner = null;
211
+ this.#deferredSchedule = null;
212
+ this.#nextTimerToken = 0;
213
+ this.#schedulerNow = this.#defaultSchedulerNow;
214
+ }
215
+
216
+ #nextDeadlineMs(): number {
217
+ let minDue = Number.POSITIVE_INFINITY;
218
+ for (const reg of this.#registrations.values()) {
219
+ minDue = Math.min(minDue, reg.nextDueMs);
220
+ }
221
+ return Number.isFinite(minDue) ? minDue : this.#schedulerNow() + DEFAULT_CHECK_INTERVAL_MS;
222
+ }
223
+
224
+ #advanceDueTimes(): void {
225
+ const now = this.#schedulerNow();
226
+ for (const reg of this.#registrations.values()) {
227
+ if (reg.nextDueMs <= now) {
228
+ reg.nextDueMs = now + reg.intervalMs;
229
+ }
230
+ }
231
+ }
232
+
233
+ #clearPendingSchedule(): void {
234
+ if (this.#pendingTimer) clearTimeout(this.#pendingTimer);
235
+ this.#pendingTimer = null;
236
+ this.#pendingDeadline = null;
237
+ this.#pendingOwner = null;
238
+ }
239
+
240
+ #requestSchedule(deadline: number): void {
241
+ if (this.#stopped || this.#registrations.size === 0) return;
242
+ if (this.#pendingDeadline !== null && this.#pendingDeadline <= deadline) return;
243
+ this.#clearPendingSchedule();
244
+ const owner = { generation: this.#generation, token: ++this.#nextTimerToken };
245
+ this.#pendingDeadline = deadline;
246
+ this.#pendingOwner = owner;
247
+ this.#pendingTimer = setTimeout(
248
+ () => {
249
+ void this.#handleTimerCallback(owner, deadline);
250
+ },
251
+ Math.max(0, deadline - this.#schedulerNow()),
252
+ );
253
+ this.#pendingTimer.unref?.();
254
+ }
255
+
256
+ #deferSchedule(generation: number, deadline: number): void {
257
+ if (generation !== this.#generation) return;
258
+ if (this.#deferredSchedule?.generation === generation) {
259
+ this.#deferredSchedule.deadline = Math.min(this.#deferredSchedule.deadline, deadline);
260
+ return;
261
+ }
262
+ this.#deferredSchedule = { generation, deadline };
263
+ }
264
+
265
+ async #handleTimerCallback(owner: ScheduleOwner, deadline: number): Promise<void> {
266
+ if (this.#pendingOwner?.generation !== owner.generation || this.#pendingOwner.token !== owner.token) return;
267
+ this.#pendingTimer = null;
268
+ this.#pendingDeadline = null;
269
+ this.#pendingOwner = null;
270
+ if (this.#inProgressOwner) {
271
+ this.#deferSchedule(owner.generation, deadline);
272
+ return;
273
+ }
274
+ await this.runTick(owner.generation, "timer");
275
+ }
276
+
277
+ #reconcileCurrentSchedule(): void {
278
+ if (this.#stopped || this.#registrations.size === 0 || this.#inProgressOwner) return;
279
+ const normalDeadline = this.#nextDeadlineMs();
280
+ const deferredDeadline =
281
+ this.#deferredSchedule?.generation === this.#generation ? this.#deferredSchedule.deadline : null;
282
+ if (deferredDeadline !== null) this.#deferredSchedule = null;
283
+ this.#requestSchedule(deferredDeadline === null ? normalDeadline : Math.min(deferredDeadline, normalDeadline));
284
+ }
285
+
286
+ #stop(): void {
287
+ this.#stopped = true;
288
+ this.#generation++;
289
+ this.#clearPendingSchedule();
290
+ this.#deferredSchedule = null;
291
+ }
292
+ }
@@ -0,0 +1,50 @@
1
+ export interface EffectiveMemoryLimitInput {
2
+ hardCapBytes?: number | null;
3
+ policyLimitBytes?: number | null;
4
+ }
5
+
6
+ export interface EffectiveMemoryLimit {
7
+ hardCapBytes: number | null;
8
+ policyLimitBytes: number | null;
9
+ effectiveBytes: number | null;
10
+ source: "none" | "hard_cap" | "policy_limit" | "hard_cap_and_policy_limit";
11
+ }
12
+
13
+ function normalizePositiveByteCount(value: number | null | undefined): number | null {
14
+ return Number.isSafeInteger(value) && typeof value === "number" && value > 0 ? value : null;
15
+ }
16
+
17
+ export function resolveEffectiveMemoryLimit(input: EffectiveMemoryLimitInput): EffectiveMemoryLimit {
18
+ const hardCapBytes = normalizePositiveByteCount(input.hardCapBytes);
19
+ const policyLimitBytes = normalizePositiveByteCount(input.policyLimitBytes);
20
+ if (hardCapBytes !== null && policyLimitBytes !== null) {
21
+ return {
22
+ hardCapBytes,
23
+ policyLimitBytes,
24
+ effectiveBytes: Math.min(hardCapBytes, policyLimitBytes),
25
+ source: "hard_cap_and_policy_limit",
26
+ };
27
+ }
28
+ if (hardCapBytes !== null) {
29
+ return {
30
+ hardCapBytes,
31
+ policyLimitBytes: null,
32
+ effectiveBytes: hardCapBytes,
33
+ source: "hard_cap",
34
+ };
35
+ }
36
+ if (policyLimitBytes !== null) {
37
+ return {
38
+ hardCapBytes: null,
39
+ policyLimitBytes,
40
+ effectiveBytes: policyLimitBytes,
41
+ source: "policy_limit",
42
+ };
43
+ }
44
+ return {
45
+ hardCapBytes: null,
46
+ policyLimitBytes: null,
47
+ effectiveBytes: null,
48
+ source: "none",
49
+ };
50
+ }