@code-yeongyu/senpi 2026.9.4-3 → 2026.9.5

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 (134) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/core/agent-session.d.ts +17 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +62 -2
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts +5 -0
  7. package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts.map +1 -1
  8. package/dist/core/extensions/builtin/compaction/model-usability-budget.js +11 -4
  9. package/dist/core/extensions/builtin/compaction/model-usability-budget.js.map +1 -1
  10. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +9 -5
  11. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts.map +1 -1
  12. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +20 -11
  13. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js.map +1 -1
  14. package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
  15. package/dist/core/extensions/builtin/recommended-models/index.d.ts.map +1 -1
  16. package/dist/core/extensions/builtin/recommended-models/index.js +1 -0
  17. package/dist/core/extensions/builtin/recommended-models/index.js.map +1 -1
  18. package/dist/core/extensions/builtin/service-tier.d.ts.map +1 -1
  19. package/dist/core/extensions/builtin/service-tier.js +12 -4
  20. package/dist/core/extensions/builtin/service-tier.js.map +1 -1
  21. package/dist/core/extensions/builtin/terminal/durable-command.d.ts +31 -0
  22. package/dist/core/extensions/builtin/terminal/durable-command.d.ts.map +1 -0
  23. package/dist/core/extensions/builtin/terminal/durable-command.js +96 -0
  24. package/dist/core/extensions/builtin/terminal/durable-command.js.map +1 -0
  25. package/dist/core/extensions/builtin/terminal/durable-file.d.ts +42 -0
  26. package/dist/core/extensions/builtin/terminal/durable-file.d.ts.map +1 -0
  27. package/dist/core/extensions/builtin/terminal/durable-file.js +102 -0
  28. package/dist/core/extensions/builtin/terminal/durable-file.js.map +1 -0
  29. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/terminal/extension.js +58 -13
  31. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  32. package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts.map +1 -1
  33. package/dist/core/extensions/builtin/terminal/monitor-notify.js +5 -1
  34. package/dist/core/extensions/builtin/terminal/monitor-notify.js.map +1 -1
  35. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +29 -0
  36. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  37. package/dist/core/extensions/builtin/terminal/monitor-registry.js +49 -0
  38. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  39. package/dist/core/extensions/builtin/terminal/monitor-status.d.ts.map +1 -1
  40. package/dist/core/extensions/builtin/terminal/monitor-status.js +21 -1
  41. package/dist/core/extensions/builtin/terminal/monitor-status.js.map +1 -1
  42. package/dist/core/extensions/builtin/terminal/prompt.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/terminal/prompt.js +9 -5
  44. package/dist/core/extensions/builtin/terminal/prompt.js.map +1 -1
  45. package/dist/core/extensions/builtin/terminal/restore.d.ts +13 -0
  46. package/dist/core/extensions/builtin/terminal/restore.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/terminal/restore.js +31 -3
  48. package/dist/core/extensions/builtin/terminal/restore.js.map +1 -1
  49. package/dist/core/extensions/builtin/terminal/shared.d.ts +25 -0
  50. package/dist/core/extensions/builtin/terminal/shared.d.ts.map +1 -1
  51. package/dist/core/extensions/builtin/terminal/shared.js +25 -0
  52. package/dist/core/extensions/builtin/terminal/shared.js.map +1 -1
  53. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +91 -0
  54. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts.map +1 -0
  55. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.js +14 -0
  56. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.js.map +1 -0
  57. package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts +29 -77
  58. package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts.map +1 -1
  59. package/dist/core/extensions/builtin/terminal/terminal-manifest.js +61 -15
  60. package/dist/core/extensions/builtin/terminal/terminal-manifest.js.map +1 -1
  61. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
  62. package/dist/core/extensions/builtin/terminal/tools/monitor.js +59 -11
  63. package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
  64. package/dist/core/footer-data-provider.d.ts.map +1 -1
  65. package/dist/core/footer-data-provider.js +18 -1
  66. package/dist/core/footer-data-provider.js.map +1 -1
  67. package/dist/core/retry-fallback/chains.d.ts +1 -13
  68. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  69. package/dist/core/retry-fallback/chains.js +3 -49
  70. package/dist/core/retry-fallback/chains.js.map +1 -1
  71. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  72. package/dist/core/retry-fallback/controller.js +4 -9
  73. package/dist/core/retry-fallback/controller.js.map +1 -1
  74. package/dist/core/retry-fallback/expansion.js +1 -1
  75. package/dist/core/retry-fallback/expansion.js.map +1 -1
  76. package/dist/core/retry-fallback/settings.d.ts +2 -4
  77. package/dist/core/retry-fallback/settings.d.ts.map +1 -1
  78. package/dist/core/retry-fallback/settings.js +7 -36
  79. package/dist/core/retry-fallback/settings.js.map +1 -1
  80. package/dist/core/sdk.d.ts.map +1 -1
  81. package/dist/core/sdk.js +1 -1
  82. package/dist/core/sdk.js.map +1 -1
  83. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  84. package/dist/modes/interactive/interactive-mode.js +10 -1
  85. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  86. package/docs/settings.md +1 -0
  87. package/docs/terminal-tools.md +35 -4
  88. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
  89. package/node_modules/@code-yeongyu/senpi-codemode/README.md +2 -2
  90. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  91. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +8 -4
  92. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/display-image.js +162 -0
  93. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +2 -2
  94. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +29 -60
  95. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +66 -1
  96. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +3 -3
  97. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +2 -2
  98. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  99. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts +1 -1
  100. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  101. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +3 -1
  102. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  103. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  104. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +14 -9
  105. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  106. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts +3 -3
  107. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
  108. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  109. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts +1 -1
  110. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  111. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +6 -4
  112. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  113. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +30 -0
  114. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  115. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +30 -0
  116. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  117. package/node_modules/@earendil-works/pi-ai/dist/models.d.ts +3 -1
  118. package/node_modules/@earendil-works/pi-ai/dist/models.d.ts.map +1 -1
  119. package/node_modules/@earendil-works/pi-ai/dist/models.js +27 -3
  120. package/node_modules/@earendil-works/pi-ai/dist/models.js.map +1 -1
  121. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  122. package/node_modules/@earendil-works/pi-ai/dist/providers/data/azure-openai-responses.json +1 -1
  123. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  124. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  125. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  126. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  127. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  128. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  129. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  130. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  131. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  132. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  133. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  134. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"service-tier.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/service-tier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\nimport type { ModelRegistry } from \"../../model-registry.ts\";\nimport { SettingsManager } from \"../../settings-manager.ts\";\nimport type { ExtensionAPI, ExtensionCommandContext, ServiceTier } from \"../types.ts\";\n\nexport type { ServiceTier };\n\ntype ProviderPayload = Record<string, unknown>;\n\nconst OPENAI_CODEX_RESPONSES_API = \"openai-codex-responses\";\nexport const CODEX_RESPONSES_API = OPENAI_CODEX_RESPONSES_API;\nconst FAST_MODEL_SUFFIX = \"-fast\";\nconst PRIORITY_TIER: ServiceTier = \"priority\";\nconst SERVICE_TIER_APIS: ReadonlySet<Api> = new Set([\"openai-responses\", OPENAI_CODEX_RESPONSES_API]);\nconst FAST_ARGUMENTS = [\"on\", \"off\"] as const;\nconst FAST_USAGE = \"Usage: /fast [on|off]\";\n\ntype FastArgument = (typeof FAST_ARGUMENTS)[number];\n\nfunction isRecord(value: unknown): value is ProviderPayload {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nexport function addServiceTierToPayload(api: Api | undefined, payload: unknown, serviceTier?: ServiceTier): unknown {\n\tif (!api || !SERVICE_TIER_APIS.has(api) || !serviceTier) {\n\t\treturn payload;\n\t}\n\n\tif (!isRecord(payload) || payload.service_tier !== undefined) {\n\t\treturn payload;\n\t}\n\n\treturn {\n\t\t...payload,\n\t\tservice_tier: serviceTier,\n\t};\n}\n\nfunction getRequestModelId(modelRegistry: ModelRegistry, model: Model<Api>): string {\n\treturn modelRegistry.getUpstreamModelId(model) ?? model.id;\n}\n\nfunction isCompatibleFastVariant(modelRegistry: ModelRegistry, baseModel: Model<Api>, fastModel: Model<Api>): boolean {\n\treturn (\n\t\tfastModel.provider === baseModel.provider &&\n\t\tfastModel.api === baseModel.api &&\n\t\tmodelRegistry.getServiceTier(fastModel) === \"priority\" &&\n\t\tgetRequestModelId(modelRegistry, fastModel) === getRequestModelId(modelRegistry, baseModel)\n\t);\n}\n\nfunction findBaseModel(modelRegistry: ModelRegistry, fastModel: Model<Api>): Model<Api> | undefined {\n\tif (!fastModel.id.endsWith(FAST_MODEL_SUFFIX)) {\n\t\treturn undefined;\n\t}\n\n\tconst baseModelId = fastModel.id.slice(0, -FAST_MODEL_SUFFIX.length);\n\tconst baseModel = modelRegistry.find(fastModel.provider, baseModelId);\n\treturn baseModel && isCompatibleFastVariant(modelRegistry, baseModel, fastModel) ? baseModel : undefined;\n}\n\nfunction findFastModel(modelRegistry: ModelRegistry, baseModel: Model<Api>): Model<Api> | undefined {\n\tif (baseModel.id.endsWith(FAST_MODEL_SUFFIX)) {\n\t\treturn undefined;\n\t}\n\n\tconst fastModel = modelRegistry.find(baseModel.provider, `${baseModel.id}${FAST_MODEL_SUFFIX}`);\n\treturn fastModel && isCompatibleFastVariant(modelRegistry, baseModel, fastModel) ? fastModel : undefined;\n}\n\n/**\n * The model that owns this model's persisted service-tier preference.\n *\n * A `-fast` catalog variant and its base model are one model to the user (`/fast` just swaps\n * between them), so both must read and write ONE key. Anything else can hold contradictory\n * preferences for the same choice.\n */\nexport function resolveServiceTierMemoryModel(modelRegistry: ModelRegistry, model: Model<Api>): Model<Api> {\n\treturn findBaseModel(modelRegistry, model) ?? model;\n}\n\n/** Remembered service tier for this model, normalized onto its base-model key. */\nexport function getRememberedServiceTier(\n\tsettingsManager: SettingsManager,\n\tmodelRegistry: ModelRegistry,\n\tmodel: Model<Api>,\n): ServiceTier | undefined {\n\tconst memoryModel = resolveServiceTierMemoryModel(modelRegistry, model);\n\treturn settingsManager.getModelServiceTier(memoryModel.provider, memoryModel.id);\n}\n\n/** Host capabilities `applyFastMode` needs; satisfied by an extension context plus `pi`. */\nexport interface FastModeContext {\n\tcwd: string;\n\tagentDir: string;\n\tmodel: Model<Api> | undefined;\n\tmodelRegistry: ModelRegistry;\n\t/** Tier the host resolved for the active model (scoped/favorite pin or catalog). */\n\tserviceTier: ServiceTier | undefined;\n\tisProjectTrusted(): boolean;\n\tnotify(message: string, level: \"info\" | \"warning\" | \"error\"): void;\n\tsetSessionModel(model: Model<Api>): Promise<boolean>;\n\tsetSessionFastMode(enabled: boolean): void;\n}\n\nexport interface FastModeResult {\n\t/** Fast mode state after the call; unchanged when the request was refused. */\n\tenabled: boolean;\n\t/** False when the request was refused (non-Codex model, active pin, failed model switch). */\n\tapplied: boolean;\n\t/** The just-persisted tier (or the unchanged one when refused); `\"priority\"` on, `\"auto\"` off. */\n\trecordedTier: ServiceTier;\n\tmessage: string;\n}\n\n/**\n * Whether the active model's priority tier is PINNED by the user's model selection (a scoped\n * or favorite `provider/id:priority` pattern) rather than inherited from the catalog.\n *\n * `ctx.serviceTier` merges both sources, so a priority the catalog does not explain must have\n * come from a `:priority` decorator; checking `scopedModels` alone would miss favorite pins,\n * which the session resolves the same way.\n */\nfunction hasPriorityPin(ctx: FastModeContext, model: Model<Api>): boolean {\n\tif (ctx.serviceTier !== PRIORITY_TIER) return false;\n\treturn ctx.modelRegistry.getServiceTier(model) !== PRIORITY_TIER;\n}\n\n/**\n * Single entry point for turning fast mode on/off: the `/fast` command and any other surface\n * (RPC) must go through this so persistence and `-fast` key normalization exist exactly once.\n *\n * `off` persists an explicit `\"auto\"` rather than deleting the key: deletion re-inherits the\n * catalog/global tier, which is what the user just asked to turn off.\n */\nexport async function applyFastMode(ctx: FastModeContext, enabled: boolean): Promise<FastModeResult> {\n\tconst model = ctx.model;\n\tif (model?.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\tconst message = \"Fast mode is only available for OpenAI Codex models.\";\n\t\tctx.notify(message, \"warning\");\n\t\treturn { enabled: false, applied: false, recordedTier: \"auto\", message };\n\t}\n\n\tif (!enabled && hasPriorityPin(ctx, model)) {\n\t\t// The pin outranks the memory, so writing \"auto\" here would be a silent no-op on the wire.\n\t\tconst message = \"Fast mode is fixed by the active model selection's priority tier.\";\n\t\tctx.notify(message, \"info\");\n\t\t// The pin keeps the model on priority no matter what the memory says, so that is the honest\n\t\t// tier to report; nothing was written, and no caller reads this field when `applied` is false.\n\t\treturn { enabled: true, applied: false, recordedTier: PRIORITY_TIER, message };\n\t}\n\n\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, model);\n\tconst tier: ServiceTier = enabled ? PRIORITY_TIER : \"auto\";\n\tsettingsManager.setModelServiceTier(memoryModel.provider, memoryModel.id, tier);\n\tawait settingsManager.flush();\n\n\tconst baseModel = findBaseModel(ctx.modelRegistry, model);\n\tconst targetModel = enabled ? findFastModel(ctx.modelRegistry, model) : baseModel;\n\tif (targetModel) {\n\t\tif (!(await ctx.setSessionModel(targetModel))) {\n\t\t\tconst message = `Could not switch to ${targetModel.provider}/${targetModel.id}.`;\n\t\t\tctx.notify(message, \"error\");\n\t\t\treturn { enabled: baseModel === undefined, applied: false, recordedTier: \"auto\", message };\n\t\t}\n\t}\n\n\tctx.setSessionFastMode(enabled);\n\tconst labelModel = targetModel ?? model;\n\tconst message = `Fast mode ${enabled ? \"enabled\" : \"disabled\"}: ${labelModel.id}`;\n\tctx.notify(message, \"info\");\n\treturn { enabled, applied: true, recordedTier: tier, message };\n}\n\nfunction toCompletions(values: readonly string[], prefix: string): AutocompleteItem[] | null {\n\tconst matches = values.filter((value) => value.startsWith(prefix.trim()));\n\treturn matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;\n}\n\nfunction toFastModeContext(pi: ExtensionAPI, ctx: ExtensionCommandContext): FastModeContext {\n\treturn {\n\t\tcwd: ctx.cwd,\n\t\tagentDir: ctx.agentDir,\n\t\tmodel: ctx.model,\n\t\tmodelRegistry: ctx.modelRegistry,\n\t\tserviceTier: ctx.serviceTier,\n\t\tisProjectTrusted: () => ctx.isProjectTrusted(),\n\t\tnotify: (message, level) => ctx.ui.notify(message, level),\n\t\tsetSessionModel: (model) => pi.setSessionModel(model),\n\t\tsetSessionFastMode: (enabled) => pi.setSessionFastMode(enabled),\n\t};\n}\n\nexport default function serviceTierExtension(pi: ExtensionAPI): void {\n\tlet settingsServiceTier: ServiceTier | undefined;\n\t/**\n\t * Session-level fast mode for Codex models that have no `-fast` catalog sibling.\n\t *\n\t * The catalog generator emits `-fast` priority variants only for the direct\n\t * `openai` provider, so `openai-codex` models never have a switch target. The\n\t * ChatGPT backend still offers the tier to subscriptions:\n\t * `chatgpt.com/backend-api/codex/models` advertises a `priority` service tier\n\t * labelled \"Fast\" (\"1.5x speed, increased usage\")\n\t * for gpt-5.4/5.5/5.6-*, and the first-party Codex CLI sends\n\t * `service_tier: \"priority\"` on subscription OAuth. The response echo is not a\n\t * confirmation channel — it reads `auto`/`default` whether or not a tier was\n\t * sent — so this toggle drives the request field directly. See issue #545.\n\t */\n\tlet sessionFastMode = false;\n\t/**\n\t * The per-model memory tier `/fast` last wrote or read for the active base model this\n\t * session. Lives only to suppress an inherited priority when the user turns fast off in\n\t * the SAME session: the host's resolved `ctx.serviceTier` is recomputed on model switch,\n\t * not on a memory write, so without this an in-session `/fast off` (no model swap) would\n\t * keep sending `priority` until a restart. Scoped to the active base key (re-derived on\n\t * model_select for the incoming model) so it never leaks across models.\n\t */\n\tlet liveMemoryTier: ServiceTier | undefined;\n\tlet liveMemoryKey: string | undefined;\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\t\tsettingsServiceTier = settingsManager.getOpenAIServiceTier();\n\n\t\tconst model = ctx.model;\n\t\tif (model?.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\t\tsessionFastMode = false;\n\t\t\tpi.setSessionFastMode(false);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fast mode is remembered per model, so a new session inherits the user's last choice for\n\t\t// THIS model instead of silently starting off. A model whose catalog/scoped tier is already\n\t\t// `priority` (a models.json priority entry, or a `-fast`-less codex model) is treated as\n\t\t// fast-active when nothing is remembered; an explicit remembered `\"auto\"` (`/fast off`)\n\t\t// wins over that inheritance. Malformed values read back as undefined.\n\t\t//\n\t\t// The flag is derived from the POST-swap model: a `-fast` catalog variant swaps down to\n\t\t// its base before this reads `ctx.serviceTier`, so inheritance is judged on the model the\n\t\t// user actually ends up on.\n\t\tconst remembered = getRememberedServiceTier(settingsManager, ctx.modelRegistry, model);\n\n\t\tconst baseModel = findBaseModel(ctx.modelRegistry, model);\n\t\tif (baseModel) {\n\t\t\tawait pi.setSessionModel(baseModel);\n\t\t}\n\n\t\tsessionFastMode = remembered === PRIORITY_TIER || (remembered === undefined && ctx.serviceTier === PRIORITY_TIER);\n\t\tpi.setSessionFastMode(sessionFastMode);\n\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, model);\n\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\tliveMemoryTier = remembered;\n\t});\n\n\tpi.on(\"model_select\", (event, ctx) => {\n\t\t// A model switch changes the base key the memory lives under, so the live tier is RE-DERIVED\n\t\t// for the incoming model instead of merely dropped: dropping it would leave a remembered\n\t\t// \"auto\" unable to suppress a catalog-inherited priority after switching away and back in one\n\t\t// session, silently re-sending the tier `/fast off` turned off. Per-key scoping is preserved —\n\t\t// each model reads ITS OWN memory, never the previous model's.\n\t\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, event.model);\n\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\tliveMemoryTier = getRememberedServiceTier(settingsManager, ctx.modelRegistry, event.model);\n\n\t\t// `service_tier` is an OpenAI-family request field, so hopping to a non-Codex model leaves the\n\t\t// intent with nothing to act on: `before_provider_request` already refuses to emit the tier\n\t\t// there, but the session flag kept `isFastModeActive()` (and with it the RPC `fastMode` and the\n\t\t// lightning indicator) claiming fast for a model that can never be served at that tier.\n\t\t//\n\t\t// Codex -> Codex is deliberately untouched: fast mode is a SESSION intent that survives a\n\t\t// mid-session Codex switch (see service-tier-extension.test.ts \"keeps session fast mode on\n\t\t// across a mid-session switch to another Codex model\"), and an incoming model's remembered\n\t\t// \"auto\" is honored on the wire by `liveMemoryTier` below, not by clearing the flag here.\n\t\tif (sessionFastMode && event.model.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\t\tsessionFastMode = false;\n\t\t\tpi.setSessionFastMode(false);\n\t\t}\n\t});\n\n\tpi.registerCommand(\"fast\", {\n\t\tdescription: \"Turn OpenAI Codex fast mode on or off for the current model\",\n\t\targumentHint: \"[on|off]\",\n\t\tgetArgumentCompletions: (prefix) => toCompletions(FAST_ARGUMENTS, prefix),\n\t\thandler: async (args, ctx) => {\n\t\t\tconst argument = args.trim().toLowerCase();\n\t\t\tif (argument !== \"\" && !FAST_ARGUMENTS.includes(argument as FastArgument)) {\n\t\t\t\tctx.ui.notify(FAST_USAGE, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// No argument keeps the established toggle UX.\n\t\t\tconst active = sessionFastMode || ctx.serviceTier === PRIORITY_TIER;\n\t\t\tconst enabled = argument === \"\" ? !active : argument === \"on\";\n\t\t\tconst result = await applyFastMode(toFastModeContext(pi, ctx), enabled);\n\t\t\tsessionFastMode = result.enabled;\n\t\t\t// Track this session's own write AFTER applyFastMode's internal model switch (which fires\n\t\t\t// model_select and clears the live tier). Keyed to the active base model so a later switch\n\t\t\t// cannot carry it across models.\n\t\t\tif (result.applied && ctx.model) {\n\t\t\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, ctx.model);\n\t\t\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\t\t\tliveMemoryTier = result.recordedTier;\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.on(\"before_provider_request\", (event, ctx) => {\n\t\tlet effectiveServiceTier: ServiceTier | undefined;\n\t\tif (ctx.model?.api === OPENAI_CODEX_RESPONSES_API) {\n\t\t\tif (sessionFastMode) {\n\t\t\t\teffectiveServiceTier = PRIORITY_TIER;\n\t\t\t} else {\n\t\t\t\tconst memoryModel = ctx.model ? resolveServiceTierMemoryModel(ctx.modelRegistry, ctx.model) : undefined;\n\t\t\t\tconst activeBaseKey = memoryModel ? `${memoryModel.provider}/${memoryModel.id}` : undefined;\n\t\t\t\t// Same-session /fast off: the host's resolved ctx.serviceTier is only recomputed on a\n\t\t\t\t// model switch, so an inherited priority would otherwise keep emitting until a restart.\n\t\t\t\t// Only a CATALOG-explained priority may be suppressed: a priority the catalog does not\n\t\t\t\t// explain is an explicit scoped/favorite `:priority` pin, which outranks the memory\n\t\t\t\t// (same discriminator `applyFastMode` uses to refuse `/fast off` under a pin).\n\t\t\t\tconst catalogExplainsPriority =\n\t\t\t\t\tctx.model !== undefined && ctx.modelRegistry.getServiceTier(ctx.model) === PRIORITY_TIER;\n\t\t\t\tif (\n\t\t\t\t\tliveMemoryTier === \"auto\" &&\n\t\t\t\t\tactiveBaseKey === liveMemoryKey &&\n\t\t\t\t\tctx.serviceTier === PRIORITY_TIER &&\n\t\t\t\t\tcatalogExplainsPriority\n\t\t\t\t) {\n\t\t\t\t\teffectiveServiceTier = undefined;\n\t\t\t\t} else {\n\t\t\t\t\teffectiveServiceTier = ctx.serviceTier;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\teffectiveServiceTier = ctx.serviceTier ?? settingsServiceTier;\n\t\t}\n\t\treturn addServiceTierToPayload(ctx.model?.api, event.payload, effectiveServiceTier);\n\t});\n}\n"]}
