@retaw106/mmc-oc 2.4.0 → 2.6.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/{context-c72brxe5.js → context-cfbjrr0n.js} +1636 -513
- package/dist/features/magic-context/compaction-marker.d.ts +34 -6
- package/dist/features/magic-context/compaction-marker.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 +0 -26
- 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/session-decision-calibration.d.ts.map +1 -1
- package/dist/features/magic-context/session-retention.fixture.d.ts +4 -0
- package/dist/features/magic-context/session-retention.fixture.d.ts.map +1 -0
- 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-persisted.d.ts +21 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- 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-native-replay.d.ts +2 -0
- package/dist/features/magic-context/storage-native-replay.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +5 -4
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/tool-definition-tokens.d.ts +113 -58
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/features/magic-context/work-metrics.d.ts +5 -3
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +32 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/ask-answer-notice.d.ts +25 -0
- package/dist/hooks/magic-context/ask-answer-notice.d.ts.map +1 -0
- 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/compaction-marker-manager.d.ts +5 -5
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-publication.d.ts +14 -0
- package/dist/hooks/magic-context/compaction-marker-publication.d.ts.map +1 -0
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +11 -0
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.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/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 +11 -3
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-history-wait.d.ts +16 -0
- package/dist/hooks/magic-context/historian-history-wait.d.ts.map +1 -0
- package/dist/hooks/magic-context/history-boundary-repair.d.ts +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/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-measured-request.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay-fit.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 +3 -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.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/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +8 -2
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +20 -0
- package/dist/hooks/magic-context/read-session-raw.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/signed-thinking-fit-snapshot.d.ts +42 -0
- package/dist/hooks/magic-context/signed-thinking-fit-snapshot.d.ts.map +1 -0
- 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 +17 -2
- package/dist/hooks/magic-context/tag-messages.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/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/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 +6 -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 -2
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-27ddyx39.js → index-1np16gxq.js} +1 -1
- package/dist/{index-ykq24x1n.js → index-8rmt6s4f.js} +78 -32
- package/dist/{index-dwh99z66.js → index-pte3cfs9.js} +194 -5
- package/dist/{index-r7s7d350.js → index-s3ebcy63.js} +1 -1
- package/dist/{index-48e9znrc.js → index-t0zx3tgz.js} +481 -109
- package/dist/{index-kahkxcqw.js → index-xk1dz6q9.js} +870 -315
- package/dist/index.js +2 -2
- package/dist/{message-time-backfill-j96x1sab.js → message-time-backfill-g9py24yt.js} +1 -1
- 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-t8bg6wkz.js → read-session-chunk-2s8x6xps.js} +3 -3
- package/dist/{recomp-orchestrator-k760qy1n.js → recomp-orchestrator-51xyaexp.js} +6 -6
- package/dist/{send-session-notification-njjf3b4s.js → send-session-notification-vdppp5x6.js} +3 -3
- package/dist/shared/model-suggestion-retry.d.ts +2 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- 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 +6 -1
- package/dist/shared/sqlite.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/tag-transcript.d.ts +2 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/transcript.d.ts +3 -1
- package/dist/shared/transcript.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/parameter-descriptions.d.ts +6 -6
- package/dist/tools/parameter-descriptions.d.ts.map +1 -1
- 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/hooks/context.d.ts +4 -0
- 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/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/message-times.d.ts +17 -0
- package/dist/v2/message-times.d.ts.map +1 -0
- package/dist/v2/server.js +4692 -2394
- package/dist/v2/store-reader.d.ts +21 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +307 -12
- package/dist/{wrapup-orchestrator-b2gswatj.js → wrapup-orchestrator-g0mww1h4.js} +6 -6
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +4 -5
- package/src/shared/model-suggestion-retry.test.ts +52 -0
- package/src/shared/model-suggestion-retry.ts +8 -3
- package/src/shared/prompt-surface-a1-golden.md +6 -7
- 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-transaction-mode-guard.test.ts +2 -0
- package/src/shared/sqlite-turn-deadline.test.ts +286 -0
- package/src/shared/sqlite.ts +237 -9
- 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 +63 -8
- package/src/shared/transcript.ts +1 -1
- package/src/shared/write-transaction-attribution.test.ts +1 -6
- package/dist/features/magic-context/message-index-sweep.d.ts +0 -11
- package/dist/features/magic-context/message-index-sweep.d.ts.map +0 -1
|
@@ -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
|
/**
|
|
@@ -488,6 +490,7 @@ async function attemptOnce(
|
|
|
488
490
|
callContext: string,
|
|
489
491
|
label: string,
|
|
490
492
|
transport?: PromptTransport,
|
|
493
|
+
allowModelSuggestions = true,
|
|
491
494
|
): Promise<void> {
|
|
492
495
|
// Keep this snapshot separate from the object passed to the client. A
|
|
493
496
|
// failed prompt facade may rewrite its request body before rejecting; the
|
|
@@ -498,9 +501,8 @@ async function attemptOnce(
|
|
|
498
501
|
await promptWithTimeout(client, attemptArgs, timeoutMs, signal, transport);
|
|
499
502
|
return;
|
|
500
503
|
} catch (error) {
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
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;
|
|
504
506
|
|
|
505
507
|
const suggestion = parseModelSuggestion(error);
|
|
506
508
|
if (!suggestion || !originalBody.model) {
|
|
@@ -576,6 +578,7 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
576
578
|
callContext,
|
|
577
579
|
explicitPrimaryLabel,
|
|
578
580
|
options.transport,
|
|
581
|
+
options.allowModelSuggestions,
|
|
579
582
|
);
|
|
580
583
|
return;
|
|
581
584
|
} catch (error) {
|
|
@@ -620,6 +623,7 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
620
623
|
callContext,
|
|
621
624
|
label,
|
|
622
625
|
options.transport,
|
|
626
|
+
options.allowModelSuggestions,
|
|
623
627
|
);
|
|
624
628
|
log(
|
|
625
629
|
`[${callContext}] fallback succeeded with ${label} (attempt ${i + 2}/${fallbacks.length + 1})`,
|
|
@@ -665,6 +669,7 @@ async function attemptAndValidate<TOutput, TValidated>(
|
|
|
665
669
|
callContext,
|
|
666
670
|
attempt.label,
|
|
667
671
|
options.transport,
|
|
672
|
+
options.allowModelSuggestions,
|
|
668
673
|
);
|
|
669
674
|
} catch (error) {
|
|
670
675
|
throw {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Magic Context agent-facing surface (exported from source)
|
|
2
2
|
|
|
3
|
-
Generated by `packages/plugin/scripts/export-agent-surface.ts` at 2026-
|
|
3
|
+
Generated by `packages/plugin/scripts/export-agent-surface.ts` at 2026-11-01.
|
|
4
4
|
Token counts are Claude BPE estimates on the raw text.
|
|
5
5
|
|
|
6
6
|
## 1. System-prompt guidance section
|
|
@@ -201,7 +201,7 @@ Finer recovery:
|
|
|
201
201
|
}
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
### mmc_note — description ~319 tokens, params ~
|
|
204
|
+
### mmc_note — description ~319 tokens, params ~196 tokens (total ~515)
|
|
205
205
|
|
|
206
206
|
**Description:**
|
|
207
207
|
|
|
@@ -228,7 +228,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
|
|
|
228
228
|
```json
|
|
229
229
|
{
|
|
230
230
|
"action": {
|
|
231
|
-
"description": "write | read | update | dismiss.
|
|
231
|
+
"description": "write | read | update | dismiss.",
|
|
232
232
|
"type": "string",
|
|
233
233
|
"enum": [
|
|
234
234
|
"write",
|
|
@@ -259,8 +259,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
|
|
|
259
259
|
"type": "number"
|
|
260
260
|
},
|
|
261
261
|
"note_ids": {
|
|
262
|
-
"description": "Note ids: one for update, 1–50 for dismiss or
|
|
263
|
-
"minItems": 1,
|
|
262
|
+
"description": "Note ids: one for update, 1–50 for dismiss, or 1–50 to read those notes in full. Write and a plain read take an empty list.",
|
|
264
263
|
"maxItems": 50,
|
|
265
264
|
"type": "array",
|
|
266
265
|
"items": {
|
|
@@ -272,7 +271,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
|
|
|
272
271
|
}
|
|
273
272
|
```
|
|
274
273
|
|
|
275
|
-
### mmc_memory — description ~279 tokens, params ~
|
|
274
|
+
### mmc_memory — description ~279 tokens, params ~175 tokens (total ~454)
|
|
276
275
|
|
|
277
276
|
**Description:**
|
|
278
277
|
|
|
@@ -310,7 +309,7 @@ Examples: category="CONFIG_VALUES", content="OpenCode source is at ~/Work/OSS/op
|
|
|
310
309
|
"type": "string"
|
|
311
310
|
},
|
|
312
311
|
"category": {
|
|
313
|
-
"description": "Kind of fact (required for write; on update/merge
|
|
312
|
+
"description": "Kind of fact (required for write; on update/merge, pass the memory's current category to keep it).",
|
|
314
313
|
"type": "string",
|
|
315
314
|
"enum": [
|
|
316
315
|
"PROJECT_RULES",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { reapDeadRpcDiscovery } from "./rpc-discovery-cleanup";
|
|
6
|
+
import { MagicContextRpcServer } from "./rpc-server";
|
|
7
|
+
import * as utils from "./rpc-utils";
|
|
8
|
+
import * as storage from "./storage-permissions";
|
|
9
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
10
|
+
|
|
11
|
+
test("RPC startup reaps dead records across projects but keeps live and inconclusive records", async () => {
|
|
12
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "rpc-reap-"));
|
|
13
|
+
const server = new MagicContextRpcServer(root, "/current-project");
|
|
14
|
+
const dir = utils.rpcPortDir(root, "/other-project");
|
|
15
|
+
const empty = utils.rpcPortDir(root, "/empty-project");
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
mkdirSync(empty, { recursive: true });
|
|
18
|
+
for (const pid of [999999, 888888, process.pid])
|
|
19
|
+
writeFileSync(
|
|
20
|
+
join(dir, `port-${pid}.json`),
|
|
21
|
+
JSON.stringify({ port: 1234, pid, token: "do-not-log" }),
|
|
22
|
+
);
|
|
23
|
+
const probe = spyOn(utils, "inspectProcessesAsync").mockResolvedValue({
|
|
24
|
+
pi: { state: "absent", processIds: [] },
|
|
25
|
+
evidence: () => ({ processStartTime: null, commandLine: null, imageName: null }),
|
|
26
|
+
liveness: (pid: number) =>
|
|
27
|
+
pid === 999999 ? "dead" : pid === 888888 ? "inconclusive" : "alive",
|
|
28
|
+
} as Awaited<ReturnType<typeof utils.inspectProcessesAsync>>);
|
|
29
|
+
try {
|
|
30
|
+
await server.start();
|
|
31
|
+
expect(readdirSync(dir).sort()).toEqual(
|
|
32
|
+
[`port-${process.pid}.json`, "port-888888.json"].sort(),
|
|
33
|
+
);
|
|
34
|
+
expect(existsSync(empty)).toBe(false);
|
|
35
|
+
} finally {
|
|
36
|
+
server.stop();
|
|
37
|
+
probe.mockRestore();
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("cleanup keeps changed, malformed and out-of-scope records and requests fresh evidence", async () => {
|
|
43
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "rpc-reap-race-"));
|
|
44
|
+
const directory = utils.rpcPortDir(root, "/project");
|
|
45
|
+
const outside = join(root, "rpc", "not-a-project-hash");
|
|
46
|
+
mkdirSync(directory, { recursive: true });
|
|
47
|
+
mkdirSync(outside, { recursive: true });
|
|
48
|
+
const path = join(directory, "port-999999.json");
|
|
49
|
+
writeFileSync(path, JSON.stringify({ port: 1234, pid: 999999, token: "old" }));
|
|
50
|
+
writeFileSync(join(directory, "port-888888.json"), "not json");
|
|
51
|
+
writeFileSync(join(directory, "unrelated.json"), "keep");
|
|
52
|
+
mkdirSync(join(directory, "port-777777.json"));
|
|
53
|
+
writeFileSync(join(outside, "port-999999.json"), "keep");
|
|
54
|
+
const probe = spyOn(utils, "inspectProcessesAsync").mockImplementation(async (fresh) => {
|
|
55
|
+
expect(fresh).toBe(true);
|
|
56
|
+
writeFileSync(path, JSON.stringify({ port: 1234, pid: 999999, token: "new" }));
|
|
57
|
+
return {
|
|
58
|
+
pi: { state: "absent", processIds: [] },
|
|
59
|
+
evidence: () => ({ startTime: null, commandLine: null }),
|
|
60
|
+
liveness: () => "dead",
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await reapDeadRpcDiscovery(root);
|
|
65
|
+
expect(probe).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(readdirSync(directory)).toHaveLength(4);
|
|
67
|
+
expect(existsSync(join(outside, "port-999999.json"))).toBe(true);
|
|
68
|
+
} finally {
|
|
69
|
+
probe.mockRestore();
|
|
70
|
+
rmSync(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("stop during startup prevents listening and publishing", async () => {
|
|
75
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "rpc-start-stop-"));
|
|
76
|
+
const server = new MagicContextRpcServer(root, root);
|
|
77
|
+
try {
|
|
78
|
+
const starting = server.start();
|
|
79
|
+
server.stop();
|
|
80
|
+
expect(await starting).toBe(0);
|
|
81
|
+
expect(existsSync(utils.rpcPortDir(root, root))).toBe(false);
|
|
82
|
+
} finally {
|
|
83
|
+
server.stop();
|
|
84
|
+
rmSync(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test.each(["ENOENT", "EACCES"])("publishing retries only ENOENT (%s)", async (code) => {
|
|
89
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "rpc-publish-race-"));
|
|
90
|
+
const server = new MagicContextRpcServer(root, root);
|
|
91
|
+
const original = storage.writeStorageFileAtomicSync;
|
|
92
|
+
let calls = 0;
|
|
93
|
+
const writer = spyOn(storage, "writeStorageFileAtomicSync").mockImplementation((...args) => {
|
|
94
|
+
if (++calls === 1) throw Object.assign(new Error("publish race"), { code });
|
|
95
|
+
return original(...args);
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await server.start();
|
|
99
|
+
expect(calls).toBe(code === "ENOENT" ? 2 : 1);
|
|
100
|
+
expect(existsSync(utils.rpcPortDir(root, root))).toBe(code === "ENOENT");
|
|
101
|
+
} finally {
|
|
102
|
+
server.stop();
|
|
103
|
+
writer.mockRestore();
|
|
104
|
+
rmSync(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("RPC stop removes its empty discovery directory but preserves other instances", async () => {
|
|
109
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "rpc-stop-reap-"));
|
|
110
|
+
const first = new MagicContextRpcServer(root, root);
|
|
111
|
+
const second = new MagicContextRpcServer(root, root);
|
|
112
|
+
const directory = utils.rpcPortDir(root, root);
|
|
113
|
+
try {
|
|
114
|
+
await first.start();
|
|
115
|
+
await second.start();
|
|
116
|
+
first.stop();
|
|
117
|
+
expect(readdirSync(directory)).toHaveLength(1);
|
|
118
|
+
second.stop();
|
|
119
|
+
expect(existsSync(directory)).toBe(false);
|
|
120
|
+
} finally {
|
|
121
|
+
first.stop();
|
|
122
|
+
second.stop();
|
|
123
|
+
rmSync(root, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Dirent, promises as fs } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { inspectProcessesAsync, parseRpcPortFile } from "./rpc-utils";
|
|
4
|
+
|
|
5
|
+
/** Reap only records proven dead; live, unreadable and uncertain records stay. */
|
|
6
|
+
export async function reapDeadRpcDiscovery(storageDir: string): Promise<void> {
|
|
7
|
+
const root = join(storageDir, "rpc");
|
|
8
|
+
let directories: Dirent[];
|
|
9
|
+
try {
|
|
10
|
+
directories = await fs.readdir(root, { withFileTypes: true });
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const records: { path: string; content: string; pid: number }[] = [];
|
|
15
|
+
const projectDirs: string[] = [];
|
|
16
|
+
for (const entry of directories) {
|
|
17
|
+
if (!entry.isDirectory() || !/^[0-9a-f]{16}$/.test(entry.name)) continue;
|
|
18
|
+
const directory = join(root, entry.name);
|
|
19
|
+
projectDirs.push(directory);
|
|
20
|
+
try {
|
|
21
|
+
for (const name of await fs.readdir(directory)) {
|
|
22
|
+
const match = /^port-(\d+)(?:-[0-9a-f]+)?\.json$/.exec(name);
|
|
23
|
+
if (!match) continue;
|
|
24
|
+
const path = join(directory, name);
|
|
25
|
+
try {
|
|
26
|
+
const content = await fs.readFile(path, "utf8");
|
|
27
|
+
const record = parseRpcPortFile(content, Number(match[1]));
|
|
28
|
+
if (record && record.pid === Number(match[1]))
|
|
29
|
+
records.push({ path, content, pid: record.pid });
|
|
30
|
+
} catch {
|
|
31
|
+
// Another host may be publishing or stopping this record.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// An inaccessible project must not prevent cleanup of other projects.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Read records before a fresh process snapshot so a recently started host
|
|
39
|
+
// cannot be mistaken for a PID missing from an older cached snapshot.
|
|
40
|
+
const processes = records.length ? await inspectProcessesAsync(true) : undefined;
|
|
41
|
+
for (const record of records) {
|
|
42
|
+
if (processes?.liveness(record.pid) !== "dead") continue;
|
|
43
|
+
try {
|
|
44
|
+
if ((await fs.readFile(record.path, "utf8")) === record.content)
|
|
45
|
+
await fs.unlink(record.path);
|
|
46
|
+
} catch {
|
|
47
|
+
// Leave the record for a later pass; never recursively remove its directory.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const directory of projectDirs) {
|
|
51
|
+
try {
|
|
52
|
+
await fs.rmdir(directory);
|
|
53
|
+
} catch {
|
|
54
|
+
// Leave non-empty directories untouched; their owners may not be proven dead.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { readdirSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
|
|
3
3
|
import type { Server, ServerWebSocket } from "bun";
|
|
4
4
|
import { log } from "./logger";
|
|
5
|
+
import { reapDeadRpcDiscovery } from "./rpc-discovery-cleanup";
|
|
5
6
|
import {
|
|
6
7
|
acknowledgeNotifications,
|
|
7
8
|
drainNotifications,
|
|
@@ -82,6 +83,7 @@ export class MagicContextRpcServer {
|
|
|
82
83
|
private portFilePath: string;
|
|
83
84
|
private portDir: string;
|
|
84
85
|
private startedAt = Date.now();
|
|
86
|
+
private stopped = false;
|
|
85
87
|
private readonly instanceId = randomBytes(8).toString("hex");
|
|
86
88
|
private unregisterOwnInstance: (() => void) | null = null;
|
|
87
89
|
/** Every authenticated WS socket, so dispose can close them all. */
|
|
@@ -93,7 +95,10 @@ export class MagicContextRpcServer {
|
|
|
93
95
|
// discovers/guesses the port.
|
|
94
96
|
private readonly token = randomBytes(32).toString("hex");
|
|
95
97
|
|
|
96
|
-
constructor(
|
|
98
|
+
constructor(
|
|
99
|
+
private storageDir: string,
|
|
100
|
+
directory: string,
|
|
101
|
+
) {
|
|
97
102
|
this.portFilePath = rpcPortFilePath(storageDir, directory, process.pid, this.instanceId);
|
|
98
103
|
this.portDir = rpcPortDir(storageDir, directory);
|
|
99
104
|
}
|
|
@@ -131,6 +136,9 @@ export class MagicContextRpcServer {
|
|
|
131
136
|
log("rpc server skipped: Bun runtime not available (no TUI consumer)");
|
|
132
137
|
return 0;
|
|
133
138
|
}
|
|
139
|
+
this.stopped = false;
|
|
140
|
+
await reapDeadRpcDiscovery(this.storageDir);
|
|
141
|
+
if (this.stopped) return 0;
|
|
134
142
|
this.startedAt = Date.now();
|
|
135
143
|
const self = this;
|
|
136
144
|
const server = Bun.serve<WsData>({
|
|
@@ -170,17 +178,26 @@ export class MagicContextRpcServer {
|
|
|
170
178
|
// The discovery file carries the RPC bearer token. The shared writer
|
|
171
179
|
// creates its directory and temporary file with owner-only modes.
|
|
172
180
|
this.unregisterOwnInstance ??= registerOwnRpcServerInstance(this.instanceId);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
const publish = () =>
|
|
182
|
+
writeStorageFileAtomicSync(
|
|
183
|
+
this.portFilePath,
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
port: this.port,
|
|
186
|
+
pid: process.pid,
|
|
187
|
+
started_at: this.startedAt,
|
|
188
|
+
kind: "OpenCode server",
|
|
189
|
+
token: this.token,
|
|
190
|
+
instance_id: this.instanceId,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
try {
|
|
194
|
+
publish();
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// An empty-directory reaper can race the writer before its temp
|
|
197
|
+
// file exists. Recreate the directory and retry that case only.
|
|
198
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
199
|
+
publish();
|
|
200
|
+
}
|
|
184
201
|
log(`[rpc] server listening on 127.0.0.1:${this.port}`);
|
|
185
202
|
} catch (err) {
|
|
186
203
|
log(`[rpc] failed to write port file: ${err}`);
|
|
@@ -191,6 +208,7 @@ export class MagicContextRpcServer {
|
|
|
191
208
|
|
|
192
209
|
/** Stop the server: close every socket, stop accepting, remove port file. */
|
|
193
210
|
stop(): void {
|
|
211
|
+
this.stopped = true;
|
|
194
212
|
for (const ws of this.sockets) {
|
|
195
213
|
try {
|
|
196
214
|
if (ws.data.authTimer) clearTimeout(ws.data.authTimer);
|
|
@@ -211,6 +229,11 @@ export class MagicContextRpcServer {
|
|
|
211
229
|
} catch {
|
|
212
230
|
// Intentional: port file may already be gone
|
|
213
231
|
}
|
|
232
|
+
try {
|
|
233
|
+
rmdirSync(this.portDir);
|
|
234
|
+
} catch {
|
|
235
|
+
// Other instances' discovery records must survive this server's stop.
|
|
236
|
+
}
|
|
214
237
|
this.unregisterOwnInstance?.();
|
|
215
238
|
this.unregisterOwnInstance = null;
|
|
216
239
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { cleanSearchText, keywordRelevance, matchedSearchSnippet } from "./search-text";
|
|
3
|
+
|
|
4
|
+
test("notification trees never contribute relevance or display text", () => {
|
|
5
|
+
expect(
|
|
6
|
+
cleanSearchText(
|
|
7
|
+
'<task_cohort id="parent"><task>hidden</task>status</task_cohort> actual answer',
|
|
8
|
+
),
|
|
9
|
+
).toBe("actual answer");
|
|
10
|
+
expect(cleanSearchText("kept <task>hidden </task_cohort> still hidden</task> kept")).toBe(
|
|
11
|
+
"kept kept",
|
|
12
|
+
);
|
|
13
|
+
expect(cleanSearchText('<task id="truncated">background result…')).toBe("");
|
|
14
|
+
expect(keywordRelevance("historian cache wiring", "<task>historian cache wiring</task>")).toBe(
|
|
15
|
+
0,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("keyword relevance measures distinct coverage rather than rank", () => {
|
|
20
|
+
expect(keywordRelevance("explain historian cache wiring", "cache icons")).toBeCloseTo(0.2);
|
|
21
|
+
expect(keywordRelevance("historian cache wiring", "historian cache wiring")).toBeCloseTo(0.8);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("snippets prefer dense passages over an irrelevant header", () => {
|
|
25
|
+
const content = `Header cache. ${"padding ".repeat(150)}Historian cache wiring uses incremental pools. ${"trailing ".repeat(100)}`;
|
|
26
|
+
const snippet = matchedSearchSnippet(content, "historian cache wiring", 80);
|
|
27
|
+
expect(snippet).toContain("Historian cache wiring");
|
|
28
|
+
expect(snippet).not.toContain("Header");
|
|
29
|
+
expect(matchedSearchSnippet(content, "unrelated deployment")).toBe("");
|
|
30
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { removeNestedBlocks } from "./system-directive";
|
|
2
|
+
|
|
3
|
+
export function removeTaskNotifications(text: string): string {
|
|
4
|
+
return removeNestedBlocks(text, ["task", "task_cohort"]);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Remove injected notifications before scoring or rendering; saved hints replay unchanged. */
|
|
8
|
+
export function cleanSearchText(text: string): string {
|
|
9
|
+
return removeNestedBlocks(text, ["system-reminder", "task", "task_cohort"])
|
|
10
|
+
.replace(/\s+/g, " ")
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const QUERY_STOP_WORDS = new Set(
|
|
15
|
+
"a an and are as at be been by can could did do does for from how i in is it me my of on or our please should that the their them there these they this to us was we were what when where which who why will with would you your".split(
|
|
16
|
+
" ",
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
function terms(text: string): string[] {
|
|
21
|
+
return [
|
|
22
|
+
...new Set(
|
|
23
|
+
(text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
|
|
24
|
+
(term) => !QUERY_STOP_WORDS.has(term),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Score distinct query coverage independently of other results. */
|
|
31
|
+
export function keywordRelevance(query: string, content: string): number {
|
|
32
|
+
const needles = terms(query);
|
|
33
|
+
if (needles.length === 0) return 0;
|
|
34
|
+
const words = new Set(terms(cleanSearchText(content)));
|
|
35
|
+
return (0.8 * needles.filter((term) => words.has(term)).length) / needles.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Pick the densest matching passage before compression discards matched words. */
|
|
39
|
+
export function matchedSearchSnippet(content: string, query: string, cap = 220): string {
|
|
40
|
+
const text = cleanSearchText(content);
|
|
41
|
+
const needles = new Set(terms(query));
|
|
42
|
+
const matches = [...text.matchAll(/[\p{L}\p{N}_]+/gu)].filter((match) =>
|
|
43
|
+
needles.has(match[0].toLowerCase()),
|
|
44
|
+
);
|
|
45
|
+
if (matches.length === 0) return "";
|
|
46
|
+
let bestStart = matches[0].index;
|
|
47
|
+
let bestCoverage = -1;
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
const start = Math.max(0, match.index - Math.floor(cap / 5));
|
|
50
|
+
const coverage = keywordRelevance(query, text.slice(start, start + cap));
|
|
51
|
+
if (coverage > bestCoverage) {
|
|
52
|
+
bestStart = start;
|
|
53
|
+
bestCoverage = coverage;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (bestStart > 0) {
|
|
57
|
+
const boundary = text.indexOf(" ", bestStart);
|
|
58
|
+
if (boundary >= 0 && boundary < matches[matches.length - 1].index) bestStart = boundary + 1;
|
|
59
|
+
}
|
|
60
|
+
const passage = text.slice(bestStart, bestStart + cap).trim();
|
|
61
|
+
return `${bestStart > 0 ? "…" : ""}${passage}${bestStart + cap < text.length ? "…" : ""}`;
|
|
62
|
+
}
|
|
@@ -27,6 +27,8 @@ const READ_ONLY_DEFERRED_TRANSACTIONS: Record<string, string> = {
|
|
|
27
27
|
"pins one read snapshot so the session's rowid coverage proof and the filtered message search see the same rows; never writes (read-only connections use it too)",
|
|
28
28
|
"packages/plugin/src/hooks/magic-context/lkg-persist.ts#loadPersistedLkgSlot":
|
|
29
29
|
"reads an LKG slot row and its prefix slices as one consistent snapshot even while another connection saves the slot; never writes (a failed verification clear opens its own transaction)",
|
|
30
|
+
"packages/plugin/src/hooks/magic-context/compaction-marker-manager.ts#reconcileForkOrphanedCompactionMarkers":
|
|
31
|
+
"reads publication intent and finalized ownership as one context.db snapshot while host-order probes use the already-open host connection; never writes context.db",
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const REPOSITORY_ROOT = resolve(import.meta.dir, "../../../..");
|