@cortexkit/opencode-magic-context 0.44.1 → 0.44.2
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 +6 -0
- package/dist/agents/dreamer.d.ts +4 -6
- package/dist/agents/dreamer.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +63 -10
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-d14ah73v.js → conflict-warning-hook-1msyvb51.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +5 -0
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
- package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +8 -22
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
- 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-activity.d.ts +10 -0
- package/dist/features/magic-context/session-activity.d.ts.map +1 -0
- package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.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.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
- package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-d107az47.js → index-18tjha8g.js} +1 -1
- package/dist/{index-0rp6pj54.js → index-3x9caerx.js} +7 -5
- package/dist/index-40s1xz22.js +125 -0
- package/dist/{index-cz88kw0n.js → index-egkfvhms.js} +117 -39
- package/dist/{index-s6b7h12g.js → index-esxfcz5z.js} +73 -2
- package/dist/index-gtzcmv90.js +7260 -0
- package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
- package/dist/{index-a9vbtdzv.js → index-r9vkp4n9.js} +1 -1
- package/dist/{index-jtdrvmcn.js → index-ttdn47aq.js} +37431 -37044
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8195 -14449
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -9
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +1 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-fkdv2g5b.js → read-session-chunk-wmvgnd24.js} +2 -2
- package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-rk40ff15.js} +5 -4
- package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-6xsrcxnp.js} +2 -2
- package/dist/shared/model-suggestion-retry.d.ts +1 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +7 -7
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +21 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +12 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/subagent-runner.d.ts +3 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +5 -0
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
- package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
- package/dist/v2/hidden-completion.d.ts +2 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +1 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +11 -0
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
- package/dist/v2/hooks/storage-gate.d.ts +11 -5
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
- package/dist/v2/host-service.d.ts +2 -1
- package/dist/v2/host-service.d.ts.map +1 -1
- package/dist/v2/server.js +24481 -22623
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-h8tr10c6.js} +6 -5
- package/package.json +1 -1
- package/src/agents/dreamer.ts +4 -6
- package/src/agents/hidden-agent-registrations.ts +3 -3
- package/src/agents/permissions.test.ts +3 -4
- package/src/config/latch-permanence-guard.test.ts +9 -0
- package/src/config/live-child-output-cap.test.ts +26 -0
- package/src/config/live-child-output-cap.ts +6 -2
- package/src/config/schema/magic-context.ts +21 -6
- package/src/features/magic-context/dreamer/task-registry.ts +13 -0
- package/src/shared/logger.test.ts +1 -0
- package/src/shared/model-suggestion-retry.ts +9 -2
- package/src/shared/prompt-async-transport.test.ts +254 -1
- package/src/shared/prompt-async-transport.ts +153 -44
- package/src/shared/rpc-async-probes.test.ts +230 -0
- package/src/shared/rpc-server.ts +6 -4
- package/src/shared/rpc-utils.test.ts +27 -1
- package/src/shared/rpc-utils.ts +161 -9
- package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
- package/src/shared/sqlite-pass-scope.test.ts +182 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
- package/src/shared/sqlite-write-locker-test-support.ts +1 -0
- package/src/shared/sqlite.ts +142 -56
- package/src/shared/storage-busy.test.ts +95 -0
- package/src/shared/subagent-runner.ts +4 -0
- package/src/shared/user-facing-codes.ts +6 -0
- package/src/shared/windows-hidden-spawns.test.ts +132 -0
- package/src/shared/write-transaction-attribution.test.ts +1 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
-
|
|
2
|
+
import {
|
|
3
|
+
createDreamTokenBudget,
|
|
4
|
+
DreamTokenBudgetExceeded,
|
|
5
|
+
registerBudgetFinalizeChild,
|
|
6
|
+
releaseBudgetFinalizeChild,
|
|
7
|
+
TOKEN_BUDGET_FINALIZE_MESSAGE,
|
|
8
|
+
} from "../features/magic-context/dreamer/token-budget";
|
|
9
|
+
import { sumTokensFromChildMessages } from "../features/magic-context/subagent-token-capture";
|
|
3
10
|
import type { PromptArgs, PromptTransport } from "./model-suggestion-retry";
|
|
4
11
|
|
|
5
12
|
type Client = ReturnType<typeof createOpencodeClient> | undefined;
|
|
@@ -21,17 +28,26 @@ const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
|
21
28
|
const DEFAULT_START_GRACE_MS = 30_000;
|
|
22
29
|
/** The wait only looks at the newest messages: the ones this send appended and
|
|
23
30
|
* the ones just before it. The host returns the most recent `limit` messages. */
|
|
24
|
-
const MESSAGE_WINDOW =
|
|
31
|
+
const MESSAGE_WINDOW = 100;
|
|
25
32
|
|
|
26
33
|
export interface PromptAsyncWaitOptions {
|
|
27
34
|
pollIntervalMs?: number;
|
|
28
35
|
startGraceMs?: number;
|
|
36
|
+
tokenBudget?: number;
|
|
37
|
+
/** Shared across retries of one child session. */
|
|
38
|
+
budgetGuard?: ReturnType<typeof createDreamTokenBudget>;
|
|
39
|
+
onBudgetUpdate?: (
|
|
40
|
+
state: ReturnType<ReturnType<typeof createDreamTokenBudget>["snapshot"]> & {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
},
|
|
43
|
+
) => void;
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
type SessionApi = {
|
|
32
47
|
promptAsync?: (options: unknown) => Promise<unknown>;
|
|
33
48
|
status?: (options?: unknown) => Promise<unknown>;
|
|
34
49
|
messages?: (options: unknown) => Promise<unknown>;
|
|
50
|
+
abort?: (options: unknown) => Promise<unknown>;
|
|
35
51
|
};
|
|
36
52
|
|
|
37
53
|
function sessionApi(client: Client): SessionApi | undefined {
|
|
@@ -59,8 +75,12 @@ export function createPromptAsyncTransport(
|
|
|
59
75
|
options: PromptAsyncWaitOptions = {},
|
|
60
76
|
): PromptTransport | undefined {
|
|
61
77
|
if (!supportsPromptAsync(client)) return undefined;
|
|
78
|
+
const budgetGuard = options.tokenBudget
|
|
79
|
+
? createDreamTokenBudget(options.tokenBudget)
|
|
80
|
+
: undefined;
|
|
62
81
|
return Object.assign(
|
|
63
|
-
(request: PromptArgs) =>
|
|
82
|
+
(request: PromptArgs) =>
|
|
83
|
+
promptAsyncAndWaitForIdle(client, request, { ...options, budgetGuard }),
|
|
64
84
|
{ childSessionId },
|
|
65
85
|
);
|
|
66
86
|
}
|
|
@@ -90,6 +110,23 @@ function isSettledAssistant(message: unknown): boolean {
|
|
|
90
110
|
return info.time?.completed != null || info.error != null || info.finish != null;
|
|
91
111
|
}
|
|
92
112
|
|
|
113
|
+
function isTerminalAssistant(message: unknown): boolean {
|
|
114
|
+
const info = infoOf(message);
|
|
115
|
+
return (
|
|
116
|
+
isSettledAssistant(message) &&
|
|
117
|
+
info.finish !== "tool-calls" &&
|
|
118
|
+
info.finish !== "tool_use" &&
|
|
119
|
+
!(
|
|
120
|
+
message &&
|
|
121
|
+
typeof message === "object" &&
|
|
122
|
+
Array.isArray((message as { parts?: unknown[] }).parts) &&
|
|
123
|
+
(message as { parts: Array<{ type?: string }> }).parts.some(
|
|
124
|
+
(part) => part.type === "tool",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
93
130
|
function unwrapData(response: unknown): unknown {
|
|
94
131
|
if (response && typeof response === "object" && "data" in response) {
|
|
95
132
|
return (response as { data?: unknown }).data;
|
|
@@ -169,13 +206,6 @@ async function readStatus(
|
|
|
169
206
|
}
|
|
170
207
|
}
|
|
171
208
|
|
|
172
|
-
/**
|
|
173
|
-
* Send one prompt with `prompt_async`, then poll until the child session is idle
|
|
174
|
-
* and its newest message is a finished assistant reply from this send. Returns
|
|
175
|
-
* without judging that reply: the caller's output validation reads it. Rejects
|
|
176
|
-
* when the caller's signal aborts, and when the host accepted the send but never
|
|
177
|
-
* started a run within the start grace.
|
|
178
|
-
*/
|
|
179
209
|
export async function promptAsyncAndWaitForIdle(
|
|
180
210
|
client: Client,
|
|
181
211
|
request: PromptArgs,
|
|
@@ -191,52 +221,131 @@ export async function promptAsyncAndWaitForIdle(
|
|
|
191
221
|
const signal = request.signal;
|
|
192
222
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
193
223
|
const startGraceMs = options.startGraceMs ?? DEFAULT_START_GRACE_MS;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
224
|
+
const budget =
|
|
225
|
+
options.budgetGuard ??
|
|
226
|
+
(options.tokenBudget ? createDreamTokenBudget(options.tokenBudget) : undefined);
|
|
227
|
+
if (budget?.snapshot().finalizeFired)
|
|
228
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
229
|
+
const seenUsage = new Set<string>();
|
|
230
|
+
let finalizing = false;
|
|
231
|
+
let completionBaseline = new Set(
|
|
198
232
|
(await readMessages(session, sessionId, dir, signal))
|
|
199
233
|
.map(messageId)
|
|
200
234
|
.filter((id): id is string => id !== null),
|
|
201
235
|
);
|
|
202
|
-
|
|
236
|
+
const initialBaseline = new Set(completionBaseline);
|
|
203
237
|
const sent = await session.promptAsync?.(request);
|
|
204
238
|
if (sent && typeof sent === "object" && "error" in sent) {
|
|
205
239
|
const rejection = (sent as { error?: unknown }).error;
|
|
206
240
|
if (rejection)
|
|
207
241
|
throw new Error(`prompt_async was rejected: ${describeRejection(rejection)}`);
|
|
208
242
|
}
|
|
209
|
-
|
|
243
|
+
let sentAt = Date.now();
|
|
210
244
|
let sawBusy = false;
|
|
211
245
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
try {
|
|
247
|
+
for (;;) {
|
|
248
|
+
await sleep(pollIntervalMs, signal);
|
|
249
|
+
const status = await readStatus(session, sessionId, dir, signal);
|
|
250
|
+
const messages = await readMessages(session, sessionId, dir, signal);
|
|
251
|
+
if (budget) {
|
|
252
|
+
for (const message of messages) {
|
|
253
|
+
const id = messageId(message);
|
|
254
|
+
if (!id || seenUsage.has(id) || infoOf(message).role !== "assistant") continue;
|
|
255
|
+
// The initial baseline belongs to a previous child attempt.
|
|
256
|
+
if (initialBaseline.has(id)) continue;
|
|
257
|
+
const tokens = sumTokensFromChildMessages([message]);
|
|
258
|
+
if (tokens.input + tokens.cacheRead + tokens.cacheWrite === 0) continue;
|
|
259
|
+
seenUsage.add(id);
|
|
260
|
+
const decision = budget.charge(
|
|
261
|
+
tokens.input,
|
|
262
|
+
tokens.cacheRead,
|
|
263
|
+
tokens.cacheWrite,
|
|
264
|
+
isTerminalAssistant(message),
|
|
265
|
+
);
|
|
266
|
+
options.onBudgetUpdate?.({ ...budget.snapshot(), sessionId });
|
|
267
|
+
if (decision === "stop") {
|
|
268
|
+
await session.abort?.({ path: { id: sessionId } });
|
|
269
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
270
|
+
}
|
|
271
|
+
if (decision === "finalize") {
|
|
272
|
+
finalizing = true;
|
|
273
|
+
registerBudgetFinalizeChild(sessionId, budget);
|
|
274
|
+
if (!session.abort)
|
|
275
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
276
|
+
await session.abort({ path: { id: sessionId } });
|
|
277
|
+
// Wait for the cancelled generation to become idle before
|
|
278
|
+
// submitting a new user turn in the same session.
|
|
279
|
+
const abortDeadline = Date.now() + startGraceMs;
|
|
280
|
+
while (Date.now() < abortDeadline) {
|
|
281
|
+
const state = await readStatus(session, sessionId, dir, signal);
|
|
282
|
+
if (state === "idle") break;
|
|
283
|
+
await sleep(pollIntervalMs, signal);
|
|
284
|
+
}
|
|
285
|
+
if ((await readStatus(session, sessionId, dir, signal)) !== "idle") {
|
|
286
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
287
|
+
}
|
|
288
|
+
// An aborted assistant is not the answer to the new user turn.
|
|
289
|
+
completionBaseline = new Set(
|
|
290
|
+
(await readMessages(session, sessionId, dir, signal))
|
|
291
|
+
.map(messageId)
|
|
292
|
+
.filter((value): value is string => value !== null),
|
|
293
|
+
);
|
|
294
|
+
const finalizeResponse = await session.promptAsync?.({
|
|
295
|
+
...request,
|
|
296
|
+
body: {
|
|
297
|
+
...request.body,
|
|
298
|
+
parts: [{ type: "text", text: TOKEN_BUDGET_FINALIZE_MESSAGE }],
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
if (
|
|
302
|
+
finalizeResponse &&
|
|
303
|
+
typeof finalizeResponse === "object" &&
|
|
304
|
+
"error" in finalizeResponse &&
|
|
305
|
+
finalizeResponse.error
|
|
306
|
+
) {
|
|
307
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
308
|
+
}
|
|
309
|
+
sentAt = Date.now();
|
|
310
|
+
sawBusy = false;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (budget.snapshot().hardStopped) {
|
|
315
|
+
await session.abort?.({ path: { id: sessionId } });
|
|
316
|
+
throw new DreamTokenBudgetExceeded(sessionId, budget.snapshot().spent);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (status === "busy" || status === "retry") {
|
|
320
|
+
sawBusy = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (status === null) continue;
|
|
324
|
+
const current = finalizing
|
|
325
|
+
? await readMessages(session, sessionId, dir, signal)
|
|
326
|
+
: messages;
|
|
327
|
+
const last = current.at(-1);
|
|
328
|
+
const lastId = messageId(last);
|
|
329
|
+
if (
|
|
330
|
+
last &&
|
|
331
|
+
lastId !== null &&
|
|
332
|
+
!completionBaseline.has(lastId) &&
|
|
333
|
+
isSettledAssistant(last)
|
|
334
|
+
)
|
|
335
|
+
return;
|
|
336
|
+
const fresh = current.some((message) => {
|
|
337
|
+
const id = messageId(message);
|
|
338
|
+
return id !== null && !completionBaseline.has(id);
|
|
339
|
+
});
|
|
340
|
+
if (sawBusy && fresh) return;
|
|
341
|
+
if (Date.now() - sentAt >= startGraceMs) {
|
|
342
|
+
if (fresh) return;
|
|
343
|
+
throw new Error(
|
|
344
|
+
`prompt_async did not start a run in child session ${sessionId} within ${startGraceMs}ms`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
240
347
|
}
|
|
348
|
+
} finally {
|
|
349
|
+
if (budget) releaseBudgetFinalizeChild(sessionId);
|
|
241
350
|
}
|
|
242
351
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import type { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
__resetSchemaFenceStateForTests,
|
|
8
|
+
getMigrationOnOpenRefusal,
|
|
9
|
+
openDatabaseAsync,
|
|
10
|
+
} from "../features/magic-context/storage-db";
|
|
11
|
+
import { createV2StorageGate, probeV2StorageAtBoot } from "../v2/hooks/storage-gate";
|
|
12
|
+
import {
|
|
13
|
+
__resetRpcIdentityTestHooks,
|
|
14
|
+
__setAsyncProcessProbeForTests,
|
|
15
|
+
__setRpcIdentityTestHooks,
|
|
16
|
+
inspectProcessesAsync,
|
|
17
|
+
} from "./rpc-utils";
|
|
18
|
+
import { Database } from "./sqlite";
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
__resetSchemaFenceStateForTests();
|
|
22
|
+
__resetRpcIdentityTestHooks();
|
|
23
|
+
__setAsyncProcessProbeForTests();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("async Windows inspection bypasses slow synchronous probes and shares a bounded snapshot", async () => {
|
|
27
|
+
let synchronousCalls = 0;
|
|
28
|
+
const slow = (() => {
|
|
29
|
+
synchronousCalls++;
|
|
30
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
31
|
+
return "";
|
|
32
|
+
}) as typeof execFileSync;
|
|
33
|
+
__setRpcIdentityTestHooks({
|
|
34
|
+
platform: "win32",
|
|
35
|
+
execFileSync: slow,
|
|
36
|
+
processListExecFileSync: slow,
|
|
37
|
+
});
|
|
38
|
+
let release!: (value: string) => void;
|
|
39
|
+
let calls = 0;
|
|
40
|
+
__setAsyncProcessProbeForTests(async (file, _args, timeout) => {
|
|
41
|
+
expect(file).toBe("powershell");
|
|
42
|
+
expect(timeout).toBe(5000);
|
|
43
|
+
calls++;
|
|
44
|
+
return new Promise<string>((resolve) => {
|
|
45
|
+
release = resolve;
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const first = inspectProcessesAsync();
|
|
49
|
+
const second = inspectProcessesAsync();
|
|
50
|
+
expect(second).toBe(first);
|
|
51
|
+
const heartbeat = await Promise.race([
|
|
52
|
+
first.then(() => "probe"),
|
|
53
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("heartbeat"), 10)),
|
|
54
|
+
]);
|
|
55
|
+
expect(heartbeat).toBe("heartbeat");
|
|
56
|
+
release(
|
|
57
|
+
JSON.stringify([
|
|
58
|
+
{
|
|
59
|
+
ProcessId: 12345,
|
|
60
|
+
ParentProcessId: 1,
|
|
61
|
+
CommandLine: "opencode serve",
|
|
62
|
+
CreationDate: "2026-09-28T00:00:00Z",
|
|
63
|
+
},
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
const result = await first;
|
|
67
|
+
expect(result.liveness(12345)).toBe("alive");
|
|
68
|
+
expect(result.evidence(12345).commandLine).toBe("opencode serve");
|
|
69
|
+
expect(result.liveness(12346)).toBe("dead");
|
|
70
|
+
expect(await inspectProcessesAsync()).toBe(result);
|
|
71
|
+
expect(calls).toBe(1);
|
|
72
|
+
expect(synchronousCalls).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("offline process inspection can bypass a cached snapshot", async () => {
|
|
76
|
+
__setRpcIdentityTestHooks({ platform: "win32" });
|
|
77
|
+
let calls = 0;
|
|
78
|
+
__setAsyncProcessProbeForTests(async () => {
|
|
79
|
+
calls++;
|
|
80
|
+
return JSON.stringify([
|
|
81
|
+
{
|
|
82
|
+
ProcessId: calls + 100,
|
|
83
|
+
ParentProcessId: 1,
|
|
84
|
+
Name: "opencode.exe",
|
|
85
|
+
CommandLine: null,
|
|
86
|
+
CreationDate: "2026-09-28T00:00:00Z",
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
const cached = await inspectProcessesAsync();
|
|
91
|
+
expect((await inspectProcessesAsync()).processSnapshot).toBe(cached.processSnapshot);
|
|
92
|
+
const fresh = await inspectProcessesAsync(true);
|
|
93
|
+
expect(fresh.processSnapshot?.source).toBe("cim");
|
|
94
|
+
expect(fresh.processSnapshot?.facts).toContainEqual(
|
|
95
|
+
expect.objectContaining({ pid: 102, imageName: "opencode.exe", commandLine: null }),
|
|
96
|
+
);
|
|
97
|
+
expect(calls).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("async Windows inspection bounds CIM and tasklist fallback and caches failure", async () => {
|
|
101
|
+
__setRpcIdentityTestHooks({ platform: "win32" });
|
|
102
|
+
const calls: Array<[string, number]> = [];
|
|
103
|
+
__setAsyncProcessProbeForTests(async (file, _args, timeout) => {
|
|
104
|
+
calls.push([file, timeout]);
|
|
105
|
+
throw new Error("probe timed out");
|
|
106
|
+
});
|
|
107
|
+
const result = await inspectProcessesAsync();
|
|
108
|
+
expect(result.pi.state).toBe("unreadable");
|
|
109
|
+
expect(result.liveness(123)).toBe("inconclusive");
|
|
110
|
+
expect(result.evidence(123)).toEqual({ startTime: null, commandLine: null });
|
|
111
|
+
await inspectProcessesAsync();
|
|
112
|
+
expect(calls).toEqual([
|
|
113
|
+
["powershell", 5000],
|
|
114
|
+
["tasklist", 1000],
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("boot storage wait stays responsive with a locked v90 store and slow Windows probes", async () => {
|
|
119
|
+
const root = mkdtempSync(join(tmpdir(), "async-storage-guard-"));
|
|
120
|
+
const dbPath = join(root, "context.db");
|
|
121
|
+
const seeded = new Database(dbPath);
|
|
122
|
+
seeded.exec(
|
|
123
|
+
"CREATE TABLE schema_migrations(version INTEGER PRIMARY KEY); INSERT INTO schema_migrations VALUES(90)",
|
|
124
|
+
);
|
|
125
|
+
seeded.exec("BEGIN IMMEDIATE");
|
|
126
|
+
mkdirSync(join(root, "rpc", "blocker"), { recursive: true });
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(root, "rpc", "blocker", "port-12345.json"),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
pid: 12345,
|
|
131
|
+
port: 43123,
|
|
132
|
+
started_at: Date.now(),
|
|
133
|
+
kind: "OpenCode server",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
let syncCalls = 0;
|
|
137
|
+
const slow = (() => {
|
|
138
|
+
syncCalls++;
|
|
139
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
140
|
+
return "";
|
|
141
|
+
}) as typeof execFileSync;
|
|
142
|
+
__setRpcIdentityTestHooks({
|
|
143
|
+
platform: "win32",
|
|
144
|
+
execFileSync: slow,
|
|
145
|
+
processListExecFileSync: slow,
|
|
146
|
+
});
|
|
147
|
+
let release!: (value: string) => void;
|
|
148
|
+
__setAsyncProcessProbeForTests(
|
|
149
|
+
() =>
|
|
150
|
+
new Promise<string>((resolve) => {
|
|
151
|
+
release = resolve;
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
try {
|
|
155
|
+
const gate = createV2StorageGate({
|
|
156
|
+
open: () => openDatabaseAsync({ dbPath, busyTimeoutMs: 0 }),
|
|
157
|
+
});
|
|
158
|
+
const started = performance.now();
|
|
159
|
+
const boot = probeV2StorageAtBoot(gate);
|
|
160
|
+
const opening = gate.probe();
|
|
161
|
+
expect(
|
|
162
|
+
await Promise.race([
|
|
163
|
+
opening.then(() => "opened"),
|
|
164
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("responsive"), 10)),
|
|
165
|
+
]),
|
|
166
|
+
).toBe("responsive");
|
|
167
|
+
expect(performance.now() - started).toBeLessThan(300);
|
|
168
|
+
expect(gate.current()).toBeUndefined();
|
|
169
|
+
release(
|
|
170
|
+
JSON.stringify([
|
|
171
|
+
{
|
|
172
|
+
ProcessId: 12345,
|
|
173
|
+
ParentProcessId: 1,
|
|
174
|
+
CommandLine: "opencode serve",
|
|
175
|
+
CreationDate: "2026-01-01T00:00:00Z",
|
|
176
|
+
},
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
expect(await opening).toBeUndefined();
|
|
180
|
+
expect(await boot).toBeUndefined();
|
|
181
|
+
expect(gate.reason()?.kind).toBe("migration_guard");
|
|
182
|
+
expect(getMigrationOnOpenRefusal()?.serverPids).toEqual([12345]);
|
|
183
|
+
expect(syncCalls).toBe(0);
|
|
184
|
+
const checked = new Database(dbPath);
|
|
185
|
+
expect(
|
|
186
|
+
checked.prepare("SELECT MAX(version) AS version FROM schema_migrations").get(),
|
|
187
|
+
).toEqual({ version: 90 });
|
|
188
|
+
checked.close();
|
|
189
|
+
} finally {
|
|
190
|
+
seeded.close();
|
|
191
|
+
rmSync(root, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a timed-out refresh retains confirmed blockers until a successful process scan", async () => {
|
|
196
|
+
let now = 1000;
|
|
197
|
+
__setRpcIdentityTestHooks({ platform: "win32", nowMs: () => now });
|
|
198
|
+
let fail = false;
|
|
199
|
+
let calls = 0;
|
|
200
|
+
let pid = 12345;
|
|
201
|
+
__setAsyncProcessProbeForTests(async () => {
|
|
202
|
+
calls++;
|
|
203
|
+
if (fail) throw new Error("timeout");
|
|
204
|
+
return JSON.stringify([
|
|
205
|
+
{
|
|
206
|
+
ProcessId: pid,
|
|
207
|
+
ParentProcessId: 1,
|
|
208
|
+
CommandLine: "opencode serve",
|
|
209
|
+
CreationDate: "2026-01-01T00:00:00Z",
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
const first = await inspectProcessesAsync();
|
|
214
|
+
now += 1999;
|
|
215
|
+
expect(await inspectProcessesAsync()).toBe(first);
|
|
216
|
+
expect(calls).toBe(1);
|
|
217
|
+
now++;
|
|
218
|
+
fail = true;
|
|
219
|
+
const failed = await inspectProcessesAsync();
|
|
220
|
+
expect(calls).toBe(3);
|
|
221
|
+
expect(failed.liveness(12345)).toBe("alive");
|
|
222
|
+
expect(failed.liveness(54321)).toBe("inconclusive");
|
|
223
|
+
expect(failed.evidence(12345)).toEqual(first.evidence(12345));
|
|
224
|
+
now += 2000;
|
|
225
|
+
fail = false;
|
|
226
|
+
pid = 54321;
|
|
227
|
+
const refreshed = await inspectProcessesAsync();
|
|
228
|
+
expect(calls).toBe(4);
|
|
229
|
+
expect(refreshed.liveness(12345)).toBe("dead");
|
|
230
|
+
});
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
registerNotificationSink,
|
|
20
20
|
} from "./rpc-notifications";
|
|
21
21
|
import {
|
|
22
|
-
|
|
22
|
+
inspectProcessesAsync,
|
|
23
23
|
parseRpcPortFile,
|
|
24
24
|
registerOwnRpcServerInstance,
|
|
25
25
|
rpcPortDir,
|
|
@@ -176,7 +176,7 @@ export class MagicContextRpcServer {
|
|
|
176
176
|
// supported: TUI discovery scans all live files and picks the most
|
|
177
177
|
// recent instead of cross-wiring via one shared project file.
|
|
178
178
|
try {
|
|
179
|
-
this.warnIfOtherLiveInstance();
|
|
179
|
+
void this.warnIfOtherLiveInstance();
|
|
180
180
|
const dir = dirname(this.portFilePath);
|
|
181
181
|
// The port file carries the RPC bearer token. The normal policy keeps
|
|
182
182
|
// it owner-only; a trusted-group deployment explicitly delegates every
|
|
@@ -263,12 +263,14 @@ export class MagicContextRpcServer {
|
|
|
263
263
|
this.unregisterOwnInstance = null;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
private warnIfOtherLiveInstance(): void {
|
|
266
|
+
private async warnIfOtherLiveInstance(): Promise<void> {
|
|
267
267
|
try {
|
|
268
268
|
for (const entry of readdirSync(this.portDir)) {
|
|
269
269
|
if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
|
|
270
270
|
const record = parseRpcPortFile(readFileSync(`${this.portDir}/${entry}`, "utf-8"));
|
|
271
|
-
if (!record || record.pid === process.pid
|
|
271
|
+
if (!record || record.pid === process.pid) continue;
|
|
272
|
+
const processes = await inspectProcessesAsync();
|
|
273
|
+
if (processes.liveness(record.pid) !== "alive") continue;
|
|
272
274
|
log(
|
|
273
275
|
`[rpc] another Magic Context RPC server is active for this project (pid ${record.pid}, port ${record.port}); starting separate instance on a new port`,
|
|
274
276
|
);
|
|
@@ -183,6 +183,32 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
describe("#411 Windows OMP/Pi live-process scan", () => {
|
|
186
|
+
test("reuses a Windows snapshot briefly, then refreshes process discovery", () => {
|
|
187
|
+
let now = PLUGIN_BUILD_MS;
|
|
188
|
+
const calls: string[] = [];
|
|
189
|
+
__setRpcIdentityTestHooks({
|
|
190
|
+
platform: "win32",
|
|
191
|
+
nowMs: () => now,
|
|
192
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
193
|
+
calls.push(file);
|
|
194
|
+
return cimOutput([
|
|
195
|
+
{
|
|
196
|
+
ProcessId: WINDOWS_FOREIGN_PID,
|
|
197
|
+
ParentProcessId: 4,
|
|
198
|
+
CommandLine: OMP_PI_ARC_COMMAND,
|
|
199
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
|
|
205
|
+
now += 1_999;
|
|
206
|
+
expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
|
|
207
|
+
expect(calls).toEqual(["powershell"]);
|
|
208
|
+
now += 1;
|
|
209
|
+
inspectLivePiProcesses();
|
|
210
|
+
expect(calls).toEqual(["powershell", "powershell"]);
|
|
211
|
+
});
|
|
186
212
|
test("(a) tasklist-only fallback skips a parent omp.exe as an ancestor", () => {
|
|
187
213
|
const calls: string[] = [];
|
|
188
214
|
__setRpcIdentityTestHooks({
|
|
@@ -607,7 +633,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
607
633
|
args: [
|
|
608
634
|
"-NoProfile",
|
|
609
635
|
"-Command",
|
|
610
|
-
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress",
|
|
636
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate | ConvertTo-Json -Compress",
|
|
611
637
|
],
|
|
612
638
|
},
|
|
613
639
|
]);
|