1
+ {"version":3,"file":"service-tier.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/service-tier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\nimport type { ModelRegistry } from \"../../model-registry.ts\";\nimport { SettingsManager } from \"../../settings-manager.ts\";\nimport type { ExtensionAPI, ExtensionCommandContext, ServiceTier } from \"../types.ts\";\n\nexport type { ServiceTier };\n\ntype ProviderPayload = Record<string, unknown>;\n\nconst OPENAI_CODEX_RESPONSES_API = \"openai-codex-responses\";\nexport const CODEX_RESPONSES_API = OPENAI_CODEX_RESPONSES_API;\nconst FAST_MODEL_SUFFIX = \"-fast\";\nconst PRIORITY_TIER: ServiceTier = \"priority\";\nconst SERVICE_TIER_APIS: ReadonlySet<Api> = new Set([\"openai-responses\", OPENAI_CODEX_RESPONSES_API]);\nconst FAST_ARGUMENTS = [\"on\", \"off\"] as const;\nconst FAST_USAGE = \"Usage: /fast [on|off]\";\n\ntype FastArgument = (typeof FAST_ARGUMENTS)[number];\n\nfunction isRecord(value: unknown): value is ProviderPayload {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nexport function addServiceTierToPayload(api: Api | undefined, payload: unknown, serviceTier?: ServiceTier): unknown {\n\tif (!api || !SERVICE_TIER_APIS.has(api) || !serviceTier) {\n\t\treturn payload;\n\t}\n\n\tif (!isRecord(payload) || payload.service_tier !== undefined) {\n\t\treturn payload;\n\t}\n\n\treturn {\n\t\t...payload,\n\t\tservice_tier: serviceTier,\n\t};\n}\n\nfunction getRequestModelId(modelRegistry: ModelRegistry, model: Model<Api>): string {\n\treturn modelRegistry.getUpstreamModelId(model) ?? model.id;\n}\n\nfunction isCompatibleFastVariant(modelRegistry: ModelRegistry, baseModel: Model<Api>, fastModel: Model<Api>): boolean {\n\treturn (\n\t\tfastModel.provider === baseModel.provider &&\n\t\tfastModel.api === baseModel.api &&\n\t\tmodelRegistry.getServiceTier(fastModel) === \"priority\" &&\n\t\tgetRequestModelId(modelRegistry, fastModel) === getRequestModelId(modelRegistry, baseModel)\n\t);\n}\n\nfunction findBaseModel(modelRegistry: ModelRegistry, fastModel: Model<Api>): Model<Api> | undefined {\n\tif (!fastModel.id.endsWith(FAST_MODEL_SUFFIX)) {\n\t\treturn undefined;\n\t}\n\n\tconst baseModelId = fastModel.id.slice(0, -FAST_MODEL_SUFFIX.length);\n\tconst baseModel = modelRegistry.find(fastModel.provider, baseModelId);\n\treturn baseModel && isCompatibleFastVariant(modelRegistry, baseModel, fastModel) ? baseModel : undefined;\n}\n\nfunction findFastModel(modelRegistry: ModelRegistry, baseModel: Model<Api>): Model<Api> | undefined {\n\tif (baseModel.id.endsWith(FAST_MODEL_SUFFIX)) {\n\t\treturn undefined;\n\t}\n\n\tconst fastModel = modelRegistry.find(baseModel.provider, `${baseModel.id}${FAST_MODEL_SUFFIX}`);\n\treturn fastModel && isCompatibleFastVariant(modelRegistry, baseModel, fastModel) ? fastModel : undefined;\n}\n\n/**\n * The model that owns this model's persisted service-tier preference.\n *\n * A `-fast` catalog variant and its base model are one model to the user (`/fast` just swaps\n * between them), so both must read and write ONE key. Anything else can hold contradictory\n * preferences for the same choice.\n */\nexport function resolveServiceTierMemoryModel(modelRegistry: ModelRegistry, model: Model<Api>): Model<Api> {\n\treturn findBaseModel(modelRegistry, model) ?? model;\n}\n\n/** Remembered service tier for this model, normalized onto its base-model key. */\nexport function getRememberedServiceTier(\n\tsettingsManager: SettingsManager,\n\tmodelRegistry: ModelRegistry,\n\tmodel: Model<Api>,\n): ServiceTier | undefined {\n\tconst memoryModel = resolveServiceTierMemoryModel(modelRegistry, model);\n\treturn settingsManager.getModelServiceTier(memoryModel.provider, memoryModel.id);\n}\n\n/** Host capabilities `applyFastMode` needs; satisfied by an extension context plus `pi`. */\nexport interface FastModeContext {\n\tcwd: string;\n\tagentDir: string;\n\tmodel: Model<Api> | undefined;\n\tmodelRegistry: ModelRegistry;\n\t/** Tier the host resolved for the active model (scoped/favorite pin or catalog). */\n\tserviceTier: ServiceTier | undefined;\n\tisProjectTrusted(): boolean;\n\tnotify(message: string, level: \"info\" | \"warning\" | \"error\"): void;\n\tsetSessionModel(model: Model<Api>): Promise<boolean>;\n\tsetSessionFastMode(enabled: boolean): void;\n}\n\nexport interface FastModeResult {\n\t/** Fast mode state after the call; unchanged when the request was refused. */\n\tenabled: boolean;\n\t/** False when the request was refused (non-Codex model, active pin, failed model switch). */\n\tapplied: boolean;\n\t/** The just-persisted tier (or the unchanged one when refused); `\"priority\"` on, `\"auto\"` off. */\n\trecordedTier: ServiceTier;\n\tmessage: string;\n}\n\n/**\n * Whether the active model's priority tier is PINNED by the user's model selection (a scoped\n * or favorite `provider/id:priority` pattern) rather than inherited from the catalog.\n *\n * `ctx.serviceTier` merges both sources, so a priority the catalog does not explain must have\n * come from a `:priority` decorator; checking `scopedModels` alone would miss favorite pins,\n * which the session resolves the same way.\n */\nfunction hasPriorityPin(ctx: FastModeContext, model: Model<Api>): boolean {\n\tif (ctx.serviceTier !== PRIORITY_TIER) return false;\n\treturn ctx.modelRegistry.getServiceTier(model) !== PRIORITY_TIER;\n}\n\n/**\n * Single entry point for turning fast mode on/off: the `/fast` command and any other surface\n * (RPC) must go through this so persistence and `-fast` key normalization exist exactly once.\n *\n * `off` persists an explicit `\"auto\"` rather than deleting the key: deletion re-inherits the\n * catalog/global tier, which is what the user just asked to turn off.\n */\nexport async function applyFastMode(ctx: FastModeContext, enabled: boolean): Promise<FastModeResult> {\n\tconst model = ctx.model;\n\tif (model?.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\tconst message = \"Fast mode is only available for OpenAI Codex models.\";\n\t\tctx.notify(message, \"warning\");\n\t\treturn { enabled: false, applied: false, recordedTier: \"auto\", message };\n\t}\n\n\tif (!enabled && hasPriorityPin(ctx, model)) {\n\t\t// The pin outranks the memory, so writing \"auto\" here would be a silent no-op on the wire.\n\t\tconst message = \"Fast mode is fixed by the active model selection's priority tier.\";\n\t\tctx.notify(message, \"info\");\n\t\t// The pin keeps the model on priority no matter what the memory says, so that is the honest\n\t\t// tier to report; nothing was written, and no caller reads this field when `applied` is false.\n\t\treturn { enabled: true, applied: false, recordedTier: PRIORITY_TIER, message };\n\t}\n\n\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, model);\n\tconst tier: ServiceTier = enabled ? PRIORITY_TIER : \"auto\";\n\tsettingsManager.setModelServiceTier(memoryModel.provider, memoryModel.id, tier);\n\tawait settingsManager.flush();\n\n\tconst baseModel = findBaseModel(ctx.modelRegistry, model);\n\tconst targetModel = enabled ? findFastModel(ctx.modelRegistry, model) : baseModel;\n\tif (targetModel) {\n\t\tif (!(await ctx.setSessionModel(targetModel))) {\n\t\t\tconst message = `Could not switch to ${targetModel.provider}/${targetModel.id}.`;\n\t\t\tctx.notify(message, \"error\");\n\t\t\treturn { enabled: baseModel === undefined, applied: false, recordedTier: \"auto\", message };\n\t\t}\n\t}\n\n\tctx.setSessionFastMode(enabled);\n\tconst labelModel = targetModel ?? model;\n\tconst message = `Fast mode ${enabled ? \"enabled\" : \"disabled\"}: ${labelModel.id}`;\n\tctx.notify(message, \"info\");\n\treturn { enabled, applied: true, recordedTier: tier, message };\n}\n\nfunction toCompletions(values: readonly string[], prefix: string): AutocompleteItem[] | null {\n\tconst matches = values.filter((value) => value.startsWith(prefix.trim()));\n\treturn matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;\n}\n\nfunction toFastModeContext(pi: ExtensionAPI, ctx: ExtensionCommandContext): FastModeContext {\n\treturn {\n\t\tcwd: ctx.cwd,\n\t\tagentDir: ctx.agentDir,\n\t\tmodel: ctx.model,\n\t\tmodelRegistry: ctx.modelRegistry,\n\t\tserviceTier: ctx.serviceTier,\n\t\tisProjectTrusted: () => ctx.isProjectTrusted(),\n\t\tnotify: (message, level) => ctx.ui.notify(message, level),\n\t\tsetSessionModel: (model) => pi.setSessionModel(model),\n\t\tsetSessionFastMode: (enabled) => pi.setSessionFastMode(enabled),\n\t};\n}\n\nexport default function serviceTierExtension(pi: ExtensionAPI): void {\n\tlet settingsServiceTier: ServiceTier | undefined;\n\t/**\n\t * Session-level fast mode for Codex models that have no `-fast` catalog sibling.\n\t *\n\t * The catalog generator emits `-fast` priority variants only for the direct\n\t * `openai` provider, so `openai-codex` models never have a switch target. The\n\t * ChatGPT backend still offers the tier to subscriptions:\n\t * `chatgpt.com/backend-api/codex/models` advertises a `priority` service tier\n\t * labelled \"Fast\" (\"1.5x speed, increased usage\")\n\t * for gpt-5.4/5.5/5.6-*, and the first-party Codex CLI sends\n\t * `service_tier: \"priority\"` on subscription OAuth. The response echo is not a\n\t * confirmation channel — it reads `auto`/`default` whether or not a tier was\n\t * sent — so this toggle drives the request field directly. See issue #545.\n\t */\n\tlet sessionFastMode = false;\n\t/**\n\t * The per-model memory tier `/fast` last wrote or read for the active base model this\n\t * session. Lives only to suppress an inherited priority when the user turns fast off in\n\t * the SAME session: the host's resolved `ctx.serviceTier` is recomputed on model switch,\n\t * not on a memory write, so without this an in-session `/fast off` (no model swap) would\n\t * keep sending `priority` until a restart. Scoped to the active base key (re-derived on\n\t * model_select for the incoming model) so it never leaks across models.\n\t */\n\tlet liveMemoryTier: ServiceTier | undefined;\n\tlet liveMemoryKey: string | undefined;\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\t\tsettingsServiceTier = settingsManager.getOpenAIServiceTier();\n\n\t\tconst model = ctx.model;\n\t\tif (model?.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\t\tsessionFastMode = false;\n\t\t\tpi.setSessionFastMode(false);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fast mode is remembered per model, so a new session inherits the user's last choice for\n\t\t// THIS model instead of silently starting off. A model whose catalog/scoped tier is already\n\t\t// `priority` (a models.json priority entry, or a `-fast`-less codex model) is treated as\n\t\t// fast-active when nothing is remembered; an explicit remembered `\"auto\"` (`/fast off`)\n\t\t// wins over that inheritance. Malformed values read back as undefined.\n\t\t//\n\t\t// The flag is derived from the POST-swap model for ordinary starts, but an explicit `-fast`\n\t\t// selection is an explicit fast-mode request even though its catalog tier disappears with the\n\t\t// swap to the base model. Preserve the requested level across that swap as well; the session\n\t\t// setter clamps it against the base model's thinkingLevelMap without changing global defaults.\n\t\tconst remembered = getRememberedServiceTier(settingsManager, ctx.modelRegistry, model);\n\t\tconst requestedThinkingLevel = pi.getThinkingLevel();\n\n\t\tconst baseModel = findBaseModel(ctx.modelRegistry, model);\n\t\tif (baseModel) {\n\t\t\tawait pi.setSessionModel(baseModel);\n\t\t\tif (requestedThinkingLevel !== undefined) {\n\t\t\t\tpi.setSessionThinkingLevel(requestedThinkingLevel);\n\t\t\t}\n\t\t}\n\n\t\tsessionFastMode =\n\t\t\tbaseModel !== undefined ||\n\t\t\tremembered === PRIORITY_TIER ||\n\t\t\t(remembered === undefined && ctx.serviceTier === PRIORITY_TIER);\n\t\tpi.setSessionFastMode(sessionFastMode);\n\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, model);\n\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\tliveMemoryTier = remembered;\n\t});\n\n\tpi.on(\"model_select\", (event, ctx) => {\n\t\t// A model switch changes the base key the memory lives under, so the live tier is RE-DERIVED\n\t\t// for the incoming model instead of merely dropped: dropping it would leave a remembered\n\t\t// \"auto\" unable to suppress a catalog-inherited priority after switching away and back in one\n\t\t// session, silently re-sending the tier `/fast off` turned off. Per-key scoping is preserved —\n\t\t// each model reads ITS OWN memory, never the previous model's.\n\t\tconst settingsManager = SettingsManager.create(ctx.cwd, ctx.agentDir, { projectTrusted: ctx.isProjectTrusted() });\n\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, event.model);\n\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\tliveMemoryTier = getRememberedServiceTier(settingsManager, ctx.modelRegistry, event.model);\n\n\t\t// `service_tier` is an OpenAI-family request field, so hopping to a non-Codex model leaves the\n\t\t// intent with nothing to act on: `before_provider_request` already refuses to emit the tier\n\t\t// there, but the session flag kept `isFastModeActive()` (and with it the RPC `fastMode` and the\n\t\t// lightning indicator) claiming fast for a model that can never be served at that tier.\n\t\t//\n\t\t// Codex -> Codex is deliberately untouched: fast mode is a SESSION intent that survives a\n\t\t// mid-session Codex switch (see service-tier-extension.test.ts \"keeps session fast mode on\n\t\t// across a mid-session switch to another Codex model\"), and an incoming model's remembered\n\t\t// \"auto\" is honored on the wire by `liveMemoryTier` below, not by clearing the flag here.\n\t\tif (sessionFastMode && event.model.api !== OPENAI_CODEX_RESPONSES_API) {\n\t\t\tsessionFastMode = false;\n\t\t\tpi.setSessionFastMode(false);\n\t\t}\n\t});\n\n\tpi.registerCommand(\"fast\", {\n\t\tdescription: \"Turn OpenAI Codex fast mode on or off for the current model\",\n\t\targumentHint: \"[on|off]\",\n\t\tgetArgumentCompletions: (prefix) => toCompletions(FAST_ARGUMENTS, prefix),\n\t\thandler: async (args, ctx) => {\n\t\t\tconst argument = args.trim().toLowerCase();\n\t\t\tif (argument !== \"\" && !FAST_ARGUMENTS.includes(argument as FastArgument)) {\n\t\t\t\tctx.ui.notify(FAST_USAGE, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// No argument keeps the established toggle UX.\n\t\t\tconst active = sessionFastMode || ctx.serviceTier === PRIORITY_TIER;\n\t\t\tconst enabled = argument === \"\" ? !active : argument === \"on\";\n\t\t\tconst result = await applyFastMode(toFastModeContext(pi, ctx), enabled);\n\t\t\tsessionFastMode = result.enabled;\n\t\t\t// Track this session's own write AFTER applyFastMode's internal model switch (which fires\n\t\t\t// model_select and clears the live tier). Keyed to the active base model so a later switch\n\t\t\t// cannot carry it across models.\n\t\t\tif (result.applied && ctx.model) {\n\t\t\t\tconst memoryModel = resolveServiceTierMemoryModel(ctx.modelRegistry, ctx.model);\n\t\t\t\tliveMemoryKey = `${memoryModel.provider}/${memoryModel.id}`;\n\t\t\t\tliveMemoryTier = result.recordedTier;\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.on(\"before_provider_request\", (event, ctx) => {\n\t\tlet effectiveServiceTier: ServiceTier | undefined;\n\t\tif (ctx.model?.api === OPENAI_CODEX_RESPONSES_API) {\n\t\t\tif (sessionFastMode) {\n\t\t\t\teffectiveServiceTier = PRIORITY_TIER;\n\t\t\t} else {\n\t\t\t\tconst memoryModel = ctx.model ? resolveServiceTierMemoryModel(ctx.modelRegistry, ctx.model) : undefined;\n\t\t\t\tconst activeBaseKey = memoryModel ? `${memoryModel.provider}/${memoryModel.id}` : undefined;\n\t\t\t\t// Same-session /fast off: the host's resolved ctx.serviceTier is only recomputed on a\n\t\t\t\t// model switch, so an inherited priority would otherwise keep emitting until a restart.\n\t\t\t\t// Only a CATALOG-explained priority may be suppressed: a priority the catalog does not\n\t\t\t\t// explain is an explicit scoped/favorite `:priority` pin, which outranks the memory\n\t\t\t\t// (same discriminator `applyFastMode` uses to refuse `/fast off` under a pin).\n\t\t\t\tconst catalogExplainsPriority =\n\t\t\t\t\tctx.model !== undefined && ctx.modelRegistry.getServiceTier(ctx.model) === PRIORITY_TIER;\n\t\t\t\tif (\n\t\t\t\t\tliveMemoryTier === \"auto\" &&\n\t\t\t\t\tactiveBaseKey === liveMemoryKey &&\n\t\t\t\t\tctx.serviceTier === PRIORITY_TIER &&\n\t\t\t\t\tcatalogExplainsPriority\n\t\t\t\t) {\n\t\t\t\t\teffectiveServiceTier = undefined;\n\t\t\t\t} else {\n\t\t\t\t\teffectiveServiceTier = ctx.serviceTier;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\teffectiveServiceTier = ctx.serviceTier ?? settingsServiceTier;\n\t\t}\n\t\treturn addServiceTierToPayload(ctx.model?.api, event.payload, effectiveServiceTier);\n\t});\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `restartable-command` durability class: the restore handler that brings a persistent
3
+ * command monitor back after a restart. It re-spawns the saved command EXACTLY ONCE, in the
4
+ * saved working directory, with NO timeout (a persistent watch has no deadline), and
5
+ * re-registers it under the SAVED `mon_` id so the stable handle the agent already knows
6
+ * keeps resolving. Nothing the pre-restart PTY produced is replayed — no output is persisted
7
+ * anywhere, so a restored watch starts from an empty buffer by construction.
8
+ */
9
+ import type { MonitorRegistry } from "./monitor-registry.ts";
10
+ import { type RestoreHandler } from "./restore.ts";
11
+ import type { TerminalToolContext } from "./tools/context.ts";
12
+ import { spawnCommandSession } from "./tools/spawn.ts";
13
+ export interface RestartableCommandDeps {
14
+ /** Spawn target: supplies the terminal manager, shell config, env and default geometry. */
15
+ readonly ctx: TerminalToolContext;
16
+ /** The live registry this generation owns; the restored watch is registered here. */
17
+ readonly registry: MonitorRegistry;
18
+ /** Seam for tests and future callers; defaults to the real PTY spawn. */
19
+ readonly spawn?: typeof spawnCommandSession;
20
+ /** Directory existence probe; defaults to a real stat of the saved cwd. */
21
+ readonly directoryExists?: (path: string) => Promise<boolean>;
22
+ /** Called after a successful restore with the stable id and its fresh runtime id. */
23
+ readonly onRestored?: (monitorId: string, runtimeId: string) => void;
24
+ }
25
+ /**
26
+ * Build the `restartable-command` restore handler. Every guard rejects BEFORE spawning, so a
27
+ * rejected entry costs zero PTY sessions; a spawn failure is reported lost rather than thrown,
28
+ * because restore must never fail the startup path for one unrecoverable watch.
29
+ */
30
+ export declare function createRestartableCommandHandler(deps: RestartableCommandDeps): RestoreHandler;
31
+ //# sourceMappingURL=durable-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-command.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/durable-command.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAmD,MAAM,cAAc,CAAC;AAGpG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,WAAW,sBAAsB;IACtC,2FAA2F;IAC3F,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;IAClC,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,mBAAmB,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,qFAAqF;IACrF,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAwCD;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,sBAAsB,GAAG,cAAc,CAoC5F"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The `restartable-command` durability class: the restore handler that brings a persistent
3
+ * command monitor back after a restart. It re-spawns the saved command EXACTLY ONCE, in the
4
+ * saved working directory, with NO timeout (a persistent watch has no deadline), and
5
+ * re-registers it under the SAVED `mon_` id so the stable handle the agent already knows
6
+ * keeps resolving. Nothing the pre-restart PTY produced is replayed — no output is persisted
7
+ * anywhere, so a restored watch starts from an empty buffer by construction.
8
+ */
9
+ import { stat } from "node:fs/promises";
10
+ import { isAbsolute } from "node:path";
11
+ import { reapplyPersistedMute } from "./restore.js";
12
+ import { DEFAULT_COLS, DEFAULT_ROWS } from "./shared.js";
13
+ import { spawnCommandSession } from "./tools/spawn.js";
14
+ const LOST = { outcome: "lost" };
15
+ async function defaultDirectoryExists(path) {
16
+ try {
17
+ return (await stat(path)).isDirectory();
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function compileFilter(source) {
24
+ if (source === undefined)
25
+ return undefined;
26
+ try {
27
+ return new RegExp(source);
28
+ }
29
+ catch {
30
+ // A filter that no longer compiles must not sink the watch: restore it unfiltered.
31
+ return undefined;
32
+ }
33
+ }
34
+ function dimension(value, fallback) {
35
+ return value !== undefined && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
36
+ }
37
+ /**
38
+ * Decide whether a manifest entry is a restorable persistent command watch. Only the
39
+ * command runtime kind qualifies, only when it was created `persistent: true` (a
40
+ * non-persistent watch had a deadline and is deliberately not resurrected), and only
41
+ * when it actually carries a command and an absolute cwd to spawn it in.
42
+ */
43
+ function restorable(monitor) {
44
+ if (monitor.runtimeKind !== "command" || !monitor.persistent)
45
+ return undefined;
46
+ const { command, cwd } = monitor;
47
+ if (command === undefined || command.length === 0)
48
+ return undefined;
49
+ if (cwd === undefined || !isAbsolute(cwd))
50
+ return undefined;
51
+ return { command, cwd };
52
+ }
53
+ /**
54
+ * Build the `restartable-command` restore handler. Every guard rejects BEFORE spawning, so a
55
+ * rejected entry costs zero PTY sessions; a spawn failure is reported lost rather than thrown,
56
+ * because restore must never fail the startup path for one unrecoverable watch.
57
+ */
58
+ export function createRestartableCommandHandler(deps) {
59
+ const spawn = deps.spawn ?? spawnCommandSession;
60
+ const directoryExists = deps.directoryExists ?? defaultDirectoryExists;
61
+ return async (monitor) => {
62
+ const target = restorable(monitor);
63
+ if (target === undefined)
64
+ return LOST;
65
+ if (!(await directoryExists(target.cwd)))
66
+ return LOST;
67
+ let spawned;
68
+ try {
69
+ spawned = await spawn(deps.ctx, {
70
+ command: target.command,
71
+ cols: dimension(deps.ctx.defaultCols, DEFAULT_COLS),
72
+ rows: dimension(deps.ctx.defaultRows, DEFAULT_ROWS),
73
+ cwd: target.cwd,
74
+ // Persistent watches carry no deadline: omit timeoutMs entirely.
75
+ });
76
+ }
77
+ catch {
78
+ return LOST;
79
+ }
80
+ deps.registry.register({
81
+ id: spawned.id,
82
+ monitorId: monitor.monitorId,
83
+ description: monitor.description,
84
+ runtime: spawned.runtime,
85
+ filter: compileFilter(monitor.filter),
86
+ // The persisted deadline rides through verbatim; a restore never extends it.
87
+ ...(monitor.expiresAt !== null ? { expiresAt: monitor.expiresAt } : {}),
88
+ });
89
+ deps.ctx.manager.bindMonitorId(monitor.monitorId, spawned.id);
90
+ deps.onRestored?.(monitor.monitorId, spawned.id);
91
+ // A persisted mute is re-applied by the FRESH runtime id; the registry resolves
92
+ // records by runtime id only, so the mon_ id would silently no-op here.
93
+ return { outcome: reapplyPersistedMute(deps.registry, monitor, spawned.id) };
94
+ };
95
+ }
96
+ //# sourceMappingURL=durable-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-command.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/durable-command.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `restartable-command` durability class: the restore handler that brings a persistent\n * command monitor back after a restart. It re-spawns the saved command EXACTLY ONCE, in the\n * saved working directory, with NO timeout (a persistent watch has no deadline), and\n * re-registers it under the SAVED `mon_` id so the stable handle the agent already knows\n * keeps resolving. Nothing the pre-restart PTY produced is replayed — no output is persisted\n * anywhere, so a restored watch starts from an empty buffer by construction.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { isAbsolute } from \"node:path\";\nimport type { MonitorRegistry } from \"./monitor-registry.ts\";\nimport { type RestoreHandler, type RestoreHandlerResult, reapplyPersistedMute } from \"./restore.ts\";\nimport { DEFAULT_COLS, DEFAULT_ROWS } from \"./shared.ts\";\nimport type { ManifestMonitor } from \"./terminal-manifest.ts\";\nimport type { TerminalToolContext } from \"./tools/context.ts\";\nimport { spawnCommandSession } from \"./tools/spawn.ts\";\n\nexport interface RestartableCommandDeps {\n\t/** Spawn target: supplies the terminal manager, shell config, env and default geometry. */\n\treadonly ctx: TerminalToolContext;\n\t/** The live registry this generation owns; the restored watch is registered here. */\n\treadonly registry: MonitorRegistry;\n\t/** Seam for tests and future callers; defaults to the real PTY spawn. */\n\treadonly spawn?: typeof spawnCommandSession;\n\t/** Directory existence probe; defaults to a real stat of the saved cwd. */\n\treadonly directoryExists?: (path: string) => Promise<boolean>;\n\t/** Called after a successful restore with the stable id and its fresh runtime id. */\n\treadonly onRestored?: (monitorId: string, runtimeId: string) => void;\n}\n\nconst LOST: RestoreHandlerResult = { outcome: \"lost\" };\n\nasync function defaultDirectoryExists(path: string): Promise<boolean> {\n\ttry {\n\t\treturn (await stat(path)).isDirectory();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction compileFilter(source: string | undefined): RegExp | undefined {\n\tif (source === undefined) return undefined;\n\ttry {\n\t\treturn new RegExp(source);\n\t} catch {\n\t\t// A filter that no longer compiles must not sink the watch: restore it unfiltered.\n\t\treturn undefined;\n\t}\n}\n\nfunction dimension(value: number | undefined, fallback: number): number {\n\treturn value !== undefined && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;\n}\n\n/**\n * Decide whether a manifest entry is a restorable persistent command watch. Only the\n * command runtime kind qualifies, only when it was created `persistent: true` (a\n * non-persistent watch had a deadline and is deliberately not resurrected), and only\n * when it actually carries a command and an absolute cwd to spawn it in.\n */\nfunction restorable(monitor: ManifestMonitor): { command: string; cwd: string } | undefined {\n\tif (monitor.runtimeKind !== \"command\" || !monitor.persistent) return undefined;\n\tconst { command, cwd } = monitor;\n\tif (command === undefined || command.length === 0) return undefined;\n\tif (cwd === undefined || !isAbsolute(cwd)) return undefined;\n\treturn { command, cwd };\n}\n\n/**\n * Build the `restartable-command` restore handler. Every guard rejects BEFORE spawning, so a\n * rejected entry costs zero PTY sessions; a spawn failure is reported lost rather than thrown,\n * because restore must never fail the startup path for one unrecoverable watch.\n */\nexport function createRestartableCommandHandler(deps: RestartableCommandDeps): RestoreHandler {\n\tconst spawn = deps.spawn ?? spawnCommandSession;\n\tconst directoryExists = deps.directoryExists ?? defaultDirectoryExists;\n\treturn async (monitor: ManifestMonitor): Promise<RestoreHandlerResult> => {\n\t\tconst target = restorable(monitor);\n\t\tif (target === undefined) return LOST;\n\t\tif (!(await directoryExists(target.cwd))) return LOST;\n\n\t\tlet spawned: Awaited<ReturnType<typeof spawnCommandSession>>;\n\t\ttry {\n\t\t\tspawned = await spawn(deps.ctx, {\n\t\t\t\tcommand: target.command,\n\t\t\t\tcols: dimension(deps.ctx.defaultCols, DEFAULT_COLS),\n\t\t\t\trows: dimension(deps.ctx.defaultRows, DEFAULT_ROWS),\n\t\t\t\tcwd: target.cwd,\n\t\t\t\t// Persistent watches carry no deadline: omit timeoutMs entirely.\n\t\t\t});\n\t\t} catch {\n\t\t\treturn LOST;\n\t\t}\n\n\t\tdeps.registry.register({\n\t\t\tid: spawned.id,\n\t\t\tmonitorId: monitor.monitorId,\n\t\t\tdescription: monitor.description,\n\t\t\truntime: spawned.runtime,\n\t\t\tfilter: compileFilter(monitor.filter),\n\t\t\t// The persisted deadline rides through verbatim; a restore never extends it.\n\t\t\t...(monitor.expiresAt !== null ? { expiresAt: monitor.expiresAt } : {}),\n\t\t});\n\t\tdeps.ctx.manager.bindMonitorId(monitor.monitorId, spawned.id);\n\t\tdeps.onRestored?.(monitor.monitorId, spawned.id);\n\t\t// A persisted mute is re-applied by the FRESH runtime id; the registry resolves\n\t\t// records by runtime id only, so the mon_ id would silently no-op here.\n\t\treturn { outcome: reapplyPersistedMute(deps.registry, monitor, spawned.id) };\n\t};\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The `checkpointed-file` durability class: the restore handler that makes a persistent file
3
+ * watch survive a restart. On create the monitor tool persists the registry's own identity
4
+ * tuple (dev/ino/size/mtimeMs/digest/present) as the manifest checkpoint; here that checkpoint
5
+ * is compared ONCE against the file as it is now, and any change that happened while the process
6
+ * was gone is reported as EXACTLY ONE line through the registry's normal event sink — so the
7
+ * notifier's coalescing and wake budget apply to it exactly as they do to a live watch event.
8
+ *
9
+ * `digest` is load-bearing: without it a same-size, same-mtime rewrite is invisible across a restart.
10
+ */
11
+ import type { MonitorRegistry } from "./monitor-registry.ts";
12
+ import type { RestoreHandler } from "./restore.ts";
13
+ import type { TerminalManifestCheckpoint, TerminalManifestWriter } from "./terminal-manifest.ts";
14
+ /**
15
+ * What a detached change is reported as. `null` means the file is byte-identical to the checkpoint;
16
+ * `"gone"` means it existed at checkpoint time and does not now, which is not restorable.
17
+ */
18
+ export type DetachedChange = "created" | "replaced" | "modified" | "gone" | null;
19
+ /** Only what the handler needs, so a caller can hand it a narrower registry/writer. */
20
+ export interface CheckpointedFileRestoreDeps {
21
+ readonly registry: Pick<MonitorRegistry, "registerFile" | "fileCheckpoint" | "emitFileLine" | "stopFile">;
22
+ /** Optional: the durable checkpoint is re-persisted through the writer when one is bound. */
23
+ readonly writer?: Pick<TerminalManifestWriter, "scheduleCheckpoint">;
24
+ /** Optional: re-bind the stable `mon_` id to its fresh runtime id so kill_bash keeps resolving it. */
25
+ readonly bindMonitorId?: (monitorId: string, runtimeId: string) => void;
26
+ readonly now?: () => number;
27
+ }
28
+ /** The live tuple `registry.fileCheckpoint` returns; identical in shape to the persisted checkpoint. */
29
+ type LiveCheckpoint = NonNullable<ReturnType<MonitorRegistry["fileCheckpoint"]>>;
30
+ /**
31
+ * The single comparison, run once per restore. `created` and `replaced` outrank `modified`:
32
+ * an absent→present transition or a new dev/ino is a stronger statement about the same file.
33
+ */
34
+ export declare function classifyDetachedChange(saved: TerminalManifestCheckpoint, live: LiveCheckpoint): DetachedChange;
35
+ /**
36
+ * Build the `checkpointed-file` restore handler. It re-registers the watch under its original
37
+ * `mon_` id and approved parent (so the permission decision still holds), then reports at most
38
+ * one detached-change line. Returns `lost` when the path is gone or unreadable, `restored` otherwise.
39
+ */
40
+ export declare function createCheckpointedFileRestoreHandler(deps: CheckpointedFileRestoreDeps): RestoreHandler;
41
+ export {};
42
+ //# sourceMappingURL=durable-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-file.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/durable-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAmB,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAElH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;AAEjF,uFAAuF;AACvF,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,GAAG,gBAAgB,GAAG,cAAc,GAAG,UAAU,CAAC,CAAC;IAC1G,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,CAAC;IACrE,sGAAsG;IACtG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,wGAAwG;AACxG,KAAK,cAAc,GAAG,WAAW,CAAC,UAAU,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,0BAA0B,EAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAM9G;AAcD;;;;GAIG;AACH,wBAAgB,oCAAoC,CAAC,IAAI,EAAE,2BAA2B,GAAG,cAAc,CAwDtG"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The `checkpointed-file` durability class: the restore handler that makes a persistent file
3
+ * watch survive a restart. On create the monitor tool persists the registry's own identity
4
+ * tuple (dev/ino/size/mtimeMs/digest/present) as the manifest checkpoint; here that checkpoint
5
+ * is compared ONCE against the file as it is now, and any change that happened while the process
6
+ * was gone is reported as EXACTLY ONE line through the registry's normal event sink — so the
7
+ * notifier's coalescing and wake budget apply to it exactly as they do to a live watch event.
8
+ *
9
+ * `digest` is load-bearing: without it a same-size, same-mtime rewrite is invisible across a restart.
10
+ */
11
+ import { stat } from "node:fs/promises";
12
+ import { resolve } from "node:path";
13
+ /**
14
+ * The single comparison, run once per restore. `created` and `replaced` outrank `modified`:
15
+ * an absent→present transition or a new dev/ino is a stronger statement about the same file.
16
+ */
17
+ export function classifyDetachedChange(saved, live) {
18
+ if (!saved.present)
19
+ return live.present ? "created" : null;
20
+ if (!live.present)
21
+ return "gone";
22
+ if (live.dev !== saved.dev || live.ino !== saved.ino)
23
+ return "replaced";
24
+ if (live.mtimeMs !== saved.mtimeMs || live.size !== saved.size || live.digest !== saved.digest)
25
+ return "modified";
26
+ return null;
27
+ }
28
+ function lost(reason) {
29
+ return { outcome: "lost", reason };
30
+ }
31
+ /** Remaining lifetime of a durable watch; a watch with no recorded expiry keeps its default window. */
32
+ function remainingMs(monitor, now) {
33
+ if (monitor.expiresAt === null)
34
+ return DEFAULT_RESTORED_WATCH_MS;
35
+ return Math.max(1, monitor.expiresAt - now);
36
+ }
37
+ const DEFAULT_RESTORED_WATCH_MS = 300_000;
38
+ /**
39
+ * Build the `checkpointed-file` restore handler. It re-registers the watch under its original
40
+ * `mon_` id and approved parent (so the permission decision still holds), then reports at most
41
+ * one detached-change line. Returns `lost` when the path is gone or unreadable, `restored` otherwise.
42
+ */
43
+ export function createCheckpointedFileRestoreHandler(deps) {
44
+ const now = deps.now ?? Date.now;
45
+ return async (monitor) => {
46
+ if (monitor.runtimeKind !== "file" || monitor.path === undefined || monitor.cwd === undefined) {
47
+ return lost(`monitor ${monitor.monitorId} is not a restorable file watch`);
48
+ }
49
+ const saved = monitor.lastCheckpoint;
50
+ if (saved === null)
51
+ return lost(`monitor ${monitor.monitorId} has no checkpoint to compare against`);
52
+ // A watch whose file existed at checkpoint time but is gone or unreadable now cannot be
53
+ // resumed; probe BEFORE re-registering so a lost restore never leaves a live watch behind.
54
+ if (saved.present) {
55
+ try {
56
+ const target = await stat(resolve(monitor.cwd, monitor.path));
57
+ if (!target.isFile())
58
+ return lost(`watched path is no longer a regular file: ${monitor.path}`);
59
+ }
60
+ catch (error) {
61
+ return lost(`watched path is gone: ${monitor.path} (${error instanceof Error ? error.message : error})`);
62
+ }
63
+ }
64
+ let runtimeId;
65
+ try {
66
+ const registered = await deps.registry.registerFile({
67
+ description: monitor.description,
68
+ path: monitor.path,
69
+ monitorId: monitor.monitorId,
70
+ event: monitor.event ?? "create",
71
+ timeoutMs: remainingMs(monitor, now()),
72
+ cwd: monitor.cwd,
73
+ // Preserve the parent approved at permission time: registerFile re-checks it.
74
+ ...(monitor.approvedParent !== undefined ? { approvedParent: monitor.approvedParent } : {}),
75
+ // The persisted deadline rides through verbatim; a restore never extends it.
76
+ ...(monitor.expiresAt !== null ? { expiresAt: monitor.expiresAt } : {}),
77
+ });
78
+ runtimeId = registered.id;
79
+ }
80
+ catch (error) {
81
+ return lost(`cannot re-watch ${monitor.path}: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ const live = deps.registry.fileCheckpoint(runtimeId);
84
+ if (live === undefined)
85
+ return lost(`monitor ${monitor.monitorId} settled before its checkpoint was compared`);
86
+ const change = classifyDetachedChange(saved, live);
87
+ if (change === "gone") {
88
+ // Lost the race with a deletion between the probe and re-registration: report lost, not a
89
+ // change, and settle the watch just registered so a lost restore leaves nothing behind.
90
+ await deps.registry.stopFile(runtimeId);
91
+ return lost(`watched path is gone: ${monitor.path}`);
92
+ }
93
+ deps.bindMonitorId?.(monitor.monitorId, runtimeId);
94
+ if (change !== null)
95
+ deps.registry.emitFileLine(runtimeId, `changed while detached: ${change} ${monitor.path}`);
96
+ // Re-checkpoint on every restore: after `replaced` this is the new identity, and otherwise
97
+ // it re-states the tuple the next restart compares against.
98
+ deps.writer?.scheduleCheckpoint(monitor.monitorId, live);
99
+ return { outcome: "restored" };
100
+ };
101
+ }
102
+ //# sourceMappingURL=durable-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-file.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/durable-file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `checkpointed-file` durability class: the restore handler that makes a persistent file\n * watch survive a restart. On create the monitor tool persists the registry's own identity\n * tuple (dev/ino/size/mtimeMs/digest/present) as the manifest checkpoint; here that checkpoint\n * is compared ONCE against the file as it is now, and any change that happened while the process\n * was gone is reported as EXACTLY ONE line through the registry's normal event sink — so the\n * notifier's coalescing and wake budget apply to it exactly as they do to a live watch event.\n *\n * `digest` is load-bearing: without it a same-size, same-mtime rewrite is invisible across a restart.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { MonitorRegistry } from \"./monitor-registry.ts\";\nimport type { RestoreHandler, RestoreHandlerResult } from \"./restore.ts\";\nimport type { ManifestMonitor, TerminalManifestCheckpoint, TerminalManifestWriter } from \"./terminal-manifest.ts\";\n\n/**\n * What a detached change is reported as. `null` means the file is byte-identical to the checkpoint;\n * `\"gone\"` means it existed at checkpoint time and does not now, which is not restorable.\n */\nexport type DetachedChange = \"created\" | \"replaced\" | \"modified\" | \"gone\" | null;\n\n/** Only what the handler needs, so a caller can hand it a narrower registry/writer. */\nexport interface CheckpointedFileRestoreDeps {\n\treadonly registry: Pick<MonitorRegistry, \"registerFile\" | \"fileCheckpoint\" | \"emitFileLine\" | \"stopFile\">;\n\t/** Optional: the durable checkpoint is re-persisted through the writer when one is bound. */\n\treadonly writer?: Pick<TerminalManifestWriter, \"scheduleCheckpoint\">;\n\t/** Optional: re-bind the stable `mon_` id to its fresh runtime id so kill_bash keeps resolving it. */\n\treadonly bindMonitorId?: (monitorId: string, runtimeId: string) => void;\n\treadonly now?: () => number;\n}\n\n/** The live tuple `registry.fileCheckpoint` returns; identical in shape to the persisted checkpoint. */\ntype LiveCheckpoint = NonNullable<ReturnType<MonitorRegistry[\"fileCheckpoint\"]>>;\n\n/**\n * The single comparison, run once per restore. `created` and `replaced` outrank `modified`:\n * an absent→present transition or a new dev/ino is a stronger statement about the same file.\n */\nexport function classifyDetachedChange(saved: TerminalManifestCheckpoint, live: LiveCheckpoint): DetachedChange {\n\tif (!saved.present) return live.present ? \"created\" : null;\n\tif (!live.present) return \"gone\";\n\tif (live.dev !== saved.dev || live.ino !== saved.ino) return \"replaced\";\n\tif (live.mtimeMs !== saved.mtimeMs || live.size !== saved.size || live.digest !== saved.digest) return \"modified\";\n\treturn null;\n}\n\nfunction lost(reason: string): RestoreHandlerResult {\n\treturn { outcome: \"lost\", reason };\n}\n\n/** Remaining lifetime of a durable watch; a watch with no recorded expiry keeps its default window. */\nfunction remainingMs(monitor: ManifestMonitor, now: number): number {\n\tif (monitor.expiresAt === null) return DEFAULT_RESTORED_WATCH_MS;\n\treturn Math.max(1, monitor.expiresAt - now);\n}\n\nconst DEFAULT_RESTORED_WATCH_MS = 300_000;\n\n/**\n * Build the `checkpointed-file` restore handler. It re-registers the watch under its original\n * `mon_` id and approved parent (so the permission decision still holds), then reports at most\n * one detached-change line. Returns `lost` when the path is gone or unreadable, `restored` otherwise.\n */\nexport function createCheckpointedFileRestoreHandler(deps: CheckpointedFileRestoreDeps): RestoreHandler {\n\tconst now = deps.now ?? Date.now;\n\treturn async (monitor: ManifestMonitor): Promise<RestoreHandlerResult> => {\n\t\tif (monitor.runtimeKind !== \"file\" || monitor.path === undefined || monitor.cwd === undefined) {\n\t\t\treturn lost(`monitor ${monitor.monitorId} is not a restorable file watch`);\n\t\t}\n\t\tconst saved = monitor.lastCheckpoint;\n\t\tif (saved === null) return lost(`monitor ${monitor.monitorId} has no checkpoint to compare against`);\n\t\t// A watch whose file existed at checkpoint time but is gone or unreadable now cannot be\n\t\t// resumed; probe BEFORE re-registering so a lost restore never leaves a live watch behind.\n\t\tif (saved.present) {\n\t\t\ttry {\n\t\t\t\tconst target = await stat(resolve(monitor.cwd, monitor.path));\n\t\t\t\tif (!target.isFile()) return lost(`watched path is no longer a regular file: ${monitor.path}`);\n\t\t\t} catch (error) {\n\t\t\t\treturn lost(`watched path is gone: ${monitor.path} (${error instanceof Error ? error.message : error})`);\n\t\t\t}\n\t\t}\n\n\t\tlet runtimeId: string;\n\t\ttry {\n\t\t\tconst registered = await deps.registry.registerFile({\n\t\t\t\tdescription: monitor.description,\n\t\t\t\tpath: monitor.path,\n\t\t\t\tmonitorId: monitor.monitorId,\n\t\t\t\tevent: monitor.event ?? \"create\",\n\t\t\t\ttimeoutMs: remainingMs(monitor, now()),\n\t\t\t\tcwd: monitor.cwd,\n\t\t\t\t// Preserve the parent approved at permission time: registerFile re-checks it.\n\t\t\t\t...(monitor.approvedParent !== undefined ? { approvedParent: monitor.approvedParent } : {}),\n\t\t\t\t// The persisted deadline rides through verbatim; a restore never extends it.\n\t\t\t\t...(monitor.expiresAt !== null ? { expiresAt: monitor.expiresAt } : {}),\n\t\t\t});\n\t\t\truntimeId = registered.id;\n\t\t} catch (error) {\n\t\t\treturn lost(`cannot re-watch ${monitor.path}: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\n\t\tconst live = deps.registry.fileCheckpoint(runtimeId);\n\t\tif (live === undefined) return lost(`monitor ${monitor.monitorId} settled before its checkpoint was compared`);\n\n\t\tconst change = classifyDetachedChange(saved, live);\n\t\tif (change === \"gone\") {\n\t\t\t// Lost the race with a deletion between the probe and re-registration: report lost, not a\n\t\t\t// change, and settle the watch just registered so a lost restore leaves nothing behind.\n\t\t\tawait deps.registry.stopFile(runtimeId);\n\t\t\treturn lost(`watched path is gone: ${monitor.path}`);\n\t\t}\n\n\t\tdeps.bindMonitorId?.(monitor.monitorId, runtimeId);\n\t\tif (change !== null) deps.registry.emitFileLine(runtimeId, `changed while detached: ${change} ${monitor.path}`);\n\t\t// Re-checkpoint on every restore: after `replaced` this is the new identity, and otherwise\n\t\t// it re-states the tuple the next restart compares against.\n\t\tdeps.writer?.scheduleCheckpoint(monitor.monitorId, live);\n\t\treturn { outcome: \"restored\" };\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/extension.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AA6TrE,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4IhE;AAED,eAAe,yBAAyB,CAAC"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/extension.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AAiXrE,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4IhE;AAED,eAAe,yBAAyB,CAAC"}
@@ -5,13 +5,15 @@ import { SettingsManager } from "../../../settings-manager.js";
5
5
  import { isAnthropicBashEnabled } from "../anthropic-bash/index.js";
6
6
  import { isEvalOnlyRouting } from "../eval-only-routing.js";
7
7
  import { TERMINAL_MONITOR_STATE_EVENT, WAKE_SOURCE_STATE_EVENT } from "../monitor-state-event.js";
8
+ import { createRestartableCommandHandler } from "./durable-command.js";
9
+ import { createCheckpointedFileRestoreHandler } from "./durable-file.js";
8
10
  import { acquireTerminalLease, releaseTerminalLease } from "./manifest-lease.js";
9
11
  import { MonitorNotifier } from "./monitor-notify.js";
10
12
  import { MONITOR_STATUS_KEY } from "./monitor-status.js";
11
13
  import { MonitorStatusTicker } from "./monitor-status-ticker.js";
12
14
  import { getTerminalNotificationDelivery, TerminalNotifier } from "./notify.js";
13
15
  import { buildTerminalPromptSection } from "./prompt.js";
14
- import { restoreTerminalState } from "./restore.js";
16
+ import { reapplyPersistedMute, restoreTerminalState, } from "./restore.js";
15
17
  import { claimParkedBundle, parkBundle, TerminalSessionBundle, teardownParkedBundle, } from "./session-bundle.js";
16
18
  import { loadTerminalSettings, TERMINAL_SETTINGS_DEFAULTS } from "./settings.js";
17
19
  import { TERMINAL_BASH_TOOL, TERMINAL_COMPANION_TOOLS } from "./shared.js";
@@ -208,7 +210,7 @@ function restoreDigestSentence(digest, restoredDescriptions, lostDescriptions) {
208
210
  * attached-elsewhere reminder; an unreadable manifest fails closed with one notice and no
209
211
  * restore attempt. Only the lease owner records manifest state afterwards.
210
212
  */
211
- async function adoptPersistedTerminalState(pi, state, sessionKey) {
213
+ async function adoptPersistedTerminalState(pi, state, toolCtx, bundle, sessionKey) {
212
214
  const ctx = state.ctx;
213
215
  const dir = terminalStateDir(ctx);
214
216
  if (dir === undefined || !ctx?.sessionManager)
@@ -223,15 +225,51 @@ async function adoptPersistedTerminalState(pi, state, sessionKey) {
223
225
  state.manifestWriter = writer;
224
226
  state.recordedBackgroundIds.clear();
225
227
  bindTerminalManifestWriter(sessionKey, writer);
226
- // Restoration handlers: this generation does not re-spawn persisted commands on its own,
227
- // so every durable entry reports lost; the outcomes still flow through the orchestrator.
228
+ // The real durability handlers, built from what this restore site already owns: the live
229
+ // registry/manager of the bundle created for this generation, plus the manifest writer.
230
+ // Each monitor's REAL outcome is recorded so the digest attributes descriptions correctly.
231
+ const nowMs = Date.now();
228
232
  const outcomesById = new Map();
229
- const reportLost = (monitor) => {
230
- outcomesById.set(monitor.monitorId, "lost");
231
- return { outcome: "lost" };
233
+ const registry = bundle.monitors;
234
+ const record = (handler) => {
235
+ return async (monitor) => {
236
+ const result = await handler(monitor);
237
+ outcomesById.set(monitor.monitorId, result.outcome);
238
+ // Re-adopt every entry that is LIVE in this generation, so the next persist rewrites
239
+ // the manifest with it instead of erasing it: `restored` and `muted` both describe a
240
+ // live monitor (a mute only silences delivery). `lost`/`expired`/`attachedElsewhere`
241
+ // are deliberately not adopted — nothing is running for them here.
242
+ if (result.outcome === "restored" || result.outcome === "muted") {
243
+ writer.adoptRestored(monitor);
244
+ // Re-bind the persisted fire budget so a restart cannot hand the watch a fresh window.
245
+ registry.adoptFireWindow(monitor.monitorId, monitor.fireWindow);
246
+ }
247
+ return result;
248
+ };
249
+ };
250
+ const restartableCommand = createRestartableCommandHandler({ ctx: toolCtx, registry });
251
+ // The file handler re-binds the mon_ id to the fresh runtime id; capture that id here so a
252
+ // persisted mute is re-applied by the runtime id the registry can actually resolve.
253
+ let freshFileRuntimeId;
254
+ const checkpointedFile = createCheckpointedFileRestoreHandler({
255
+ registry,
256
+ writer,
257
+ bindMonitorId: (monitorId, runtimeId) => {
258
+ freshFileRuntimeId = runtimeId;
259
+ bundle.manager.bindMonitorId(monitorId, runtimeId);
260
+ },
261
+ now: () => nowMs,
262
+ });
263
+ const handlers = {
264
+ "restartable-command": record(restartableCommand),
265
+ "checkpointed-file": record(async (monitor) => {
266
+ freshFileRuntimeId = undefined;
267
+ const result = await checkpointedFile(monitor);
268
+ if (result.outcome !== "restored" || freshFileRuntimeId === undefined)
269
+ return result;
270
+ return { ...result, outcome: reapplyPersistedMute(registry, monitor, freshFileRuntimeId) };
271
+ }),
232
272
  };
233
- const handlers = { "restartable-command": reportLost, "checkpointed-file": reportLost };
234
- const nowMs = Date.now();
235
273
  const digest = await restoreTerminalState({ manifest: writer.store, handlers, now: () => nowMs });
236
274
  let manifest = null;
237
275
  let unreadable = digest.storeError;
@@ -252,11 +290,15 @@ async function adoptPersistedTerminalState(pi, state, sessionKey) {
252
290
  const restoredDescriptions = [];
253
291
  const lostDescriptions = [];
254
292
  for (const monitor of manifest.monitors) {
255
- if (monitor.expiresAt !== null && monitor.expiresAt < nowMs)
293
+ // Same boundary as the restore orchestrator: at the deadline the entry is expired.
294
+ if (monitor.expiresAt !== null && monitor.expiresAt <= nowMs)
256
295
  continue;
257
- if (outcomesById.get(monitor.monitorId) === "restored")
296
+ const outcome = outcomesById.get(monitor.monitorId);
297
+ // Only `restored` and `lost` carry descriptions; a muted restore is reported by its own
298
+ // count clause, so naming it under either list would desync the clause counts.
299
+ if (outcome === "restored")
258
300
  restoredDescriptions.push(monitor.description);
259
- else
301
+ else if (outcome === undefined || outcome === "lost")
260
302
  lostDescriptions.push(monitor.description);
261
303
  }
262
304
  for (const background of manifest.backgroundSessions)
@@ -279,6 +321,9 @@ function detachManifestWriter(state, sessionKey) {
279
321
  */
280
322
  async function suspendAndFlushManifest(state, sessionKey) {
281
323
  const writer = state.manifestWriter;
324
+ // Final live-state sync: the shutdown write must carry each watch's burned fire window.
325
+ if (writer && state.bundle)
326
+ await writer.observeMonitorState(state.bundle.monitors.snapshot());
282
327
  detachManifestWriter(state, sessionKey);
283
328
  if (writer)
284
329
  await writer.recordShutdown();
@@ -357,7 +402,7 @@ export function registerTerminalExtension(pi) {
357
402
  }
358
403
  }
359
404
  else if (sessionKey !== undefined) {
360
- await adoptPersistedTerminalState(pi, state, sessionKey);
405
+ await adoptPersistedTerminalState(pi, state, toolCtx, state.bundle, sessionKey);
361
406
  }
362
407
  syncToolset(pi, state);
363
408
  });