@cortexkit/opencode-magic-context 0.43.2 → 0.44.1
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/README.md +18 -2
- package/dist/agents/magic-context-prompt.d.ts +2 -0
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts +18 -0
- package/dist/config/live-child-output-cap.d.ts.map +1 -0
- package/dist/config/project-security.d.ts +6 -3
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +28 -1
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-hyk9y29x.js → conflict-warning-hook-d14ah73v.js} +6 -6
- package/dist/{derive-budgets-b8ybssh9.js → derive-budgets-gd2270s0.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +3 -2
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/curate-category-rotation.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts +13 -0
- package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +5 -2
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/module-apply.d.ts +1 -0
- package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
- package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-learnings.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +2 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +19 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +7 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/index.d.ts +1 -0
- package/dist/features/magic-context/index.d.ts.map +1 -1
- package/dist/features/magic-context/install-instance-id.d.ts +33 -0
- package/dist/features/magic-context/install-instance-id.d.ts.map +1 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +7 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts +35 -0
- package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts.map +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +23 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +31 -8
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +40 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
- package/dist/features/magic-context/session-decision-calibration.d.ts.map +1 -1
- 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-meta-persisted.d.ts +20 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +1 -0
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-unavailable-reason.d.ts +17 -0
- package/dist/features/magic-context/storage-unavailable-reason.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/store-generation-rebase.d.ts +33 -1
- package/dist/features/magic-context/store-generation-rebase.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +8 -3
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +105 -9
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/context-limit-resolution.d.ts +31 -0
- package/dist/hooks/magic-context/context-limit-resolution.d.ts.map +1 -0
- package/dist/hooks/magic-context/embed-history-runner.d.ts +0 -1
- package/dist/hooks/magic-context/embed-history-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts +233 -0
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook.d.ts +1 -3
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/image-token-estimate.d.ts +9 -0
- package/dist/hooks/magic-context/image-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +60 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +6 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-client.d.ts +21 -0
- package/dist/hooks/magic-context/module-client.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts +6 -2
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-tool-backends.d.ts +47 -0
- package/dist/hooks/magic-context/module-tool-backends.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-transport.d.ts +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/persist-filtered-noise.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +28 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +35 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts +18 -0
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-served-m0.d.ts +20 -0
- package/dist/hooks/magic-context/rust-served-m0.d.ts.map +1 -0
- package/dist/hooks/magic-context/store-ahead-refusal.d.ts +40 -0
- package/dist/hooks/magic-context/store-ahead-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +5 -5
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- 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/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +14 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-input-size.d.ts +21 -0
- package/dist/hooks/magic-context/tool-input-size.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-sweep-policy.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +124 -11
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +14 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-pa67brzn.js → index-0rp6pj54.js} +3 -3
- package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
- package/dist/{index-wgzyeyf5.js → index-3bygzk2d.js} +23 -7
- package/dist/{index-jgnag7sa.js → index-a9vbtdzv.js} +4 -4
- package/dist/{index-bmzt6xgy.js → index-cz88kw0n.js} +13 -3
- package/dist/{index-6j8th2hd.js → index-d107az47.js} +2 -2
- package/dist/{index-g6hpfkqa.js → index-jtdrvmcn.js} +3290 -286
- package/dist/{index-9r1cbh9e.js → index-kr5qehbd.js} +1 -1
- package/dist/{index-febg06dy.js → index-kwe03yde.js} +1 -1
- package/dist/{index-tdrv6nd0.js → index-s6b7h12g.js} +352 -71
- package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
- package/dist/{index-0ccmyzbj.js → index-stb5fg12.js} +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5230 -5663
- package/dist/{message-time-backfill-0ccvyk9n.js → message-time-backfill-q374fxc5.js} +4 -4
- package/dist/plugin/dream-timer.d.ts +2 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/native-compaction-guard.d.ts +22 -0
- package/dist/plugin/native-compaction-guard.d.ts.map +1 -0
- package/dist/plugin/rpc-handlers.d.ts +11 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-53a7qbrh.js → read-session-chunk-fkdv2g5b.js} +8 -4
- package/dist/{recomp-orchestrator-adsgqgse.js → recomp-orchestrator-k4h5n0p3.js} +9 -9
- package/dist/{safe-notification-target-vrkjrbgb.js → safe-notification-target-50gm9c58.js} +3 -3
- package/dist/{send-session-notification-e28r4xx2.js → send-session-notification-g2k03zyn.js} +5 -5
- package/dist/shared/cache-ttl-display.d.ts +3 -1
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +11 -0
- package/dist/shared/harness.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +9 -0
- package/dist/shared/model-cache-ttl.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +13 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +25 -0
- package/dist/shared/prompt-async-transport.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +17 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +3 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts +7 -0
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -0
- package/dist/shared/status-summary.d.ts +9 -1
- package/dist/shared/status-summary.d.ts.map +1 -1
- package/dist/shared/status-view.d.ts +1 -1
- package/dist/shared/status-view.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +10 -5
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +6 -3
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/v2/fold/boundary.d.ts +61 -0
- package/dist/v2/fold/boundary.d.ts.map +1 -0
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +16 -3
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +45 -14
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +6 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/rpc-live-state.d.ts +1 -1
- package/dist/v2/hooks/rpc-live-state.d.ts.map +1 -1
- package/dist/v2/hooks/rust-mode.d.ts +47 -0
- package/dist/v2/hooks/rust-mode.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +15 -0
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -0
- package/dist/v2/hooks/storage-gate.d.ts +42 -0
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -0
- package/dist/v2/hooks/storage-notice.d.ts +29 -0
- package/dist/v2/hooks/storage-notice.d.ts.map +1 -0
- package/dist/v2/hooks/store.d.ts +16 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/tools.d.ts +11 -2
- package/dist/v2/hooks/tools.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +4 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/hooks/usage-persist.d.ts +31 -0
- package/dist/v2/hooks/usage-persist.d.ts.map +1 -0
- package/dist/v2/server.js +27201 -22695
- package/dist/v2/store-reader.d.ts +9 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/wrapup-orchestrator-35y56636.js +18 -0
- package/package.json +1 -1
- package/src/agents/magic-context-prompt.test.ts +125 -0
- package/src/agents/magic-context-prompt.ts +15 -10
- package/src/config/index.ts +40 -0
- package/src/config/live-child-output-cap.test.ts +75 -0
- package/src/config/live-child-output-cap.ts +70 -0
- package/src/config/live-run-config.test.ts +24 -11
- package/src/config/misplaced-agent-model.test.ts +61 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/project-security.ts +28 -4
- package/src/config/schema/magic-context.test.ts +16 -1
- package/src/config/schema/magic-context.ts +35 -6
- package/src/shared/cache-ttl-display.ts +14 -20
- package/src/shared/harness.ts +14 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/logger.test.ts +63 -2
- package/src/shared/logger.ts +23 -5
- package/src/shared/model-cache-ttl.test.ts +40 -0
- package/src/shared/model-cache-ttl.ts +58 -0
- package/src/shared/model-suggestion-retry.test.ts +115 -4
- package/src/shared/model-suggestion-retry.ts +57 -12
- package/src/shared/prompt-async-transport.test.ts +217 -0
- package/src/shared/prompt-async-transport.ts +242 -0
- package/src/shared/prompt-surface-a1-golden.md +17 -17
- package/src/shared/rpc-server.ts +13 -1
- package/src/shared/rpc-types.ts +18 -1
- package/src/shared/rpc-utils.ts +28 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +198 -0
- package/src/shared/sqlite-write-locker-test-support.ts +62 -0
- package/src/shared/status-detail-text.test.ts +23 -0
- package/src/shared/status-summary.ts +25 -1
- package/src/shared/status-view.ts +1 -1
- package/src/shared/tag-transcript.ts +82 -19
- package/src/shared/user-facing-codes.ts +12 -5
- package/src/shared/window-geometry.ts +6 -3
- package/dist/v2/hooks/provider-admission.d.ts +0 -22
- package/dist/v2/hooks/provider-admission.d.ts.map +0 -1
- package/dist/wrapup-orchestrator-8xzbpvm8.js +0 -18
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { resolveHistorianModel } from "../shared/model-resolution";
|
|
5
|
+
import { misplacedAgentModelWarnings, parsePluginConfig } from "./index";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A bare `historian.model` is schema-valid, reads naturally, and resolves to
|
|
9
|
+
* nothing. These tests pin both halves: that the resolver really does ignore it,
|
|
10
|
+
* and that the loader now says so instead of leaving the user with a subsystem
|
|
11
|
+
* that looks configured and never runs.
|
|
12
|
+
*/
|
|
13
|
+
describe("misplaced agent model warning", () => {
|
|
14
|
+
it("is what a top-level historian.model actually resolves to", () => {
|
|
15
|
+
// The premise, asserted rather than assumed: if this ever starts
|
|
16
|
+
// resolving, the warning below becomes wrong and should be deleted.
|
|
17
|
+
expect(resolveHistorianModel({ historian: { model: "openai/gpt-5" } }, "opencode")).toEqual(
|
|
18
|
+
{
|
|
19
|
+
primary: undefined,
|
|
20
|
+
fallbacks: [],
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("warns when the model sits outside every harness block", () => {
|
|
26
|
+
const warnings = misplacedAgentModelWarnings({ historian: { model: "openai/gpt-5" } });
|
|
27
|
+
expect(warnings).toHaveLength(1);
|
|
28
|
+
expect(warnings[0]).toContain("historian.model is not read");
|
|
29
|
+
expect(warnings[0]).toContain("historian.opencode.model");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("stays quiet when a harness block carries the model", () => {
|
|
33
|
+
expect(
|
|
34
|
+
misplacedAgentModelWarnings({
|
|
35
|
+
historian: { model: "openai/gpt-5", opencode: { model: "openai/gpt-5" } },
|
|
36
|
+
}),
|
|
37
|
+
).toEqual([]);
|
|
38
|
+
expect(
|
|
39
|
+
misplacedAgentModelWarnings({ historian: { opencode: { model: "openai/gpt-5" } } }),
|
|
40
|
+
).toEqual([]);
|
|
41
|
+
expect(misplacedAgentModelWarnings({})).toEqual([]);
|
|
42
|
+
expect(misplacedAgentModelWarnings({ historian: { disable: true } })).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("covers the dreamer on the same rule", () => {
|
|
46
|
+
const warnings = misplacedAgentModelWarnings({ dreamer: { model: "openai/gpt-5" } });
|
|
47
|
+
expect(warnings).toHaveLength(1);
|
|
48
|
+
expect(warnings[0]).toContain("dreamer.model is not read");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("reaches the pinned config-warning channel", () => {
|
|
52
|
+
// configWarnings is what /ctx-status, the Pi session-start notice and
|
|
53
|
+
// doctor read, so landing here is what makes the warning visible.
|
|
54
|
+
const parsed = parsePluginConfig({ historian: { model: "openai/gpt-5" } });
|
|
55
|
+
expect(
|
|
56
|
+
(parsed.configWarnings ?? []).some((warning) =>
|
|
57
|
+
warning.includes("historian.model is not read"),
|
|
58
|
+
),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -225,6 +225,23 @@ describe("stripUnsafeProjectConfigFields", () => {
|
|
|
225
225
|
expect(warning).toContain("historian.pi.thinking_level");
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
it("strips the historian runner from project config so a repo cannot redirect the completion", () => {
|
|
229
|
+
const raw: Record<string, unknown> = {
|
|
230
|
+
historian: {
|
|
231
|
+
runner: "host",
|
|
232
|
+
opencode: { runner: "host" },
|
|
233
|
+
temperature: 0.2,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const warnings = stripUnsafeProjectConfigFields(raw);
|
|
238
|
+
|
|
239
|
+
expect(raw.historian).toEqual({ opencode: {}, temperature: 0.2 });
|
|
240
|
+
const warning = warnings.join("\n");
|
|
241
|
+
expect(warning).toContain("historian.runner");
|
|
242
|
+
expect(warning).toContain("historian.opencode.runner");
|
|
243
|
+
});
|
|
244
|
+
|
|
228
245
|
it("strips mural.model from project config but keeps the feature switch", () => {
|
|
229
246
|
const raw: Record<string, unknown> = {
|
|
230
247
|
mural: { enabled: true, model: "repo-controlled-model" },
|
|
@@ -23,7 +23,17 @@ const HARNESS_KEYS = PER_HARNESS_MODEL_KEYS;
|
|
|
23
23
|
/** Every historian model-resolution field, including per-harness qualifiers.
|
|
24
24
|
* Variant and thinking_level merge onto the user's historian model at resolve
|
|
25
25
|
* time, so leaving them would let a cloned repo force extra spend. */
|
|
26
|
-
const HISTORIAN_USER_ONLY_FIELDS =
|
|
26
|
+
const HISTORIAN_USER_ONLY_FIELDS = [
|
|
27
|
+
...PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution,
|
|
28
|
+
// `runner` chooses which process and which provider account runs the hidden
|
|
29
|
+
// historian completion. A cloned repo redirecting it would move that spend and
|
|
30
|
+
// that prompt text somewhere the user never agreed to.
|
|
31
|
+
"runner",
|
|
32
|
+
// `host_runner` turns this machine's pull loop on and off. A cloned repo
|
|
33
|
+
// turning it ON would start spending the user's provider account on folds for
|
|
34
|
+
// every project this process serves, not just its own.
|
|
35
|
+
"host_runner",
|
|
36
|
+
] as const;
|
|
27
37
|
const PROMPT_SURFACE_USER_ONLY_FIELDS = ["guidance_override_path", "tool_descriptions"] as const;
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -348,10 +358,13 @@ function makeProjectThresholdWarning(field: string, reason: string): string {
|
|
|
348
358
|
* can opt its own runtime into the experimental Rust pipeline. The resolver
|
|
349
359
|
* requires trusted user-level `subc` configuration before Rust can activate.
|
|
350
360
|
* - historian model-resolution fields (model, fallback_models, variant,
|
|
351
|
-
* thinking_level)
|
|
352
|
-
* spend is user-level only. Qualifiers merge onto the user's historian
|
|
361
|
+
* thinking_level) and `runner`, including both per-harness blocks — historian
|
|
362
|
+
* model spend is user-level only. Qualifiers merge onto the user's historian
|
|
353
363
|
* model at resolve time, so a cloned repo cannot force extra thinking or
|
|
354
|
-
* variant cost
|
|
364
|
+
* variant cost, and cannot move the completion to a different process or
|
|
365
|
+
* provider account. `host_runner` is stripped for the same reason in the other
|
|
366
|
+
* direction: a repo must not switch this machine's pull loop on and start
|
|
367
|
+
* spending the user's provider account on folds.
|
|
355
368
|
* - `mural.model` at the top-level block, the legacy experimental spelling,
|
|
356
369
|
* and any nested `mural.model` under hidden agents — a cloned repo cannot
|
|
357
370
|
* choose where project memory is sent.
|
|
@@ -579,6 +592,17 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record<string, unknow
|
|
|
579
592
|
}
|
|
580
593
|
}
|
|
581
594
|
|
|
595
|
+
// `dreamer.runner` chooses which process and which provider account runs the
|
|
596
|
+
// module-routed dreamer completions, for the same reason `historian.runner`
|
|
597
|
+
// is user-level only.
|
|
598
|
+
const dreamer = projectRaw.dreamer;
|
|
599
|
+
if (isPlainObject(dreamer) && "runner" in dreamer) {
|
|
600
|
+
delete dreamer.runner;
|
|
601
|
+
warnings.push(
|
|
602
|
+
"Ignoring dreamer.runner from project config (security: which process and provider account run dreamer completions is a user-level setting).",
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
582
606
|
const mural = projectRaw.mural;
|
|
583
607
|
if (isPlainObject(mural) && "model" in mural) {
|
|
584
608
|
delete mural.model;
|
|
@@ -38,7 +38,7 @@ describe("MagicContextConfigSchema", () => {
|
|
|
38
38
|
retrieval_count_promotion_threshold: 3,
|
|
39
39
|
},
|
|
40
40
|
todowrite: {
|
|
41
|
-
enabled:
|
|
41
|
+
enabled: false,
|
|
42
42
|
overlay: true,
|
|
43
43
|
},
|
|
44
44
|
});
|
|
@@ -50,6 +50,21 @@ describe("MagicContextConfigSchema", () => {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("defaults todowrite off but honors explicit enablement and disablement", () => {
|
|
54
|
+
expect(MagicContextConfigSchema.parse({}).todowrite).toEqual({
|
|
55
|
+
enabled: false,
|
|
56
|
+
overlay: true,
|
|
57
|
+
});
|
|
58
|
+
expect(MagicContextConfigSchema.parse({ todowrite: {} }).todowrite.enabled).toBe(false);
|
|
59
|
+
expect(MagicContextConfigSchema.parse({ todowrite: { enabled: true } }).todowrite).toEqual({
|
|
60
|
+
enabled: true,
|
|
61
|
+
overlay: true,
|
|
62
|
+
});
|
|
63
|
+
expect(
|
|
64
|
+
MagicContextConfigSchema.parse({ todowrite: { enabled: false } }).todowrite.enabled,
|
|
65
|
+
).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
53
68
|
describe("budget configuration", () => {
|
|
54
69
|
it("accepts 90% execute thresholds and rejects 91%", () => {
|
|
55
70
|
expect(
|
|
@@ -624,6 +624,12 @@ const AgentMetadataSchema = AgentOverrideConfigSchema.pick({
|
|
|
624
624
|
|
|
625
625
|
/** Combined dreamer metadata plus independent strict execution blocks. */
|
|
626
626
|
export const DreamerConfigSchema = AgentMetadataSchema.extend({
|
|
627
|
+
runner: z
|
|
628
|
+
.enum(["broca", "host"])
|
|
629
|
+
.optional()
|
|
630
|
+
.describe(
|
|
631
|
+
'Which side runs the dreamer completions the Rust module routes (classify-memories) in Rust transform mode: "host" runs them on this process\'s carrier, "broca" routes them to the Broca module. When unset, historian.runner applies, and when that is unset too the harness decides the same way it does for the historian. User-level config only.',
|
|
632
|
+
),
|
|
627
633
|
opencode: DreamerOpenCodeHarnessBlockSchema.optional(),
|
|
628
634
|
pi: DreamerPiHarnessBlockSchema.optional(),
|
|
629
635
|
omp: DreamerOmpHarnessBlockSchema.optional(),
|
|
@@ -645,6 +651,25 @@ export const HistorianConfigSchema = AgentMetadataSchema.extend({
|
|
|
645
651
|
opencode: OpenCodeHarnessBlockSchema.optional(),
|
|
646
652
|
pi: PiHarnessBlockSchema.optional(),
|
|
647
653
|
omp: OmpHarnessBlockSchema.optional(),
|
|
654
|
+
runner: z
|
|
655
|
+
.enum(["broca", "host"])
|
|
656
|
+
.optional()
|
|
657
|
+
.describe(
|
|
658
|
+
'Which side runs the historian completion in Rust transform mode: "host" queues it for this process to run on the configured historian model, "broca" routes it to the Broca module. When unset the harness decides: OpenCode 1 and OpenCode 2 use "host", Claude Code (through the Thalamus gateway, which has no host to run a completion) uses "broca". User-level config only — it decides whose provider account pays for the call.',
|
|
659
|
+
),
|
|
660
|
+
host_runner: z
|
|
661
|
+
.object({
|
|
662
|
+
enabled: z
|
|
663
|
+
.boolean()
|
|
664
|
+
.optional()
|
|
665
|
+
.describe(
|
|
666
|
+
"Whether this process answers historian runs queued for a claimant (default true). Setting it to false stops the pull loop without changing historian.runner, so an operator can take one machine out of the lane and leave the queued runs for another claimant or for the runner setting to be changed deliberately.",
|
|
667
|
+
),
|
|
668
|
+
})
|
|
669
|
+
.optional()
|
|
670
|
+
.describe(
|
|
671
|
+
"Controls for this process's historian pull loop, which answers runs the Rust module queues for the host runner (`historian.runner: \"host\"`, or unset on OpenCode 1 and OpenCode 2). User-level config only — it decides whether this machine's provider account is spent on folds.",
|
|
672
|
+
),
|
|
648
673
|
two_pass: z
|
|
649
674
|
.boolean()
|
|
650
675
|
.default(false)
|
|
@@ -675,7 +700,7 @@ const BaseEmbeddingConfigSchema = z
|
|
|
675
700
|
.enum(["local", "openai-compatible", "off", "synapse"])
|
|
676
701
|
.default("local")
|
|
677
702
|
.describe(
|
|
678
|
-
"Embedding provider. 'local' uses Xenova/all-MiniLM-L6-v2, 'openai-compatible' requires endpoint and model, 'synapse' uses the certified local Synapse lane with an explicit fallback provider, and 'off' disables embeddings.",
|
|
703
|
+
"Embedding provider. 'local' uses Xenova/all-MiniLM-L6-v2, 'openai-compatible' requires endpoint and model, 'synapse' uses the certified local Synapse lane with an explicit fallback provider, and 'off' disables embeddings. Session history is embedded for semantic ctx_search whenever the provider is not 'off', regardless of memory.enabled; memories are embedded only while memory.enabled is true.",
|
|
679
704
|
),
|
|
680
705
|
fallback_provider: EmbeddingFallbackProviderSchema.optional().describe(
|
|
681
706
|
"Fallback provider for the Synapse lane. Required when provider is 'synapse'; local, openai-compatible, and off are valid.",
|
|
@@ -1131,7 +1156,7 @@ export const MagicContextConfigSchema = z
|
|
|
1131
1156
|
.union([z.string(), z.object({ default: z.string() }).catchall(z.string())])
|
|
1132
1157
|
.default("5m")
|
|
1133
1158
|
.describe(
|
|
1134
|
-
'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard,
|
|
1159
|
+
'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard). Explicit per-model entries win; otherwise GPT-5.6 and later (including gpt-6*, through any provider prefix) use a built-in 30m lifetime before the object default or 5m fallback. An unset or global "5m" opts into built-in defaults; any other global string is an explicit policy and wins. Policy is frozen per session, including across restarts; a model switch resolves against that frozen policy. /ctx-status shows the effective value and source. OpenAI documents at least 30 minutes since the latest write or reuse: https://developers.openai.com/api/docs/guides/prompt-caching (Cache lifetime and Summary of model differences). Set to "never" to mean MC never assumes expiry (for lanes kept warm externally by a cache-keep tool) — disables the idle-TTL heuristic so MC never initiates a rebuild based on elapsed time. Provider-side extended TTL is a separate request-level concern (cache_control: { ttl } in the request body).',
|
|
1135
1160
|
),
|
|
1136
1161
|
prompt_surface: PromptSurfaceConfigSchema.default({ default: "full" }).describe(
|
|
1137
1162
|
"Prompt-surface presets: default is full; models use bare model IDs, provider/model, or provider/* routing keys. Guidance and tool-description overrides are user-level only. OpenCode 1.x, Pi, and OMP register tool descriptions once per process (they follow the default preset). OpenCode 2 rewrites the five ctx_* descriptions per request from the draft model.",
|
|
@@ -1363,9 +1388,9 @@ export const MagicContextConfigSchema = z
|
|
|
1363
1388
|
.object({
|
|
1364
1389
|
enabled: z
|
|
1365
1390
|
.boolean()
|
|
1366
|
-
.default(
|
|
1391
|
+
.default(false)
|
|
1367
1392
|
.describe(
|
|
1368
|
-
"Pi only: register Magic Context's todowrite task-list tool
|
|
1393
|
+
"Pi only: off by default. Set todowrite.enabled=true to register Magic Context's todowrite task-list tool and /todos command. OpenCode ships its own built-in todowrite; this setting has no effect there.",
|
|
1369
1394
|
),
|
|
1370
1395
|
overlay: z
|
|
1371
1396
|
.boolean()
|
|
@@ -1374,7 +1399,7 @@ export const MagicContextConfigSchema = z
|
|
|
1374
1399
|
"Pi only: show the persistent todo overlay above the editor while tasks are active.",
|
|
1375
1400
|
),
|
|
1376
1401
|
})
|
|
1377
|
-
.default({ enabled:
|
|
1402
|
+
.default({ enabled: false, overlay: true })
|
|
1378
1403
|
.describe(
|
|
1379
1404
|
"Pi-only todowrite tool and overlay controls. Pi registers tools and widgets at extension boot, so changing this after /cd requires /reload or restart.",
|
|
1380
1405
|
),
|
|
@@ -1413,7 +1438,9 @@ export const MagicContextConfigSchema = z
|
|
|
1413
1438
|
enabled: z
|
|
1414
1439
|
.boolean()
|
|
1415
1440
|
.default(true)
|
|
1416
|
-
.describe(
|
|
1441
|
+
.describe(
|
|
1442
|
+
"Enable cross-session memory (default: true). Does not affect history embedding or semantic ctx_search over session history; set embedding.provider to 'off' to stop all embedding.",
|
|
1443
|
+
),
|
|
1417
1444
|
injection_budget_tokens: z
|
|
1418
1445
|
.number()
|
|
1419
1446
|
.min(500)
|
|
@@ -1525,10 +1552,12 @@ export const LIVE_RELOAD_CONFIG_PATHS = [
|
|
|
1525
1552
|
"historian.omp.fallback_models",
|
|
1526
1553
|
"historian.omp.thinking_level",
|
|
1527
1554
|
"historian.two_pass",
|
|
1555
|
+
"historian.maxTokens",
|
|
1528
1556
|
"historian_timeout_ms",
|
|
1529
1557
|
"commit_cluster_trigger.enabled",
|
|
1530
1558
|
"commit_cluster_trigger.min_clusters",
|
|
1531
1559
|
"memory.auto_promote",
|
|
1560
|
+
"dreamer.maxTokens",
|
|
1532
1561
|
"dreamer.opencode.model",
|
|
1533
1562
|
"dreamer.opencode.fallback_models",
|
|
1534
1563
|
"dreamer.opencode.variant",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MagicContextConfig } from "../config/schema/magic-context";
|
|
2
|
-
import {
|
|
2
|
+
import { type ResolvedCacheTtl, resolveModelCacheTtl } from "./model-cache-ttl";
|
|
3
3
|
|
|
4
|
-
export type CacheTtlDisplaySource = "
|
|
4
|
+
export type CacheTtlDisplaySource = ResolvedCacheTtl["source"] | "session";
|
|
5
5
|
|
|
6
6
|
export interface CacheTtlDisplay {
|
|
7
7
|
value: string;
|
|
@@ -10,6 +10,7 @@ export interface CacheTtlDisplay {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface ResolveCacheTtlDisplayArgs {
|
|
13
|
+
frozen?: ResolvedCacheTtl;
|
|
13
14
|
configured: MagicContextConfig["cache_ttl"];
|
|
14
15
|
configuredExplicitly: boolean;
|
|
15
16
|
modelKey: string | undefined;
|
|
@@ -24,6 +25,7 @@ export interface ResolveCacheTtlDisplayArgs {
|
|
|
24
25
|
* displayed; otherwise use live config because the session value may be for another model.
|
|
25
26
|
*/
|
|
26
27
|
export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheTtlDisplay {
|
|
28
|
+
if (args.frozen) return args.frozen;
|
|
27
29
|
if (
|
|
28
30
|
(args.sessionModelKey && (!args.modelKey || args.sessionModelKey === args.modelKey)) ||
|
|
29
31
|
(!args.modelKey && !args.sessionModelKey && args.sessionValue !== "5m")
|
|
@@ -35,27 +37,19 @@ export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheT
|
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const matched = resolveModelConfigValue(args.configured, args.modelKey);
|
|
47
|
-
if (matched) {
|
|
48
|
-
return { value: matched.value, source: "config", modelKey: args.modelKey };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
value: args.configured.default ?? "5m",
|
|
53
|
-
source: "default",
|
|
54
|
-
modelKey: args.modelKey,
|
|
55
|
-
};
|
|
40
|
+
const resolved = resolveModelCacheTtl(args.configured, args.modelKey);
|
|
41
|
+
if (
|
|
42
|
+
resolved.source === "default" &&
|
|
43
|
+
typeof args.configured === "string" &&
|
|
44
|
+
args.configuredExplicitly
|
|
45
|
+
)
|
|
46
|
+
return { ...resolved, source: "config" };
|
|
47
|
+
return resolved;
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
export function formatCacheTtlDisplay(display: CacheTtlDisplay): string {
|
|
51
|
+
if (display.source === "OpenAI GPT-5.6+ default")
|
|
52
|
+
return `Cache TTL: ${display.value} (${display.source})`;
|
|
59
53
|
if (display.source === "session") return `Cache TTL: ${display.value} (session)`;
|
|
60
54
|
if (display.source === "config") {
|
|
61
55
|
return `Cache TTL: ${display.value} (config for ${display.modelKey ?? "current model"})`;
|
package/src/shared/harness.ts
CHANGED
|
@@ -52,6 +52,20 @@ export function getHarness(): HarnessId {
|
|
|
52
52
|
return currentHarness;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Whether this process may read OpenCode's own session store (opencode.db).
|
|
57
|
+
*
|
|
58
|
+
* Only the OpenCode plugins own that store. A Pi or OMP process keeps its
|
|
59
|
+
* history in Pi JSONL sessions and shares only Magic Context's context.db with
|
|
60
|
+
* OpenCode. Any history read in a Pi process for a session with no Pi provider
|
|
61
|
+
* used to fall through to opencode.db, which let background work load another
|
|
62
|
+
* project's OpenCode sessions into Pi's heap. Pi therefore treats the OpenCode
|
|
63
|
+
* store as absent, the same as on a Pi-only install.
|
|
64
|
+
*/
|
|
65
|
+
export function harnessOwnsOpenCodeStore(harness: HarnessId = currentHarness): boolean {
|
|
66
|
+
return harness === "opencode" || harness === "opencode2";
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
70
|
* Test-only helper to reset harness state between test cases. Do NOT call
|
|
57
71
|
* from production code paths.
|
package/src/shared/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./logger";
|
|
2
2
|
export * from "./model-suggestion-retry";
|
|
3
3
|
export * from "./normalize-sdk-response";
|
|
4
|
+
export * from "./prompt-async-transport";
|
|
4
5
|
export * from "./prompt-surface";
|
|
5
6
|
export * from "./prompt-surface-runtime";
|
|
6
7
|
export * from "./resolve-fallbacks";
|
|
@@ -30,6 +30,8 @@ type RotationScenarioResult = {
|
|
|
30
30
|
files: string[];
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
type RetryScenarioResult = { content: string; failed: number };
|
|
34
|
+
|
|
33
35
|
type HotPathScenarioResult = {
|
|
34
36
|
statCalls: number;
|
|
35
37
|
};
|
|
@@ -49,6 +51,16 @@ if (!root || !loggerModuleUrl || !scenario) {
|
|
|
49
51
|
const logPath = path.join(root, "nested", "magic-context.log");
|
|
50
52
|
process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
|
|
51
53
|
let statCalls = 0;
|
|
54
|
+
if (scenario === "retry") {
|
|
55
|
+
let attempts = 0;
|
|
56
|
+
mock.module("node:fs", () => ({
|
|
57
|
+
...fs,
|
|
58
|
+
appendFileSync: (...args) => {
|
|
59
|
+
if (attempts++ === 0) throw new Error("injected transient write failure");
|
|
60
|
+
return fs.appendFileSync(...args);
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
52
64
|
if (scenario === "hot-path") {
|
|
53
65
|
mock.module("node:fs", () => ({
|
|
54
66
|
...fs,
|
|
@@ -61,7 +73,28 @@ if (scenario === "hot-path") {
|
|
|
61
73
|
|
|
62
74
|
const logger = await import(loggerModuleUrl);
|
|
63
75
|
|
|
64
|
-
if (scenario === "
|
|
76
|
+
if (scenario === "retry") {
|
|
77
|
+
logger.log("first buffered line");
|
|
78
|
+
logger.log("second buffered line");
|
|
79
|
+
logger.flushLogger();
|
|
80
|
+
const failed = logger.getLoggerDiagnostics().swallowedWriteCount;
|
|
81
|
+
logger.log("third buffered line");
|
|
82
|
+
logger.flushLogger();
|
|
83
|
+
console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed }));
|
|
84
|
+
} else if (scenario === "oversized") {
|
|
85
|
+
logger.log("x".repeat(2_000_000));
|
|
86
|
+
logger.flushLogger();
|
|
87
|
+
console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed: logger.getLoggerDiagnostics().swallowedWriteCount }));
|
|
88
|
+
} else if (scenario === "bounded") {
|
|
89
|
+
const failedPath = path.join(root, "unwritable-target");
|
|
90
|
+
fs.mkdirSync(failedPath);
|
|
91
|
+
process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
|
|
92
|
+
for (let i = 0; i < 100; i++) logger.log("line-" + i + "-" + "x".repeat(20_000));
|
|
93
|
+
logger.flushLogger();
|
|
94
|
+
process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
|
|
95
|
+
logger.flushLogger();
|
|
96
|
+
console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed: logger.getLoggerDiagnostics().swallowedWriteCount }));
|
|
97
|
+
} else if (scenario === "rotation") {
|
|
65
98
|
const maxLogBytes = 32 * 1024 * 1024;
|
|
66
99
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
67
100
|
fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "o"));
|
|
@@ -144,7 +177,14 @@ afterEach(() => {
|
|
|
144
177
|
});
|
|
145
178
|
|
|
146
179
|
async function runLoggerScenario<T>(
|
|
147
|
-
scenario:
|
|
180
|
+
scenario:
|
|
181
|
+
| "recovery"
|
|
182
|
+
| "diagnostics"
|
|
183
|
+
| "rotation"
|
|
184
|
+
| "hot-path"
|
|
185
|
+
| "retry"
|
|
186
|
+
| "bounded"
|
|
187
|
+
| "oversized",
|
|
148
188
|
): Promise<T> {
|
|
149
189
|
const root = mkdtempSync(path.join(os.tmpdir(), "magic-context-logger-test-"));
|
|
150
190
|
scenarioRoots.push(root);
|
|
@@ -171,6 +211,27 @@ async function runLoggerScenario<T>(
|
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
describe("logger", () => {
|
|
214
|
+
test("retries the whole buffered batch after a transient append failure", async () => {
|
|
215
|
+
const result = await runLoggerScenario<RetryScenarioResult>("retry");
|
|
216
|
+
expect(result.failed).toBe(1);
|
|
217
|
+
expect(result.content).toContain("first buffered line");
|
|
218
|
+
expect(result.content).toContain("second buffered line");
|
|
219
|
+
expect(result.content).toContain("third buffered line");
|
|
220
|
+
});
|
|
221
|
+
test("reports a single oversized line even if no payload remains buffered", async () => {
|
|
222
|
+
const result = await runLoggerScenario<RetryScenarioResult>("oversized");
|
|
223
|
+
expect(result.content).toMatch(/logger dropped 1 line: buffer exceeded/);
|
|
224
|
+
expect(Buffer.byteLength(result.content)).toBeLessThan(1000);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("bounds failed batches and reports the number of discarded lines", async () => {
|
|
228
|
+
const result = await runLoggerScenario<RetryScenarioResult>("bounded");
|
|
229
|
+
expect(result.failed).toBeGreaterThan(0);
|
|
230
|
+
expect(Buffer.byteLength(result.content)).toBeLessThan(1_100_000);
|
|
231
|
+
expect(result.content).toMatch(/logger dropped \d+ lines: buffer exceeded/);
|
|
232
|
+
expect(result.content).toContain("line-99-");
|
|
233
|
+
});
|
|
234
|
+
|
|
174
235
|
test("recreates a log directory removed while the process is running", async () => {
|
|
175
236
|
const result = await runLoggerScenario<LoggerScenarioResult>("recovery");
|
|
176
237
|
|
package/src/shared/logger.ts
CHANGED
|
@@ -9,6 +9,16 @@ let buffer: string[] = [];
|
|
|
9
9
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
10
|
const FLUSH_INTERVAL_MS = 500;
|
|
11
11
|
const BUFFER_SIZE_LIMIT = 50;
|
|
12
|
+
const MAX_BUFFERED_BYTES = 1024 * 1024;
|
|
13
|
+
let bufferedBytes = 0;
|
|
14
|
+
let droppedLines = 0;
|
|
15
|
+
|
|
16
|
+
function boundBuffer(): void {
|
|
17
|
+
while (bufferedBytes > MAX_BUFFERED_BYTES && buffer.length > 0) {
|
|
18
|
+
bufferedBytes -= Buffer.byteLength(buffer.shift() ?? "");
|
|
19
|
+
droppedLines++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
12
22
|
const MAX_LOG_FILE_BYTES = 32 * 1024 * 1024;
|
|
13
23
|
const SIZE_CHECK_INTERVAL_FLUSHES = 64;
|
|
14
24
|
|
|
@@ -141,11 +151,12 @@ function flush(): void {
|
|
|
141
151
|
clearTimeout(flushTimer);
|
|
142
152
|
flushTimer = null;
|
|
143
153
|
}
|
|
144
|
-
if (buffer.length === 0) return;
|
|
145
|
-
const
|
|
146
|
-
|
|
154
|
+
if (buffer.length === 0 && droppedLines === 0) return;
|
|
155
|
+
const notice = droppedLines
|
|
156
|
+
? `[${new Date().toISOString()}] [magic-context][global] logger dropped ${droppedLines} ${droppedLines === 1 ? "line" : "lines"}: buffer exceeded ${MAX_BUFFERED_BYTES} bytes while writes were pending\n`
|
|
157
|
+
: "";
|
|
147
158
|
try {
|
|
148
|
-
const data = capLogData(
|
|
159
|
+
const data = capLogData(notice + buffer.join(""));
|
|
149
160
|
const logFile = getMagicContextLogPath();
|
|
150
161
|
ensureDir(logFile);
|
|
151
162
|
let currentSize = getCurrentLogSize(logFile);
|
|
@@ -155,6 +166,9 @@ function flush(): void {
|
|
|
155
166
|
currentSize = 0;
|
|
156
167
|
}
|
|
157
168
|
fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
|
|
169
|
+
buffer = [];
|
|
170
|
+
bufferedBytes = 0;
|
|
171
|
+
droppedLines = 0;
|
|
158
172
|
activeLogFile = logFile;
|
|
159
173
|
activeLogSize = currentSize + dataSize;
|
|
160
174
|
flushesSinceSizeCheck++;
|
|
@@ -163,6 +177,7 @@ function flush(): void {
|
|
|
163
177
|
activeLogSize = null;
|
|
164
178
|
flushesSinceSizeCheck = 0;
|
|
165
179
|
recordSwallowedWrite(error);
|
|
180
|
+
boundBuffer();
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
|
|
@@ -186,7 +201,10 @@ export function log(message: string, data?: unknown): void {
|
|
|
186
201
|
`${data.message}${data.stack ? `\n${data.stack}` : ""}`,
|
|
187
202
|
)}`
|
|
188
203
|
: ` ${JSON.stringify(sanitizeConfigValue(data))}`;
|
|
189
|
-
|
|
204
|
+
const line = `[${timestamp}] ${sanitizeDiagnosticText(message)}${serialized}\n`;
|
|
205
|
+
buffer.push(line);
|
|
206
|
+
bufferedBytes += Buffer.byteLength(line);
|
|
207
|
+
boundBuffer();
|
|
190
208
|
if (buffer.length >= BUFFER_SIZE_LIMIT) {
|
|
191
209
|
flush();
|
|
192
210
|
} else {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { MagicContextConfigSchema } from "../config/schema/magic-context";
|
|
3
|
+
import { resolveCacheTtl } from "../hooks/magic-context/event-resolvers";
|
|
4
|
+
|
|
5
|
+
describe("known model cache TTL", () => {
|
|
6
|
+
it("matches canonical model versions through provider prefixes", () => {
|
|
7
|
+
for (const prefix of ["", "openai/", "openai-codex/", "openrouter/openai/", "azure/"]) {
|
|
8
|
+
for (const model of [
|
|
9
|
+
"gpt-5.6",
|
|
10
|
+
"gpt-5.6-sol",
|
|
11
|
+
"gpt-5.7",
|
|
12
|
+
"gpt-5.10",
|
|
13
|
+
"gpt-6",
|
|
14
|
+
"gpt-6.1",
|
|
15
|
+
"gpt-6-mini",
|
|
16
|
+
"gpt-7",
|
|
17
|
+
]) {
|
|
18
|
+
expect(resolveCacheTtl("5m", prefix + model), prefix + model).toBe("30m");
|
|
19
|
+
}
|
|
20
|
+
for (const model of ["gpt-5.5", "gpt-5.1", "gpt-5", "gpt-4o", "unknown", "not-gpt-6"]) {
|
|
21
|
+
expect(resolveCacheTtl("5m", prefix + model), prefix + model).toBe("5m");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it("uses built-ins for unset and global 5m, but honors global 10m", () => {
|
|
26
|
+
expect(resolveCacheTtl(MagicContextConfigSchema.parse({}).cache_ttl, "openai/gpt-6")).toBe(
|
|
27
|
+
"30m",
|
|
28
|
+
);
|
|
29
|
+
expect(resolveCacheTtl("5m", "openai/gpt-6")).toBe("30m");
|
|
30
|
+
expect(resolveCacheTtl("10m", "openai/gpt-6")).toBe("10m");
|
|
31
|
+
});
|
|
32
|
+
it("honors per-model entries before built-ins and built-ins before object defaults", () => {
|
|
33
|
+
expect(resolveCacheTtl({ default: "10m" }, "openai/gpt-6")).toBe("30m");
|
|
34
|
+
expect(resolveCacheTtl({ default: "10m", "gpt-6": "5m" }, "openai/gpt-6")).toBe("5m");
|
|
35
|
+
expect(resolveCacheTtl({ default: "10m", "openai/*": "never" }, "openai/gpt-6")).toBe(
|
|
36
|
+
"never",
|
|
37
|
+
);
|
|
38
|
+
expect(resolveCacheTtl({ default: "10m" }, "other/model")).toBe("10m");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { canonicalModelIdentity } from "./harness-provider-map";
|
|
2
|
+
import { resolveModelConfigValue } from "./prompt-surface";
|
|
3
|
+
|
|
4
|
+
export type CacheTtlConfig = string | Record<string, string>;
|
|
5
|
+
export type CacheTtlSource = "config" | "default" | "OpenAI GPT-5.6+ default";
|
|
6
|
+
export interface ResolvedCacheTtl {
|
|
7
|
+
value: string;
|
|
8
|
+
source: CacheTtlSource;
|
|
9
|
+
modelKey: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Provider documentation: https://developers.openai.com/api/docs/guides/prompt-caching
|
|
13
|
+
// Cache lifetime / Summary of model differences: at least 30 minutes since write or reuse.
|
|
14
|
+
const MODEL_CACHE_LIFETIMES = [
|
|
15
|
+
{
|
|
16
|
+
source: "OpenAI GPT-5.6+ default" as const,
|
|
17
|
+
value: "30m",
|
|
18
|
+
matches(model: string): boolean {
|
|
19
|
+
const version = /^gpt-(\d+)(?:\.(\d+))?(?:$|[^\d.])/.exec(model);
|
|
20
|
+
if (!version) return false;
|
|
21
|
+
const major = Number(version[1]);
|
|
22
|
+
const minor = Number(version[2] ?? 0);
|
|
23
|
+
return major > 5 || (major === 5 && minor >= 6);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function resolveModelCacheTtl(
|
|
29
|
+
config: CacheTtlConfig | undefined,
|
|
30
|
+
modelKey: string | undefined,
|
|
31
|
+
): ResolvedCacheTtl {
|
|
32
|
+
if (config && typeof config !== "string") {
|
|
33
|
+
const match =
|
|
34
|
+
modelKey &&
|
|
35
|
+
!modelKey.includes("/") &&
|
|
36
|
+
Object.hasOwn(config, modelKey) &&
|
|
37
|
+
modelKey !== "default"
|
|
38
|
+
? config[modelKey]
|
|
39
|
+
: resolveModelConfigValue(config, modelKey)?.value;
|
|
40
|
+
if (match !== undefined) return { value: match, source: "config", modelKey };
|
|
41
|
+
}
|
|
42
|
+
// A non-5m global string is an explicit policy. The generic 5m default is
|
|
43
|
+
// not evidence of provider eviction: documented lifetimes avoid paid rewrites.
|
|
44
|
+
if (typeof config === "string" && config !== "5m")
|
|
45
|
+
return { value: config, source: "config", modelKey };
|
|
46
|
+
const model =
|
|
47
|
+
canonicalModelIdentity(modelKey ?? "")
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.split("/")
|
|
50
|
+
.at(-1) ?? "";
|
|
51
|
+
const known = MODEL_CACHE_LIFETIMES.find((entry) => entry.matches(model));
|
|
52
|
+
if (known) return { value: known.value, source: known.source, modelKey };
|
|
53
|
+
return {
|
|
54
|
+
value: (typeof config === "object" ? config.default : config) ?? "5m",
|
|
55
|
+
source: "default",
|
|
56
|
+
modelKey,
|
|
57
|
+
};
|
|
58
|
+
}
|