@retaw106/mmc-oc 2.2.1 → 2.3.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.
- package/dist/cli.js +2 -2
- package/dist/config/deprecated-config.d.ts +4 -0
- package/dist/config/deprecated-config.d.ts.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -2
- package/dist/config/schema/magic-context.d.ts +3 -3
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +2 -2
- package/dist/{context-mrvm92n8.js → context-wt7g4f8k.js} +1641 -1543
- package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
- package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
- package/dist/features/magic-context/message-index-async.d.ts +21 -3
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +7 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +29 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts +2 -0
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
- package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
- package/dist/features/magic-context/storage-tags.d.ts +155 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +2 -0
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +3 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +7 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
- package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/pending-materialization.d.ts +8 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
- package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +45 -0
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +41 -1
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +38 -24
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
- package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
- package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +21 -4
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index-18yrysyj.js +1477 -0
- package/dist/{index-g2z2k5m4.js → index-3gj1ak4c.js} +2 -2
- package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
- package/dist/{index-amycb7b1.js → index-a5mw0xp3.js} +3 -3
- package/dist/{index-1j34a56j.js → index-czba8tw3.js} +266 -32
- package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
- package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
- package/dist/{index-5b7x80ec.js → index-kw21z5pe.js} +6055 -3973
- package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
- package/dist/{index-btp5tdc8.js → index-z2w1eq3y.js} +840 -456
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -2
- package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-sykevdax.js} +3 -3
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-863d6c06.js} +6 -4
- package/dist/recomp-orchestrator-wrjy3qym.js +21 -0
- package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
- package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-cz6ev088.js} +5 -5
- package/dist/shared/auto-search-lifecycle.d.ts +14 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/models-dev-temperature.d.ts +18 -0
- package/dist/shared/models-dev-temperature.d.ts.map +1 -0
- package/dist/shared/processed-image-eligibility.d.ts +22 -0
- package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
- package/dist/shared/project-directory-key.d.ts +16 -0
- package/dist/shared/project-directory-key.d.ts.map +1 -1
- package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +33 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/stable-json.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.d.ts +9 -0
- package/dist/shared/storage-file-mutex.d.ts.map +1 -0
- package/dist/shared/storage-permissions.d.ts +6 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -1
- package/dist/shared/subagent-temperature.d.ts +46 -0
- package/dist/shared/subagent-temperature.d.ts.map +1 -0
- package/dist/shared/tag-owner-id.d.ts +1 -0
- package/dist/shared/tag-owner-id.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +9 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +9 -66
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/standalone-plugin-marker.d.ts +8 -0
- package/dist/standalone-plugin-marker.d.ts.map +1 -0
- package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
- package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/constants.d.ts +1 -2
- package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
- package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
- package/dist/v2/hooks/generate.d.ts +9 -2
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
- package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
- package/dist/v2/server.d.ts.map +1 -1
- package/dist/v2/server.js +34550 -30855
- package/dist/web/main.js +373 -19
- package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-bmybc5p5.js} +10 -10
- package/package.json +1 -1
- package/src/config/deprecated-config.ts +18 -0
- package/src/config/index.test.ts +13 -37
- package/src/config/index.ts +2 -2
- package/src/config/project-security.ts +3 -3
- package/src/config/schema/magic-context.ts +10 -6
- package/src/shared/auto-search-lifecycle.ts +43 -0
- package/src/shared/logger.ts +10 -2
- package/src/shared/models-dev-cache.ts +1 -7
- package/src/shared/models-dev-temperature.ts +107 -0
- package/src/shared/processed-image-eligibility.test.ts +68 -0
- package/src/shared/processed-image-eligibility.ts +41 -0
- package/src/shared/processed-image-position-review.json +20 -0
- package/src/shared/processed-image-position-review.test.ts +19 -0
- package/src/shared/project-directory-key.ts +20 -0
- package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
- package/src/shared/recovery-reasoning-prefix.ts +35 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
- package/src/shared/sqlite-foreground-wait.test.ts +68 -0
- package/src/shared/sqlite-pass-scope.test.ts +4 -4
- package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
- package/src/shared/sqlite.ts +380 -39
- package/src/shared/stable-json.ts +37 -7
- package/src/shared/storage-file-mutex.test.ts +50 -0
- package/src/shared/storage-file-mutex.ts +130 -0
- package/src/shared/storage-permissions.ts +46 -0
- package/src/shared/subagent-temperature.ts +110 -0
- package/src/shared/tag-owner-id.ts +13 -0
- package/src/shared/tag-transcript.ts +118 -0
- package/src/shared/window-geometry.test.ts +102 -236
- package/src/shared/window-geometry.ts +23 -413
- package/dist/index-ramz5kkm.js +0 -514
- package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
- package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
package/src/config/index.test.ts
CHANGED
|
@@ -7,11 +7,7 @@ import { resolveEpochFloorForPass } from "../features/magic-context/storage-meta
|
|
|
7
7
|
import { resolveHistorianModel } from "../shared/model-resolution";
|
|
8
8
|
import { Database } from "../shared/sqlite";
|
|
9
9
|
import { createTestTempDir, createTestTempDirFromPath } from "../shared/test-temp-dir";
|
|
10
|
-
import {
|
|
11
|
-
getWindowOverlay,
|
|
12
|
-
reloadWindowOverlay,
|
|
13
|
-
setWindowOverlayPath,
|
|
14
|
-
} from "../shared/window-geometry";
|
|
10
|
+
import { WINDOW_OVERLAY_PATH_DEPRECATION_WARNING } from "./deprecated-config";
|
|
15
11
|
import { loadPluginConfig, loadPluginConfigDetailed } from "./index";
|
|
16
12
|
import { resolveConfigProfile } from "./profiles";
|
|
17
13
|
import { getProtectedTokensTierOverrides } from "./project-security";
|
|
@@ -156,39 +152,19 @@ function loadDetailedWithConfigs(userConfigText: string, projectConfigText?: str
|
|
|
156
152
|
}
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
describe("loadPluginConfig — Fusiform
|
|
160
|
-
it("
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
writeFileSync(
|
|
166
|
-
overlayPath,
|
|
167
|
-
JSON.stringify({
|
|
168
|
-
schema: "fusiform-window-overlay/v1",
|
|
169
|
-
generated_at: "2026-09-01T00:00:00Z",
|
|
170
|
-
minted_provider_ids: [],
|
|
171
|
-
cells: [{ provider_id: "hunt-provider", model_id: modelId, facts: {} }],
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
writeOverlay("before-rewrite");
|
|
177
|
-
loadWithUserConfig(configText);
|
|
178
|
-
expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
|
|
179
|
-
|
|
180
|
-
writeOverlay("after-rewrite");
|
|
181
|
-
expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
|
|
182
|
-
|
|
183
|
-
loadWithUserConfig(configText);
|
|
184
|
-
expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
|
|
155
|
+
describe("loadPluginConfig — retired Fusiform window path", () => {
|
|
156
|
+
it("parses the old key but warns once that it is ignored", () => {
|
|
157
|
+
const path = "/tmp/retired-fusiform-window.json";
|
|
158
|
+
const config = loadWithUserConfig(
|
|
159
|
+
JSON.stringify({ models: { window_overlay_path: path } }),
|
|
160
|
+
);
|
|
185
161
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
162
|
+
expect(config.models?.window_overlay_path).toBe(path);
|
|
163
|
+
const warnings = config.configWarnings?.filter((message) =>
|
|
164
|
+
message.includes("models.window_overlay_path"),
|
|
165
|
+
);
|
|
166
|
+
expect(warnings).toHaveLength(1);
|
|
167
|
+
expect(warnings?.[0]).toContain(WINDOW_OVERLAY_PATH_DEPRECATION_WARNING);
|
|
192
168
|
});
|
|
193
169
|
});
|
|
194
170
|
|
package/src/config/index.ts
CHANGED
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
} from "../shared/jsonc-parser";
|
|
14
14
|
import { setOutputReserveConfig } from "../shared/models-dev-cache";
|
|
15
15
|
import type { PromptSurfaceConfig } from "../shared/prompt-surface";
|
|
16
|
-
import { setWindowOverlayPath } from "../shared/window-geometry";
|
|
17
16
|
import { migrateLegacyAgentEnabledInMemory } from "./agent-disable";
|
|
18
17
|
import { mmcProjectConfigBasePath, opencodeUserConfigBasePath } from "./config-paths";
|
|
18
|
+
import { warnDeprecatedWindowOverlayPath } from "./deprecated-config";
|
|
19
19
|
import {
|
|
20
20
|
legacyHarnessBlockPaths,
|
|
21
21
|
RemovedHistorianHarnessBlockError,
|
|
@@ -370,6 +370,7 @@ export function parsePluginConfig(
|
|
|
370
370
|
preMigrationWarnings,
|
|
371
371
|
);
|
|
372
372
|
const migrated = migrateLegacyAgentEnabledInMemory(migratedExperimental, preMigrationWarnings);
|
|
373
|
+
warnDeprecatedWindowOverlayPath(migrated, preMigrationWarnings);
|
|
373
374
|
const parsed = MagicContextConfigSchema.safeParse(migrated);
|
|
374
375
|
const disabledHooks = Array.isArray(rawConfig.disabled_hooks)
|
|
375
376
|
? rawConfig.disabled_hooks.filter((value): value is string => typeof value === "string")
|
|
@@ -711,7 +712,6 @@ export function loadPluginConfigDetailed(
|
|
|
711
712
|
if (profileResolution.activeProfile) config.profile = profileResolution.activeProfile;
|
|
712
713
|
if (applyRuntimeGlobals) {
|
|
713
714
|
setOutputReserveConfig(config.output_reserve);
|
|
714
|
-
setWindowOverlayPath(config.models?.window_overlay_path);
|
|
715
715
|
}
|
|
716
716
|
const leafValidationWarnings = [...(config.configWarnings ?? [])];
|
|
717
717
|
if (config.configWarnings?.length) {
|
|
@@ -301,8 +301,8 @@ function makeProjectThresholdWarning(field: string, reason: string): string {
|
|
|
301
301
|
* - `debug_rpc` — a repo must not enable process heap capture or memory diagnostics.
|
|
302
302
|
* - `allow_home_project` — only the user may opt a home-directory session
|
|
303
303
|
* into a durable project identity.
|
|
304
|
-
* - `output_reserve`
|
|
305
|
-
*
|
|
304
|
+
* - `output_reserve` — only the user may change process-wide window geometry.
|
|
305
|
+
* The deprecated `models.window_overlay_path` field is ignored at every tier.
|
|
306
306
|
* - `language`: a repo must not inject prompt text through a user preference.
|
|
307
307
|
* - `maintenance` — budgets, cooldowns, and anomaly thresholds of the
|
|
308
308
|
* automatic memory-maintenance task are user-level only; a cloned repo
|
|
@@ -398,7 +398,7 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record<string, unknow
|
|
|
398
398
|
if (isPlainObject(models) && "window_overlay_path" in models) {
|
|
399
399
|
delete models.window_overlay_path;
|
|
400
400
|
warnings.push(
|
|
401
|
-
"Ignoring models.window_overlay_path from project config (
|
|
401
|
+
"Ignoring models.window_overlay_path from project config (deprecated; this setting no longer changes model window geometry).",
|
|
402
402
|
);
|
|
403
403
|
}
|
|
404
404
|
|
|
@@ -261,8 +261,8 @@ export interface MagicContextConfig {
|
|
|
261
261
|
prompt_surface: PromptSurfaceConfig;
|
|
262
262
|
/** User-only output-token reservation override. Zero disables reservation. */
|
|
263
263
|
output_reserve?: number | { default: number; [modelKey: string]: number };
|
|
264
|
-
/**
|
|
265
|
-
models?: { window_overlay_path?:
|
|
264
|
+
/** Deprecated user-only window metadata path; parsed only so loaders can warn and ignore it. */
|
|
265
|
+
models?: { window_overlay_path?: unknown };
|
|
266
266
|
/** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
|
|
267
267
|
toast_duration_ms?: number;
|
|
268
268
|
execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
|
|
@@ -487,12 +487,16 @@ export const MagicContextConfigSchema = z.object({
|
|
|
487
487
|
),
|
|
488
488
|
models: z
|
|
489
489
|
.object({
|
|
490
|
-
window_overlay_path: z
|
|
490
|
+
window_overlay_path: z
|
|
491
|
+
.unknown()
|
|
492
|
+
.optional()
|
|
493
|
+
.describe(
|
|
494
|
+
"Deprecated: ignored. Fusiform no longer publishes window-overlay data, so this setting no longer changes model window geometry. Remove this key.",
|
|
495
|
+
)
|
|
496
|
+
.meta({ deprecated: true }),
|
|
491
497
|
})
|
|
492
498
|
.optional()
|
|
493
|
-
.describe(
|
|
494
|
-
"User-only Fusiform window-overlay settings. The path defaults to <dataDir>/fusiform/window-overlay.json.",
|
|
495
|
-
),
|
|
499
|
+
.describe("Deprecated user-only Fusiform window-overlay settings; ignored."),
|
|
496
500
|
toast_duration_ms: z
|
|
497
501
|
.number()
|
|
498
502
|
.min(0)
|
|
@@ -1,4 +1,47 @@
|
|
|
1
|
+
import type { AutoSearchHintNoHintReason } from "../features/magic-context/storage-meta-persisted";
|
|
2
|
+
|
|
1
3
|
const pending = new Map<string, Set<AbortController>>();
|
|
4
|
+
const unpublishedNoHints = new Map<string, Map<string, AutoSearchHintNoHintReason>>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A failed save can still send the user message without a search hint. Remember
|
|
8
|
+
* that no-hint decision until a later pass saves it to SQLite, including across
|
|
9
|
+
* closing and reopening the database handle. Switching or shutting down a session
|
|
10
|
+
* does not prove deletion and must not discard it. If all compare-and-swap (CAS)
|
|
11
|
+
* save retries fail and the process restarts before repair, this unsaved record
|
|
12
|
+
* is lost: that compound failure is the remaining durability gap.
|
|
13
|
+
*/
|
|
14
|
+
export function rememberUnpublishedAutoSearchNoHint(
|
|
15
|
+
sessionId: string,
|
|
16
|
+
messageId: string,
|
|
17
|
+
reason: AutoSearchHintNoHintReason,
|
|
18
|
+
): void {
|
|
19
|
+
let messages = unpublishedNoHints.get(sessionId);
|
|
20
|
+
if (!messages) {
|
|
21
|
+
messages = new Map();
|
|
22
|
+
unpublishedNoHints.set(sessionId, messages);
|
|
23
|
+
}
|
|
24
|
+
if (!messages.has(messageId)) messages.set(messageId, reason);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getUnpublishedAutoSearchNoHint(
|
|
28
|
+
sessionId: string,
|
|
29
|
+
messageId: string,
|
|
30
|
+
): AutoSearchHintNoHintReason | undefined {
|
|
31
|
+
return unpublishedNoHints.get(sessionId)?.get(messageId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function forgetUnpublishedAutoSearchNoHint(sessionId: string, messageId: string): void {
|
|
35
|
+
const messages = unpublishedNoHints.get(sessionId);
|
|
36
|
+
messages?.delete(messageId);
|
|
37
|
+
if (messages?.size === 0) unpublishedNoHints.delete(sessionId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Deleting a session releases its unsaved no-hint records; switching away does not. */
|
|
41
|
+
export function clearUnpublishedAutoSearchNoHints(sessionId?: string): void {
|
|
42
|
+
if (sessionId === undefined) unpublishedNoHints.clear();
|
|
43
|
+
else unpublishedNoHints.delete(sessionId);
|
|
44
|
+
}
|
|
2
45
|
|
|
3
46
|
/** Cleanup cancels optional searches and backfill, not durable served decisions. */
|
|
4
47
|
export function registerAutoSearchWork(sessionId: string): {
|
package/src/shared/logger.ts
CHANGED
|
@@ -162,8 +162,16 @@ function flush(): void {
|
|
|
162
162
|
let currentSize = getCurrentLogSize(logFile);
|
|
163
163
|
const dataSize = Buffer.byteLength(data);
|
|
164
164
|
if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
// Several processes share this file and each caches its size. A stale
|
|
166
|
+
// cache must not rotate a file another process already rotated: that
|
|
167
|
+
// second rotation would overwrite the full predecessor with the few
|
|
168
|
+
// lines written since. Decide on the file's real size instead.
|
|
169
|
+
flushesSinceSizeCheck = SIZE_CHECK_INTERVAL_FLUSHES;
|
|
170
|
+
currentSize = getCurrentLogSize(logFile);
|
|
171
|
+
if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
|
|
172
|
+
rotateLogFile(logFile, currentSize);
|
|
173
|
+
currentSize = 0;
|
|
174
|
+
}
|
|
167
175
|
}
|
|
168
176
|
fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
|
|
169
177
|
buffer = [];
|
|
@@ -33,12 +33,7 @@ import { getHarness } from "./harness";
|
|
|
33
33
|
import { modelRefLookupOrder } from "./harness-provider-map";
|
|
34
34
|
import { sessionLog } from "./logger";
|
|
35
35
|
import { writeStorageFileAtomicSync } from "./storage-permissions";
|
|
36
|
-
import {
|
|
37
|
-
deriveWindowGeometry,
|
|
38
|
-
getWindowOverlay,
|
|
39
|
-
resolveWindowOverlayFacts,
|
|
40
|
-
type WindowGeometryResult,
|
|
41
|
-
} from "./window-geometry";
|
|
36
|
+
import { deriveWindowGeometry, type WindowGeometryResult } from "./window-geometry";
|
|
42
37
|
|
|
43
38
|
interface OpencodeClientLike {
|
|
44
39
|
config: {
|
|
@@ -515,7 +510,6 @@ export function getSdkWindowGeometry(
|
|
|
515
510
|
output: metadata.outputLimit,
|
|
516
511
|
},
|
|
517
512
|
{
|
|
518
|
-
overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
|
|
519
513
|
outputReserveOverride: resolveOutputReserve(providerID, modelID),
|
|
520
514
|
harness: options?.harness ?? "opencode",
|
|
521
515
|
contextCap:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature capability from the models.dev catalog that OpenCode caches on disk.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 1 copies models.dev's `temperature` flag onto each model, but OpenCode 2's
|
|
5
|
+
* model rows, Pi's model registry and Oh My Pi's Gemini models carry no equivalent
|
|
6
|
+
* field. For those hosts the cached catalog is the only positive evidence on the
|
|
7
|
+
* machine, so a missing file, an unparsable file or a missing entry all mean "unknown"
|
|
8
|
+
* and the caller omits temperature.
|
|
9
|
+
*
|
|
10
|
+
* This module is bundled into the Pi/OMP historian child extension, so it must only
|
|
11
|
+
* depend on Node built-ins.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface CatalogTemperature {
|
|
18
|
+
temperature: boolean;
|
|
19
|
+
/** The catalog entry that decided, as `provider/model`. */
|
|
20
|
+
entry: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Harness provider ids that name the same models.dev provider. Codex (`openai-codex`)
|
|
25
|
+
* is deliberately absent: the ChatGPT backend is a different transport from the
|
|
26
|
+
* OpenAI API that models.dev describes, so it gets no catalog answer and no temperature.
|
|
27
|
+
*/
|
|
28
|
+
const CATALOG_PROVIDER_ALIASES: Readonly<Record<string, string>> = {
|
|
29
|
+
"google-antigravity": "google",
|
|
30
|
+
"opencode-zen": "opencode",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The Antigravity auth plugins register Gemini models as `google/antigravity-<model>`. */
|
|
34
|
+
const ANTIGRAVITY_MODEL_PREFIX = "antigravity-";
|
|
35
|
+
|
|
36
|
+
type CatalogIndex = Map<string, boolean>;
|
|
37
|
+
|
|
38
|
+
let cached: { path: string; mtimeMs: number; size: number; index: CatalogIndex } | undefined;
|
|
39
|
+
|
|
40
|
+
/** OpenCode reads `OPENCODE_MODELS_PATH` instead of its cache when it is set. */
|
|
41
|
+
export function modelsDevCatalogPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
42
|
+
if (env.OPENCODE_MODELS_PATH) return env.OPENCODE_MODELS_PATH;
|
|
43
|
+
return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode", "models.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildIndex(raw: unknown): CatalogIndex {
|
|
47
|
+
const index: CatalogIndex = new Map();
|
|
48
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return index;
|
|
49
|
+
for (const [providerID, provider] of Object.entries(raw as Record<string, unknown>)) {
|
|
50
|
+
const models = (provider as { models?: unknown } | null)?.models;
|
|
51
|
+
if (!models || typeof models !== "object") continue;
|
|
52
|
+
for (const [modelID, model] of Object.entries(models as Record<string, unknown>)) {
|
|
53
|
+
const temperature = (model as { temperature?: unknown } | null)?.temperature;
|
|
54
|
+
if (typeof temperature === "boolean")
|
|
55
|
+
index.set(`${providerID}/${modelID}`, temperature);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadIndex(): CatalogIndex | undefined {
|
|
62
|
+
const path = modelsDevCatalogPath();
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(path);
|
|
65
|
+
if (cached?.path === path && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
|
|
66
|
+
return cached.index;
|
|
67
|
+
const index = buildIndex(JSON.parse(readFileSync(path, "utf8")));
|
|
68
|
+
cached = { path, mtimeMs: stat.mtimeMs, size: stat.size, index };
|
|
69
|
+
return index;
|
|
70
|
+
} catch {
|
|
71
|
+
// Absent or partially written catalog: no evidence either way.
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Spellings to try for a harness `provider/model` reference, most specific first. */
|
|
77
|
+
export function catalogCandidates(providerID: string, modelID: string): string[] {
|
|
78
|
+
const provider = Object.hasOwn(CATALOG_PROVIDER_ALIASES, providerID)
|
|
79
|
+
? CATALOG_PROVIDER_ALIASES[providerID]
|
|
80
|
+
: providerID;
|
|
81
|
+
const candidates = [`${provider}/${modelID}`];
|
|
82
|
+
if (provider === "google" && modelID.startsWith(ANTIGRAVITY_MODEL_PREFIX))
|
|
83
|
+
candidates.push(`google/${modelID.slice(ANTIGRAVITY_MODEL_PREFIX.length)}`);
|
|
84
|
+
return candidates;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Look `provider/model` up in the cached catalogue, trying only the exact spellings from
|
|
89
|
+
* `catalogCandidates`. A malformed reference, a missing catalogue or no entry returns undefined.
|
|
90
|
+
*/
|
|
91
|
+
export function catalogTemperature(model: string): CatalogTemperature | undefined {
|
|
92
|
+
const slash = model.indexOf("/");
|
|
93
|
+
if (slash <= 0 || slash === model.length - 1) return undefined;
|
|
94
|
+
const index = loadIndex();
|
|
95
|
+
if (!index) return undefined;
|
|
96
|
+
for (const entry of catalogCandidates(model.slice(0, slash), model.slice(slash + 1))) {
|
|
97
|
+
const temperature = index.get(entry);
|
|
98
|
+
if (temperature !== undefined) return { temperature, entry };
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const __catalogTest = {
|
|
104
|
+
reset(): void {
|
|
105
|
+
cached = undefined;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createProcessedImageEligibility } from "./processed-image-eligibility";
|
|
3
|
+
|
|
4
|
+
const messages = ["old", "image", "answer", "tail"];
|
|
5
|
+
|
|
6
|
+
test("image position age inherits its next positive tag, never zero or an earlier tag", () => {
|
|
7
|
+
const policy = createProcessedImageEligibility(
|
|
8
|
+
messages,
|
|
9
|
+
new Map([
|
|
10
|
+
["old", 1],
|
|
11
|
+
["answer", 6],
|
|
12
|
+
]),
|
|
13
|
+
{ watermark: 1 },
|
|
14
|
+
);
|
|
15
|
+
expect(policy.ageTags.get("image")).toBe(6);
|
|
16
|
+
expect(policy.mayStrip("image")).toBe(false);
|
|
17
|
+
expect(policy.ageTags.has("tail")).toBe(false);
|
|
18
|
+
expect(policy.mayStrip("tail")).toBe(false);
|
|
19
|
+
const own = createProcessedImageEligibility(
|
|
20
|
+
messages,
|
|
21
|
+
new Map([
|
|
22
|
+
["image", 3],
|
|
23
|
+
["answer", 6],
|
|
24
|
+
]),
|
|
25
|
+
{ watermark: 4 },
|
|
26
|
+
);
|
|
27
|
+
expect(own.ageTags.get("image")).toBe(3);
|
|
28
|
+
expect(own.mayStrip("image")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("answered image-only positions outside the protected window are eligible", () => {
|
|
32
|
+
const policy = createProcessedImageEligibility(
|
|
33
|
+
messages,
|
|
34
|
+
new Map([
|
|
35
|
+
["answer", 3],
|
|
36
|
+
["tail", 5],
|
|
37
|
+
]),
|
|
38
|
+
{ watermark: 4, protectedCutoff: 5, contextPercentage: 94.99 },
|
|
39
|
+
);
|
|
40
|
+
expect(policy.ageTags.get("image")).toBe(3);
|
|
41
|
+
expect(policy.mayStrip("image")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("image positions inside the protected window stay below the 95 percent emergency", () => {
|
|
45
|
+
const policy = createProcessedImageEligibility(messages, new Map([["answer", 6]]), {
|
|
46
|
+
watermark: 10,
|
|
47
|
+
protectedCutoff: 5,
|
|
48
|
+
contextPercentage: 94.99,
|
|
49
|
+
});
|
|
50
|
+
expect(policy.mayStrip("image")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("image positions follow emergency drops when the newest window yields at 95 percent", () => {
|
|
54
|
+
for (const contextPercentage of [95, 99]) {
|
|
55
|
+
const policy = createProcessedImageEligibility(messages, new Map([["answer", 6]]), {
|
|
56
|
+
watermark: 10,
|
|
57
|
+
protectedCutoff: 5,
|
|
58
|
+
contextPercentage,
|
|
59
|
+
});
|
|
60
|
+
expect(policy.mayStrip("image")).toBe(true);
|
|
61
|
+
const notDroppedYet = createProcessedImageEligibility(messages, new Map([["answer", 11]]), {
|
|
62
|
+
watermark: 10,
|
|
63
|
+
protectedCutoff: 5,
|
|
64
|
+
contextPercentage,
|
|
65
|
+
});
|
|
66
|
+
expect(notDroppedYet.mayStrip("image")).toBe(false);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared processed-image age and recency policy for both OpenCode adapters.
|
|
3
|
+
* Use the owning message's positive maximum tag; an untagged image-only message
|
|
4
|
+
* inherits the next positive tagged message after it, never age zero or a tag
|
|
5
|
+
* from an earlier message. Without either tag, retain the image at the live tail.
|
|
6
|
+
* A positive dropped-tag watermark must cover that position. Below 95% usage,
|
|
7
|
+
* retain every position at or beyond the protected newest-window cutoff. At
|
|
8
|
+
* 95% and above only that window yields, exactly as for emergency tool drops;
|
|
9
|
+
* independent per-tool protections still apply. Content before retained provider-signed
|
|
10
|
+
* thinking must stay unchanged unless an existing whole-turn rewrite removes that
|
|
11
|
+
* thinking first. Callers enforce these protections, later-assistant/stable-id checks, durable
|
|
12
|
+
* persistence and rewrite-only discovery. Frozen replay bypasses eligibility.
|
|
13
|
+
*/
|
|
14
|
+
export function createProcessedImageEligibility<T>(
|
|
15
|
+
messages: readonly T[],
|
|
16
|
+
messageTagNumbers: ReadonlyMap<T, number>,
|
|
17
|
+
options: { watermark: number; protectedCutoff?: number | null; contextPercentage?: number },
|
|
18
|
+
): { ageTags: ReadonlyMap<T, number>; mayStrip: (message: T) => boolean } {
|
|
19
|
+
const ageTags = new Map<T, number>();
|
|
20
|
+
let nextTag: number | undefined;
|
|
21
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
22
|
+
const message = messages[index];
|
|
23
|
+
const ownTag = messageTagNumbers.get(message);
|
|
24
|
+
if (ownTag !== undefined && Number.isFinite(ownTag) && ownTag > 0) nextTag = ownTag;
|
|
25
|
+
if (nextTag !== undefined) ageTags.set(message, nextTag);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ageTags,
|
|
29
|
+
mayStrip: (message) => {
|
|
30
|
+
const age = ageTags.get(message);
|
|
31
|
+
return (
|
|
32
|
+
options.watermark > 0 &&
|
|
33
|
+
age !== undefined &&
|
|
34
|
+
age <= options.watermark &&
|
|
35
|
+
((options.contextPercentage ?? 0) >= 95 ||
|
|
36
|
+
options.protectedCutoff == null ||
|
|
37
|
+
age < options.protectedCutoff)
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "own maximum wins over a later smaller tag", "tags": [[2, 10, 4], [3]], "target": 0, "watermark": 8, "cutoff": null, "percentage": 60, "age": 10, "eligible": false },
|
|
3
|
+
{ "name": "own maximum at watermark", "tags": [[2, 10, 4], [30]], "target": 0, "watermark": 10, "cutoff": null, "percentage": 60, "age": 10, "eligible": true },
|
|
4
|
+
{ "name": "first later tag not largest later tag", "tags": [[], [3], [20]], "target": 0, "watermark": 3, "cutoff": null, "percentage": 60, "age": 3, "eligible": true },
|
|
5
|
+
{ "name": "first later message contributes its own maximum", "tags": [[], [2, 9], [3]], "target": 0, "watermark": 5, "cutoff": null, "percentage": 60, "age": 9, "eligible": false },
|
|
6
|
+
{ "name": "skip empty and zero tags when looking forward", "tags": [[1], [], [0], [4]], "target": 1, "watermark": 4, "cutoff": null, "percentage": 60, "age": 4, "eligible": true },
|
|
7
|
+
{ "name": "zero own tag inherits later positive", "tags": [[0], [7]], "target": 0, "watermark": 6, "cutoff": null, "percentage": 60, "age": 7, "eligible": false },
|
|
8
|
+
{ "name": "preceding tag cannot age live tail", "tags": [[1], [], []], "target": 1, "watermark": 100, "cutoff": null, "percentage": 60, "age": null, "eligible": false },
|
|
9
|
+
{ "name": "all untagged is ineligible", "tags": [[], []], "target": 0, "watermark": 100, "cutoff": null, "percentage": 60, "age": null, "eligible": false },
|
|
10
|
+
{ "name": "watermark zero even with emergency", "tags": [[], [1]], "target": 0, "watermark": 0, "cutoff": 1, "percentage": 95, "age": 1, "eligible": false },
|
|
11
|
+
{ "name": "age above watermark even with emergency", "tags": [[], [7]], "target": 0, "watermark": 6, "cutoff": 1, "percentage": 100, "age": 7, "eligible": false },
|
|
12
|
+
{ "name": "cutoff inclusive at 94.99 percent", "tags": [[5], [8]], "target": 0, "watermark": 9, "cutoff": 5, "percentage": 94.99, "age": 5, "eligible": false },
|
|
13
|
+
{ "name": "inside cutoff is a range not set membership", "tags": [[], [6]], "target": 0, "watermark": 9, "cutoff": 5, "percentage": 60, "age": 6, "eligible": false },
|
|
14
|
+
{ "name": "below cutoff is eligible", "tags": [[4], [8]], "target": 0, "watermark": 9, "cutoff": 5, "percentage": 94.99, "age": 4, "eligible": true },
|
|
15
|
+
{ "name": "exactly 95 waives cutoff", "tags": [[], [6]], "target": 0, "watermark": 9, "cutoff": 5, "percentage": 95, "age": 6, "eligible": true },
|
|
16
|
+
{ "name": "above 95 waives cutoff", "tags": [[5], [8]], "target": 0, "watermark": 9, "cutoff": 5, "percentage": 95.01, "age": 5, "eligible": true },
|
|
17
|
+
{ "name": "emergency cannot invent tail age", "tags": [[1], []], "target": 1, "watermark": 100, "cutoff": 1, "percentage": 100, "age": null, "eligible": false },
|
|
18
|
+
{ "name": "own positive tag overrides later greater tag", "tags": [[2], [20]], "target": 0, "watermark": 2, "cutoff": 3, "percentage": 60, "age": 2, "eligible": true },
|
|
19
|
+
{ "name": "last message with own tag is aged", "tags": [[1], [4]], "target": 1, "watermark": 4, "cutoff": null, "percentage": 60, "age": 4, "eligible": true }
|
|
20
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createProcessedImageEligibility } from "./processed-image-eligibility";
|
|
3
|
+
import cases from "./processed-image-position-review.json";
|
|
4
|
+
|
|
5
|
+
// OpenCode, Rust and Pi can reuse these cases: each row supplies per-message
|
|
6
|
+
// tag numbers rather than provider-specific message objects or image encodings.
|
|
7
|
+
for (const row of cases) {
|
|
8
|
+
test(`independent image position table: ${row.name}`, () => {
|
|
9
|
+
const messages = row.tags.map((_, i) => `message-${i}`);
|
|
10
|
+
const tags = new Map(messages.map((message, i) => [message, Math.max(0, ...row.tags[i])]));
|
|
11
|
+
const policy = createProcessedImageEligibility(messages, tags, {
|
|
12
|
+
watermark: row.watermark,
|
|
13
|
+
protectedCutoff: row.cutoff,
|
|
14
|
+
contextPercentage: row.percentage,
|
|
15
|
+
});
|
|
16
|
+
expect(policy.ageTags.get(messages[row.target]) ?? null).toBe(row.age);
|
|
17
|
+
expect(policy.mayStrip(messages[row.target])).toBe(row.eligible);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -20,6 +20,26 @@ export function projectDirectoryKey(directory: string): string {
|
|
|
20
20
|
return path.resolve(directory);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether two spellings name the same directory, judged by spelling alone.
|
|
25
|
+
*
|
|
26
|
+
* Hosts and the plugin do not agree on how to write a directory: OpenCode 2 on
|
|
27
|
+
* Windows stores a session's directory as `D:/work/app` while the plugin's own
|
|
28
|
+
* location is `D:\work\app`, and a drive letter can arrive in either case.
|
|
29
|
+
* Both sides go through `projectDirectoryKey`, so separators, Windows case,
|
|
30
|
+
* `\\?\` prefixes, trailing separators and `.`/`..` segments cannot make one
|
|
31
|
+
* directory look like two. POSIX paths keep their case. Symlinks are not
|
|
32
|
+
* resolved here; callers that must follow them use `canonicalProjectDirectory`.
|
|
33
|
+
*
|
|
34
|
+
* An empty spelling never matches anything: `path.resolve("")` is the process's
|
|
35
|
+
* working directory, and a session with no recorded directory must not be
|
|
36
|
+
* taken for one in the directory the process happens to run in.
|
|
37
|
+
*/
|
|
38
|
+
export function sameProjectDirectory(a: string, b: string): boolean {
|
|
39
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
40
|
+
return projectDirectoryKey(a) === projectDirectoryKey(b);
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
let realpathForCanonicalDirectory: (directory: string) => string = (directory) =>
|
|
24
44
|
realpathSync.native(directory);
|
|
25
45
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { recoveryReasoningPrefixes } from "./recovery-reasoning-prefix";
|
|
3
|
+
|
|
4
|
+
for (const key of ["content", "parts"] as const) {
|
|
5
|
+
test(`recovery reasoning ${key}: exact preceding messages and same-message parts`, () => {
|
|
6
|
+
const messages = [
|
|
7
|
+
{ role: "user", [key]: [{ type: "text", text: "original" }] },
|
|
8
|
+
{
|
|
9
|
+
role: "assistant",
|
|
10
|
+
[key]: [
|
|
11
|
+
{ type: "text", text: "before" },
|
|
12
|
+
{ type: "thinking", thinkingSignature: "first" },
|
|
13
|
+
{ type: "text", text: "between" },
|
|
14
|
+
{ type: "redacted_thinking", data: "opaque" },
|
|
15
|
+
{ type: "text", text: "after" },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
const original = recoveryReasoningPrefixes(messages, key);
|
|
20
|
+
expect(original).toHaveLength(2);
|
|
21
|
+
const edited = structuredClone(messages);
|
|
22
|
+
edited[0]![key]![0]!.text = "changed";
|
|
23
|
+
expect(recoveryReasoningPrefixes(edited, key)).not.toEqual(original);
|
|
24
|
+
const parts = structuredClone(messages);
|
|
25
|
+
parts[1]![key]![2]!.text = "changed";
|
|
26
|
+
expect(recoveryReasoningPrefixes(parts, key)[0]).toBe(original[0]);
|
|
27
|
+
expect(recoveryReasoningPrefixes(parts, key)[1]).not.toBe(original[1]);
|
|
28
|
+
const suffix = structuredClone(messages);
|
|
29
|
+
suffix[1]![key]![4]!.text = "changed";
|
|
30
|
+
expect(recoveryReasoningPrefixes(suffix, key)).toEqual(original);
|
|
31
|
+
suffix[1]![key]!.splice(3, 1);
|
|
32
|
+
expect(recoveryReasoningPrefixes(suffix, key)).not.toEqual(original);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize each request prefix ending at a thinking/reasoning part, including
|
|
3
|
+
* earlier messages and earlier parts of that message. Comparing these snapshots
|
|
4
|
+
* detects edits to bytes authenticated by a kept signature, or missing blocks.
|
|
5
|
+
* Include reasoning without a visible signature too: a host may carry its
|
|
6
|
+
* provider signature in fields this helper does not interpret.
|
|
7
|
+
*/
|
|
8
|
+
export function recoveryReasoningPrefixes(
|
|
9
|
+
messages: readonly unknown[],
|
|
10
|
+
partsKey: "parts" | "content",
|
|
11
|
+
): string[] {
|
|
12
|
+
const prefixes: string[] = [];
|
|
13
|
+
for (let index = 0; index < messages.length; index++) {
|
|
14
|
+
const message = messages[index] as Record<string, unknown> | null;
|
|
15
|
+
const parts = message?.[partsKey];
|
|
16
|
+
if (!Array.isArray(parts)) continue;
|
|
17
|
+
for (let p = 0; p < parts.length; p++) {
|
|
18
|
+
const type = (parts[p] as { type?: string } | null)?.type;
|
|
19
|
+
if (
|
|
20
|
+
type !== "thinking" &&
|
|
21
|
+
type !== "reasoning" &&
|
|
22
|
+
type !== "redactedThinking" &&
|
|
23
|
+
type !== "redacted_thinking"
|
|
24
|
+
)
|
|
25
|
+
continue;
|
|
26
|
+
prefixes.push(
|
|
27
|
+
JSON.stringify([
|
|
28
|
+
...messages.slice(0, index),
|
|
29
|
+
{ ...message, [partsKey]: parts.slice(0, p + 1) },
|
|
30
|
+
]),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return prefixes;
|
|
35
|
+
}
|
|
@@ -118,7 +118,7 @@ test("exhausted routed acquisition never enters the callback or multiplies privi
|
|
|
118
118
|
db.close();
|
|
119
119
|
rmSync(dir, { recursive: true, force: true });
|
|
120
120
|
}
|
|
121
|
-
});
|
|
121
|
+
}, 30_000);
|
|
122
122
|
|
|
123
123
|
for (const mode of ["run", "get", "all", "exec"] as const) {
|
|
124
124
|
test(`foreground ${mode} autocommit waits share one turn budget and restore production timeout`, () => {
|
|
@@ -147,7 +147,8 @@ for (const mode of ["run", "get", "all", "exec"] as const) {
|
|
|
147
147
|
}
|
|
148
148
|
expect(() => db.exec("BEGIN IMMEDIATE")).toThrow("acquisition remained busy");
|
|
149
149
|
});
|
|
150
|
-
expect(performance.now() - started).
|
|
150
|
+
expect(performance.now() - started).toBeGreaterThanOrEqual(1900);
|
|
151
|
+
expect(performance.now() - started).toBeLessThan(2600);
|
|
151
152
|
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
152
153
|
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
153
154
|
blocker.exec("ROLLBACK");
|
|
@@ -159,5 +160,5 @@ for (const mode of ["run", "get", "all", "exec"] as const) {
|
|
|
159
160
|
db.close();
|
|
160
161
|
rmSync(dir, { recursive: true, force: true });
|
|
161
162
|
}
|
|
162
|
-
});
|
|
163
|
+
}, 30_000);
|
|
163
164
|
}
|