@retaw106/mmc-oc 2.3.0 → 2.5.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 +12 -1
- package/dist/config/schema/magic-context.d.ts +33 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +12 -1
- package/dist/{context-wt7g4f8k.js → context-xx09sh1g.js} +1985 -603
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +6 -2
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +5 -8
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +5 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-host-instruction-retention.d.ts +4 -0
- package/dist/features/magic-context/storage-host-instruction-retention.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +4 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +57 -2
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts +3 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
- package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
- 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/compartment-runner.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
- package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/file-token-coverage.d.ts +11 -0
- package/dist/hooks/magic-context/file-token-coverage.d.ts.map +1 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/host-served-rows.d.ts +10 -0
- package/dist/hooks/magic-context/host-served-rows.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +2 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts +2 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +2 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +10 -4
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/render-options.d.ts +19 -0
- package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +3 -2
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +4 -0
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
- package/dist/hooks/magic-context/tag-content-primitives.d.ts +1 -1
- package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +15 -2
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/text-complete.d.ts +1 -1
- package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
- package/dist/hooks/magic-context/todo-view.d.ts +2 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +1 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +8 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
- package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +4 -9
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
- package/dist/{index-kw21z5pe.js → index-0d40b4q8.js} +17119 -16569
- package/dist/{index-z2w1eq3y.js → index-1vxffj5j.js} +558 -211
- package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
- package/dist/{index-18yrysyj.js → index-bpjjcdy9.js} +1 -1
- package/dist/{index-a5mw0xp3.js → index-k25thxq7.js} +1 -1
- package/dist/{index-3gj1ak4c.js → index-mjf0p7yq.js} +1 -1
- package/dist/{index-czba8tw3.js → index-pte3cfs9.js} +226 -14
- package/dist/index.js +2 -2
- package/dist/{message-time-backfill-sykevdax.js → message-time-backfill-g9py24yt.js} +1 -1
- package/dist/model-resolution-cvyrrcjw.js +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/subagent-tool-policy.d.ts +4 -0
- package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
- package/dist/{read-session-chunk-863d6c06.js → read-session-chunk-eb3fy45y.js} +3 -3
- package/dist/{recomp-orchestrator-wrjy3qym.js → recomp-orchestrator-mmpy5cb7.js} +6 -6
- package/dist/{send-session-notification-cz6ev088.js → send-session-notification-zpqs213d.js} +3 -3
- package/dist/shared/model-suggestion-retry.d.ts +23 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +5 -0
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/reclaim-distance.d.ts +33 -0
- package/dist/shared/reclaim-distance.d.ts.map +1 -0
- package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
- package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +2 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/search-text.d.ts +8 -0
- package/dist/shared/search-text.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +8 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/stable-json.d.ts +7 -0
- package/dist/shared/stable-json.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.d.ts.map +1 -1
- package/dist/shared/system-directive.d.ts +2 -0
- package/dist/shared/system-directive.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-json.d.ts +22 -0
- package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
- package/dist/shared/user-facing-codes.d.ts +2 -2
- package/dist/v2/fold/native-replay.d.ts +11 -2
- package/dist/v2/fold/native-replay.d.ts.map +1 -1
- package/dist/v2/fold/policy.d.ts.map +1 -1
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +20 -1
- package/dist/v2/hidden-completion.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/generate.d.ts +36 -1
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +22 -1
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +35 -4
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts +10 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts +2 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +12 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/server.js +16272 -13742
- package/dist/v2/store-reader.d.ts +38 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +262 -17
- package/dist/{wrapup-orchestrator-bmybc5p5.js → wrapup-orchestrator-jq589pgr.js} +6 -6
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +8 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/schema/magic-context.test.ts +50 -0
- package/src/config/schema/magic-context.ts +34 -1
- package/src/shared/model-suggestion-retry.test.ts +52 -0
- package/src/shared/model-suggestion-retry.ts +13 -4
- package/src/shared/prompt-surface-runtime.ts +12 -0
- package/src/shared/reclaim-distance.test.ts +111 -0
- package/src/shared/reclaim-distance.ts +91 -0
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/rpc-discovery-cleanup.test.ts +125 -0
- package/src/shared/rpc-discovery-cleanup.ts +57 -0
- package/src/shared/rpc-server.ts +36 -13
- package/src/shared/search-text.test.ts +30 -0
- package/src/shared/search-text.ts +62 -0
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +237 -1
- package/src/shared/sqlite-turn-deadline.test.ts +286 -0
- package/src/shared/sqlite.ts +292 -23
- package/src/shared/stable-json.ts +56 -14
- package/src/shared/storage-file-mutex.test.ts +51 -2
- package/src/shared/storage-file-mutex.ts +11 -1
- package/src/shared/system-directive.test.ts +37 -0
- package/src/shared/system-directive.ts +19 -1
- package/src/shared/tag-transcript.ts +16 -4
- package/src/shared/tail-hygiene-json.test.ts +117 -0
- package/src/shared/tail-hygiene-json.ts +217 -0
- package/src/shared/user-facing-codes.ts +2 -2
- package/dist/model-resolution-wazv26n0.js +0 -6
|
@@ -135,6 +135,7 @@ describe("MagicContextConfigSchema", () => {
|
|
|
135
135
|
},
|
|
136
136
|
maintenance: {
|
|
137
137
|
enabled: true,
|
|
138
|
+
fallback_models: [],
|
|
138
139
|
budgets: {
|
|
139
140
|
inputTokens: 400_000,
|
|
140
141
|
outputTokens: 32_000,
|
|
@@ -159,6 +160,55 @@ describe("MagicContextConfigSchema", () => {
|
|
|
159
160
|
expect(result).toEqual(input);
|
|
160
161
|
});
|
|
161
162
|
|
|
163
|
+
it("parses both maintenance model spellings and distinguishes absent from empty fallbacks", () => {
|
|
164
|
+
const stringForm = MagicContextConfigSchema.parse({
|
|
165
|
+
maintenance: { enabled: true, model: "openai/gpt-6-luna" },
|
|
166
|
+
}).maintenance;
|
|
167
|
+
expect(stringForm.model).toBe("openai/gpt-6-luna");
|
|
168
|
+
expect(stringForm.fallback_models).toBeUndefined();
|
|
169
|
+
expect(
|
|
170
|
+
MagicContextConfigSchema.parse({
|
|
171
|
+
maintenance: { fallback_models: [] },
|
|
172
|
+
}).maintenance.fallback_models,
|
|
173
|
+
).toEqual([]);
|
|
174
|
+
|
|
175
|
+
const objectForm = MagicContextConfigSchema.parse({
|
|
176
|
+
maintenance: {
|
|
177
|
+
enabled: true,
|
|
178
|
+
model: { model: "xiaomi/mimo-v2.6-flash", variant: "high" },
|
|
179
|
+
fallback_models: ["openai/gpt-6-luna", { model: "p/l", thinking_level: "off" }],
|
|
180
|
+
},
|
|
181
|
+
}).maintenance;
|
|
182
|
+
// Object entries keep both harness qualifiers verbatim — the
|
|
183
|
+
// active harness reads its own, no cross-harness mapping.
|
|
184
|
+
expect(objectForm.model).toEqual({
|
|
185
|
+
model: "xiaomi/mimo-v2.6-flash",
|
|
186
|
+
variant: "high",
|
|
187
|
+
});
|
|
188
|
+
expect(objectForm.fallback_models).toEqual([
|
|
189
|
+
"openai/gpt-6-luna",
|
|
190
|
+
{ model: "p/l", thinking_level: "off" },
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("plan 18: rejects blank maintenance model references at parse time (no silent executor flow)", () => {
|
|
195
|
+
expect(
|
|
196
|
+
MagicContextConfigSchema.safeParse({
|
|
197
|
+
maintenance: { enabled: true, model: " " },
|
|
198
|
+
}).success,
|
|
199
|
+
).toBe(false);
|
|
200
|
+
expect(
|
|
201
|
+
MagicContextConfigSchema.safeParse({
|
|
202
|
+
maintenance: { enabled: true, model: { model: "" } },
|
|
203
|
+
}).success,
|
|
204
|
+
).toBe(false);
|
|
205
|
+
expect(
|
|
206
|
+
MagicContextConfigSchema.safeParse({
|
|
207
|
+
maintenance: { enabled: true, fallback_models: ["ok/one", " "] },
|
|
208
|
+
}).success,
|
|
209
|
+
).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
162
212
|
it("accepts a boolean storage permission policy and rejects non-booleans", () => {
|
|
163
213
|
expect(
|
|
164
214
|
MagicContextConfigSchema.parse({
|
|
@@ -135,6 +135,23 @@ const HistorianModelEntryObjectSchema = z
|
|
|
135
135
|
export const HistorianModelEntrySchema = z.union([z.string(), HistorianModelEntryObjectSchema]);
|
|
136
136
|
export type HistorianModelEntry = z.infer<typeof HistorianModelEntrySchema>;
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A maintenance model entry (plan 18): the same string/object shape as a
|
|
140
|
+
* historian model entry, but blank references are REJECTED at parse time —
|
|
141
|
+
* a blank primary must surface as a config diagnostic, never silently flow
|
|
142
|
+
* into the hidden executor or get corrected to some other model.
|
|
143
|
+
*/
|
|
144
|
+
const MaintenanceModelEntrySchema = HistorianModelEntrySchema.superRefine((entry, ctx) => {
|
|
145
|
+
const model = typeof entry === "string" ? entry : entry.model;
|
|
146
|
+
if (typeof model !== "string" || model.trim().length === 0) {
|
|
147
|
+
ctx.addIssue({
|
|
148
|
+
code: "custom",
|
|
149
|
+
message:
|
|
150
|
+
"maintenance model entries must be non-empty: a bare provider/model string or { model, variant?, thinking_level? }",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
138
155
|
/**
|
|
139
156
|
* A profile may select models but must not alter execution policy. It uses the
|
|
140
157
|
* same flat historian model-resolution fields; execution metadata is not
|
|
@@ -400,6 +417,13 @@ export interface MagicContextConfig {
|
|
|
400
417
|
*/
|
|
401
418
|
maintenance: {
|
|
402
419
|
enabled: boolean;
|
|
420
|
+
/**
|
|
421
|
+
* Primary maintenance model entry. When absent, inherit historian
|
|
422
|
+
* model configuration; never inherit the main-session model.
|
|
423
|
+
*/
|
|
424
|
+
model?: HistorianModelEntry;
|
|
425
|
+
/** Ordered fallbacks; absent inherits historian fallbacks only with an inherited primary. */
|
|
426
|
+
fallback_models?: HistorianModelEntry[];
|
|
403
427
|
/** Per-run four-dimensional budget (decision 9; provisional, calibrated in P3/P4). */
|
|
404
428
|
budgets: {
|
|
405
429
|
inputTokens: number;
|
|
@@ -800,6 +824,15 @@ export const MagicContextConfigSchema = z.object({
|
|
|
800
824
|
.describe(
|
|
801
825
|
"Enable the automatic memory-maintenance task (default: true). USER-LEVEL ONLY — the whole maintenance group is stripped from project-tier config.",
|
|
802
826
|
),
|
|
827
|
+
model: MaintenanceModelEntrySchema.optional().describe(
|
|
828
|
+
"Primary maintenance model entry: a bare provider/model string or { model, variant? (OpenCode), thinking_level? (Pi) }. USER-LEVEL ONLY. When absent, inherit historian model configuration. If neither has a primary, automatic passes skip and manual runs refuse. The main-session model is never inherited.",
|
|
829
|
+
),
|
|
830
|
+
fallback_models: z
|
|
831
|
+
.array(MaintenanceModelEntrySchema)
|
|
832
|
+
.optional()
|
|
833
|
+
.describe(
|
|
834
|
+
"Ordered fallback maintenance model entries, tried after CALL failures only. When absent and the primary is inherited, inherit historian fallbacks; explicit [] disables fallbacks. Illegal manifests, permission refusals, and exhausted budgets never switch models.",
|
|
835
|
+
),
|
|
803
836
|
budgets: z
|
|
804
837
|
.object({
|
|
805
838
|
inputTokens: z
|
|
@@ -922,7 +955,7 @@ export const MagicContextConfigSchema = z.object({
|
|
|
922
955
|
anomalyThreshold: { absoluteRows: 10, poolFraction: 0.25 },
|
|
923
956
|
})
|
|
924
957
|
.describe(
|
|
925
|
-
"Automatic memory-maintenance run controls (dreamer replacement). USER-LEVEL ONLY:
|
|
958
|
+
"Automatic memory-maintenance run controls (dreamer replacement). USER-LEVEL ONLY: project-tier config cannot pick maintenance models or change run controls. An absent model inherits historian model configuration, never the session model.",
|
|
926
959
|
),
|
|
927
960
|
});
|
|
928
961
|
|
|
@@ -38,6 +38,58 @@ function createArgs(model?: { providerID: string; modelID: string }) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
describe("promptSyncWithModelSuggestionRetry", () => {
|
|
41
|
+
for (const validated of [false, true]) {
|
|
42
|
+
for (const allowModelSuggestions of [undefined, true, false]) {
|
|
43
|
+
test(`provider suggestions ${String(allowModelSuggestions)}, validated=${validated}`, async () => {
|
|
44
|
+
const models: string[] = [];
|
|
45
|
+
const prompt = mock(async (args: PromptCall) => {
|
|
46
|
+
const model = args.body.model?.modelID ?? "";
|
|
47
|
+
models.push(model);
|
|
48
|
+
if (model !== "suggested" && model !== "last") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`model not found: provider/${model}. did you mean: suggested?`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
});
|
|
55
|
+
const options = {
|
|
56
|
+
allowModelSuggestions,
|
|
57
|
+
fallbackModels: ["provider/fallback", "provider/last"],
|
|
58
|
+
};
|
|
59
|
+
const args = createArgs({ providerID: "provider", modelID: "primary" });
|
|
60
|
+
if (validated) {
|
|
61
|
+
await promptSyncWithValidatedOutputRetry(createClient(prompt), args, {
|
|
62
|
+
...options,
|
|
63
|
+
fetchOutput: async () => "output",
|
|
64
|
+
validateOutput: (output) => output,
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
await promptSyncWithModelSuggestionRetry(createClient(prompt), args, options);
|
|
68
|
+
}
|
|
69
|
+
expect(models).toEqual(
|
|
70
|
+
allowModelSuggestions === false
|
|
71
|
+
? ["primary", "fallback", "last"]
|
|
72
|
+
: ["primary", "suggested"],
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("disabled suggestions preserve the original error without fallbacks", async () => {
|
|
79
|
+
const error = new Error("model not found: provider/primary. did you mean: suggested?");
|
|
80
|
+
const prompt = mock(async () => {
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
await expect(
|
|
84
|
+
promptSyncWithModelSuggestionRetry(
|
|
85
|
+
createClient(prompt),
|
|
86
|
+
createArgs({ providerID: "provider", modelID: "primary" }),
|
|
87
|
+
{ allowModelSuggestions: false },
|
|
88
|
+
),
|
|
89
|
+
).rejects.toBe(error);
|
|
90
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
41
93
|
test("primary succeeds, no fallback iteration", async () => {
|
|
42
94
|
const prompt = mock(async () => ({}));
|
|
43
95
|
const client = createClient(prompt);
|
|
@@ -71,6 +71,8 @@ export interface PromptAttemptInfo {
|
|
|
71
71
|
export interface PromptRetryOptions {
|
|
72
72
|
transport?: PromptTransport;
|
|
73
73
|
timeoutMs?: number;
|
|
74
|
+
/** False for callers whose configured model chain must not accept provider suggestions. Defaults to true. */
|
|
75
|
+
allowModelSuggestions?: boolean;
|
|
74
76
|
/** External abort signal — cancels the in-flight LLM prompt immediately when aborted */
|
|
75
77
|
signal?: AbortSignal;
|
|
76
78
|
/**
|
|
@@ -361,8 +363,12 @@ async function abortChildRun(client: Client, sessionId: string): Promise<void> {
|
|
|
361
363
|
* Everything else (auth errors, ProviderModelNotFoundError without suggestion,
|
|
362
364
|
* rate limits, transient network failures, etc.) is considered retryable on a
|
|
363
365
|
* different model.
|
|
366
|
+
* Exported as the shared fallback-switch classifier (plan 18): the
|
|
367
|
+
* memory-maintenance attempt loop reuses this exact predicate so "which
|
|
368
|
+
* failures may switch models" has ONE definition across historian and
|
|
369
|
+
* maintainer call sites.
|
|
364
370
|
*/
|
|
365
|
-
function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
|
|
371
|
+
export function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
|
|
366
372
|
if (
|
|
367
373
|
externalSignal?.aborted ||
|
|
368
374
|
(error instanceof HiddenCompletionRefusal && error.terminal) ||
|
|
@@ -484,6 +490,7 @@ async function attemptOnce(
|
|
|
484
490
|
callContext: string,
|
|
485
491
|
label: string,
|
|
486
492
|
transport?: PromptTransport,
|
|
493
|
+
allowModelSuggestions = true,
|
|
487
494
|
): Promise<void> {
|
|
488
495
|
// Keep this snapshot separate from the object passed to the client. A
|
|
489
496
|
// failed prompt facade may rewrite its request body before rejecting; the
|
|
@@ -494,9 +501,8 @@ async function attemptOnce(
|
|
|
494
501
|
await promptWithTimeout(client, attemptArgs, timeoutMs, signal, transport);
|
|
495
502
|
return;
|
|
496
503
|
} catch (error) {
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
if (isNonRetryable(error, signal)) throw error;
|
|
504
|
+
// Surface the original error when suggestions are disabled or cannot help.
|
|
505
|
+
if (isNonRetryable(error, signal) || !allowModelSuggestions) throw error;
|
|
500
506
|
|
|
501
507
|
const suggestion = parseModelSuggestion(error);
|
|
502
508
|
if (!suggestion || !originalBody.model) {
|
|
@@ -572,6 +578,7 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
572
578
|
callContext,
|
|
573
579
|
explicitPrimaryLabel,
|
|
574
580
|
options.transport,
|
|
581
|
+
options.allowModelSuggestions,
|
|
575
582
|
);
|
|
576
583
|
return;
|
|
577
584
|
} catch (error) {
|
|
@@ -616,6 +623,7 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
616
623
|
callContext,
|
|
617
624
|
label,
|
|
618
625
|
options.transport,
|
|
626
|
+
options.allowModelSuggestions,
|
|
619
627
|
);
|
|
620
628
|
log(
|
|
621
629
|
`[${callContext}] fallback succeeded with ${label} (attempt ${i + 2}/${fallbacks.length + 1})`,
|
|
@@ -661,6 +669,7 @@ async function attemptAndValidate<TOutput, TValidated>(
|
|
|
661
669
|
callContext,
|
|
662
670
|
attempt.label,
|
|
663
671
|
options.transport,
|
|
672
|
+
options.allowModelSuggestions,
|
|
664
673
|
);
|
|
665
674
|
} catch (error) {
|
|
666
675
|
throw {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
4
|
|
|
@@ -59,6 +60,17 @@ export function promptSurfaceHashMaterial(
|
|
|
59
60
|
: `${systemContent}\n\0magic-context-prompt-surface:${preset}`;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Hash system text after the caller resolves guidance and freezes host date lines.
|
|
65
|
+
* Keep the legacy MD5 hex encoding so existing cached heads retain their identity.
|
|
66
|
+
*/
|
|
67
|
+
export function systemPromptFingerprint(
|
|
68
|
+
systemContent: string,
|
|
69
|
+
preset: PromptSurfacePreset = "full",
|
|
70
|
+
): string {
|
|
71
|
+
return createHash("md5").update(promptSurfaceHashMaterial(systemContent, preset)).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
const PROMPT_SURFACE_TOOL_ID_SET = new Set<string>(ACTIVE_TOOL_IDS);
|
|
63
75
|
|
|
64
76
|
export interface PromptSurfaceGuidanceSelection {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
farBehindToolBookmark,
|
|
4
|
+
isReclaimUserTurn,
|
|
5
|
+
type ReclaimDistanceEntry,
|
|
6
|
+
} from "./reclaim-distance";
|
|
7
|
+
|
|
8
|
+
const output = (tag: number, tokens = 1000): ReclaimDistanceEntry => ({
|
|
9
|
+
toolOutputTag: tag,
|
|
10
|
+
tokens,
|
|
11
|
+
});
|
|
12
|
+
const turn = (): ReclaimDistanceEntry => ({ userTurn: true, tokens: 10 });
|
|
13
|
+
|
|
14
|
+
describe("reclaim distance", () => {
|
|
15
|
+
it("repeated rebuilds cannot age the current task", () => {
|
|
16
|
+
const entries = [turn(), ...Array.from({ length: 28 }, (_, i) => output(i + 1))];
|
|
17
|
+
for (const count of [2, 3, 5]) {
|
|
18
|
+
let bookmark = 0;
|
|
19
|
+
for (let pass = 0; pass < count + 1; pass++) {
|
|
20
|
+
expect(bookmark).toBe(0);
|
|
21
|
+
bookmark = Math.max(bookmark, farBehindToolBookmark(entries, 200_000));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("three newer user turns establish a bookmark, not two", () => {
|
|
27
|
+
const entries = [turn(), output(1), turn(), output(2), turn()];
|
|
28
|
+
expect(farBehindToolBookmark(entries, 200_000)).toBe(0);
|
|
29
|
+
expect(farBehindToolBookmark([...entries, turn()], 200_000)).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("single-turn tool loop ages at exactly a quarter of the usable window", () => {
|
|
33
|
+
const entries = [turn(), output(1), output(2, 49_999)];
|
|
34
|
+
expect(farBehindToolBookmark(entries, 200_000)).toBe(0);
|
|
35
|
+
expect(farBehindToolBookmark([...entries, { tokens: 1 }], 200_000)).toBe(1);
|
|
36
|
+
expect(farBehindToolBookmark([turn(), output(1, 100_000)], 200_000)).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("multipart outputs never age from their own trailing chunks", () => {
|
|
40
|
+
expect(farBehindToolBookmark([output(1), output(1, 60_000)], 200_000)).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("unknown windows disable only token distance", () => {
|
|
44
|
+
expect(farBehindToolBookmark([output(1), output(2, 99_999)], 0)).toBe(0);
|
|
45
|
+
expect(farBehindToolBookmark([output(1), turn(), turn(), turn()], 0)).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("excludes synthetic host wakes, MC reminders, heads and result carriers", () => {
|
|
49
|
+
expect(
|
|
50
|
+
isReclaimUserTurn({
|
|
51
|
+
role: "user",
|
|
52
|
+
parts: [
|
|
53
|
+
{
|
|
54
|
+
kind: "text",
|
|
55
|
+
synthetic: true,
|
|
56
|
+
text: "<system-reminder>Background task finished.</system-reminder>",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
).toBe(false);
|
|
61
|
+
expect(
|
|
62
|
+
isReclaimUserTurn({
|
|
63
|
+
role: "user",
|
|
64
|
+
parts: [
|
|
65
|
+
{
|
|
66
|
+
kind: "text",
|
|
67
|
+
text: "<system-reminder>Your next step: call ctx_reduce on the outputs you've already used (5).</system-reminder>",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
).toBe(false);
|
|
72
|
+
expect(
|
|
73
|
+
isReclaimUserTurn({
|
|
74
|
+
role: "user",
|
|
75
|
+
syntheticHead: true,
|
|
76
|
+
parts: [{ kind: "text", text: "history" }],
|
|
77
|
+
}),
|
|
78
|
+
).toBe(false);
|
|
79
|
+
expect(
|
|
80
|
+
isReclaimUserTurn({ role: "user", parts: [{ kind: "tool_result", text: "output" }] }),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
expect(
|
|
83
|
+
isReclaimUserTurn({
|
|
84
|
+
role: "user",
|
|
85
|
+
parts: [
|
|
86
|
+
{ kind: "tool_result", text: "output" },
|
|
87
|
+
{ kind: "text", text: "continue" },
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("counts an authored part even after synthetic or ignored parts", () => {
|
|
94
|
+
for (const provenance of [{ synthetic: true }, { ignored: true }]) {
|
|
95
|
+
expect(
|
|
96
|
+
isReclaimUserTurn({ role: "user", parts: [{ kind: "image", ...provenance }] }),
|
|
97
|
+
).toBe(false);
|
|
98
|
+
expect(
|
|
99
|
+
isReclaimUserTurn({
|
|
100
|
+
role: "user",
|
|
101
|
+
parts: [{ kind: "text", text: "notice", ...provenance }, { kind: "image" }],
|
|
102
|
+
}),
|
|
103
|
+
).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("a young lower-tag result holds back distant higher tags", () => {
|
|
108
|
+
expect(farBehindToolBookmark([output(2), output(3, 50_000), output(1)], 200_000)).toBe(0);
|
|
109
|
+
expect(farBehindToolBookmark([output(1), output(3, 50_000), output(2)], 200_000)).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const RECLAIM_DISTANCE_USER_TURNS = 3;
|
|
2
|
+
export const RECLAIM_DISTANCE_WINDOW_FRACTION = 0.25;
|
|
3
|
+
|
|
4
|
+
export interface ReclaimDistanceEntry {
|
|
5
|
+
/** Local content tokens, excluding Magic Context's history and reduction/note reminders. */
|
|
6
|
+
tokens: number;
|
|
7
|
+
/** One event per authored user-role message, after any folded tool results. */
|
|
8
|
+
userTurn?: boolean;
|
|
9
|
+
/** Durable identifier of the tool invocation whose result ends at this entry. */
|
|
10
|
+
toolOutputTag?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Only authored content starts a user turn; host control notices do not. */
|
|
14
|
+
export function isReclaimUserTurn(args: {
|
|
15
|
+
role?: string;
|
|
16
|
+
syntheticHead?: boolean;
|
|
17
|
+
synthetic?: boolean;
|
|
18
|
+
ignored?: boolean;
|
|
19
|
+
parts: readonly { kind: string; text?: string; synthetic?: boolean; ignored?: boolean }[];
|
|
20
|
+
}): boolean {
|
|
21
|
+
return (
|
|
22
|
+
args.role === "user" &&
|
|
23
|
+
!args.syntheticHead &&
|
|
24
|
+
!args.synthetic &&
|
|
25
|
+
!args.ignored &&
|
|
26
|
+
args.parts.some((part) => {
|
|
27
|
+
if (part.synthetic || part.ignored) return false;
|
|
28
|
+
if (part.kind !== "text" && part.kind !== "image" && part.kind !== "file") return false;
|
|
29
|
+
if (part.kind !== "text") return true;
|
|
30
|
+
return stripReclaimMcText(part.text ?? "").trim().length > 0;
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Remove Magic Context's reduction/note reminders, not generic host wake notices. */
|
|
36
|
+
export function stripReclaimMcText(text: string): string {
|
|
37
|
+
return text
|
|
38
|
+
.replace(/^§\d+§\s*/, "")
|
|
39
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, (reminder) =>
|
|
40
|
+
/^(?:Your next step: call ctx_reduce on the outputs you've already used|Still unstamped:|Housekeeping: .* are reclaimable\. Stamp each output with ctx_reduce|.* are reclaimable\. Make a ctx_reduce pass now|.* are still unstamped\. Call ctx_reduce now)/.test(
|
|
41
|
+
reminder.slice("<system-reminder>".length).trimStart(),
|
|
42
|
+
)
|
|
43
|
+
? ""
|
|
44
|
+
: reminder,
|
|
45
|
+
)
|
|
46
|
+
.replace(/<instruction name="deferred_notes">[\s\S]*?<\/instruction>/g, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A free cache rebuild is not evidence that the model has moved on. An output
|
|
51
|
+
* is far behind only after three newer user turns or a quarter-window of newer
|
|
52
|
+
* content. Return a prefix of qualifying tags; callers drop against the PREVIOUS
|
|
53
|
+
* durable bookmark before advancing it, so distance must survive two rebuilds.
|
|
54
|
+
*/
|
|
55
|
+
export function farBehindToolBookmark(
|
|
56
|
+
entries: readonly ReclaimDistanceEntry[],
|
|
57
|
+
usableWindow: number,
|
|
58
|
+
): number {
|
|
59
|
+
let newerTurns = 0;
|
|
60
|
+
let newerTokens = 0;
|
|
61
|
+
const farTags: number[] = [];
|
|
62
|
+
let firstYoungTag = Infinity;
|
|
63
|
+
const seenOutputs = new Set<number>();
|
|
64
|
+
const tokenDistance =
|
|
65
|
+
usableWindow > 0 && Number.isFinite(usableWindow)
|
|
66
|
+
? usableWindow * RECLAIM_DISTANCE_WINDOW_FRACTION
|
|
67
|
+
: Infinity;
|
|
68
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
69
|
+
const entry = entries[index];
|
|
70
|
+
if (!entry) continue;
|
|
71
|
+
const tag = entry.toolOutputTag;
|
|
72
|
+
// Multi-part outputs are positioned at their last result, not their call
|
|
73
|
+
// or first chunk: an output must never count its own remaining payload.
|
|
74
|
+
if (tag !== undefined && !seenOutputs.has(tag)) {
|
|
75
|
+
seenOutputs.add(tag);
|
|
76
|
+
if (newerTurns >= RECLAIM_DISTANCE_USER_TURNS || newerTokens >= tokenDistance) {
|
|
77
|
+
farTags.push(tag);
|
|
78
|
+
} else {
|
|
79
|
+
firstYoungTag = Math.min(firstYoungTag, tag);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (entry.userTurn) newerTurns++;
|
|
83
|
+
if (Number.isFinite(entry.tokens) && entry.tokens > 0) newerTokens += entry.tokens;
|
|
84
|
+
}
|
|
85
|
+
// Tags follow call order, not result arrival. A late lower-tag sibling must
|
|
86
|
+
// hold the prefix even when a higher-tag result is already distant.
|
|
87
|
+
return farTags.reduce(
|
|
88
|
+
(bookmark, tag) => (tag < firstYoungTag ? Math.max(bookmark, tag) : bookmark),
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { setTimeout as delay, setImmediate as tick } from "node:timers/promises";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { inRootAsyncContext } from "./sqlite";
|
|
6
|
+
|
|
7
|
+
const scope = new AsyncLocalStorage();
|
|
8
|
+
const gc = globalThis.Bun ? () => Bun.gc(true) : globalThis.gc;
|
|
9
|
+
const references = {};
|
|
10
|
+
const resources = [];
|
|
11
|
+
const timerStores = {};
|
|
12
|
+
const ambientTick = () => { timerStores.ambient = scope.getStore() !== undefined; };
|
|
13
|
+
const rootTick = () => { timerStores.root = scope.getStore() !== undefined; };
|
|
14
|
+
function create(name, root, worker) {
|
|
15
|
+
const payload = { synthetic: name };
|
|
16
|
+
references[name] = new WeakRef(payload);
|
|
17
|
+
scope.run(payload, () => {
|
|
18
|
+
const make = worker
|
|
19
|
+
? () => new Worker(new URL("worker.mjs", import.meta.url))
|
|
20
|
+
: () => setInterval(root ? rootTick : ambientTick, 5);
|
|
21
|
+
resources.push(root ? inRootAsyncContext(make) : make());
|
|
22
|
+
assert.equal(scope.getStore(), payload, "Root execution must restore the caller's store");
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
create("ambientWorker", false, true);
|
|
26
|
+
create("rootWorker", true, true);
|
|
27
|
+
create("ambientTimer", false, false);
|
|
28
|
+
create("rootTimer", true, false);
|
|
29
|
+
await delay(30);
|
|
30
|
+
for (let i = 0; i < 12; i++) { await tick(); gc(); await tick(); }
|
|
31
|
+
const retained = Object.fromEntries(Object.entries(references).map(([key, ref]) => [key, Number(ref.deref() !== undefined)]));
|
|
32
|
+
for (const resource of resources) {
|
|
33
|
+
if (resource instanceof Worker) await resource.terminate();
|
|
34
|
+
else clearInterval(resource);
|
|
35
|
+
}
|
|
36
|
+
console.log(JSON.stringify({ runtime: globalThis.Bun ? `Bun ${Bun.version}` : process.version, timerStores, retained }));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
6
|
+
|
|
7
|
+
for (const runtime of ["Node", "Bun"] as const) {
|
|
8
|
+
test(`root resources do not inherit request stores (${runtime})`, async () => {
|
|
9
|
+
const directory = createTestTempDirFromPath(join(tmpdir(), "root-context-"));
|
|
10
|
+
try {
|
|
11
|
+
const entry = join(directory, "probe.mjs");
|
|
12
|
+
const build = await Bun.build({
|
|
13
|
+
entrypoints: [new URL("root-async-context.probe.mjs", import.meta.url).pathname],
|
|
14
|
+
target: "node",
|
|
15
|
+
format: "esm",
|
|
16
|
+
external: ["bun:sqlite", "node:sqlite"],
|
|
17
|
+
});
|
|
18
|
+
expect(build.success).toBe(true);
|
|
19
|
+
await writeFile(entry, await build.outputs[0].text());
|
|
20
|
+
await writeFile(
|
|
21
|
+
join(directory, "worker.mjs"),
|
|
22
|
+
`import { parentPort } from 'node:worker_threads'; parentPort.on('message', () => {});`,
|
|
23
|
+
);
|
|
24
|
+
const child = Bun.spawn(
|
|
25
|
+
[...(runtime === "Node" ? ["node", "--expose-gc"] : [process.execPath]), entry],
|
|
26
|
+
{
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
const [stdout, stderr, exit] = await Promise.all([
|
|
33
|
+
new Response(child.stdout).text(),
|
|
34
|
+
new Response(child.stderr).text(),
|
|
35
|
+
child.exited,
|
|
36
|
+
]);
|
|
37
|
+
expect(exit, stderr).toBe(0);
|
|
38
|
+
const result = JSON.parse(stdout);
|
|
39
|
+
console.log(`${runtime} ALS propagation: ${stdout.trim()}`);
|
|
40
|
+
expect(result.timerStores).toEqual({ ambient: true, root: false });
|
|
41
|
+
expect(result.retained.rootWorker).toBe(0);
|
|
42
|
+
expect(result.retained.rootTimer).toBe(0);
|
|
43
|
+
expect(result.retained.ambientTimer).toBe(1);
|
|
44
|
+
if (runtime === "Node") expect(result.retained.ambientWorker).toBe(1);
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(directory, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|