@cortexkit/opencode-magic-context 0.18.0 → 0.20.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/README.md +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +17 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts +11 -0
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +1 -0
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/aft-availability.d.ts +11 -0
- package/dist/features/magic-context/key-files/aft-availability.d.ts.map +1 -0
- package/dist/features/magic-context/key-files/identify-key-files.d.ts +45 -0
- package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/project-key-files.d.ts +42 -0
- package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -0
- package/dist/features/magic-context/key-files/read-history.d.ts +26 -0
- package/dist/features/magic-context/key-files/read-history.d.ts.map +1 -0
- package/dist/features/magic-context/memory/embedding-identity.d.ts +11 -0
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -0
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +1 -1
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +64 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +24 -0
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +10 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +50 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +16 -7
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +7 -2
- package/dist/hooks/magic-context/compartment-runner.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/historian-state-file.d.ts +25 -11
- package/dist/hooks/magic-context/historian-state-file.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +11 -4
- 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/inject-compartments.d.ts +2 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/key-files-block.d.ts +27 -0
- package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -0
- package/dist/hooks/magic-context/live-session-state.d.ts +3 -1
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +2 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +10 -4
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-context-state.d.ts +5 -2
- package/dist/hooks/magic-context/transform-context-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +15 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +3 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.js +2316 -1112
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/sidebar-snapshot-cache.d.ts.map +1 -1
- package/dist/shared/conflict-detector.d.ts +49 -0
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/conflict-fixer.d.ts +1 -1
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/data-path.d.ts +84 -0
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +2 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +3 -2
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +3 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +1 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +13 -2
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/stable-json.d.ts +21 -0
- package/dist/shared/stable-json.d.ts.map +1 -0
- package/dist/shared/transcript.d.ts +2 -2
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shared/conflict-detector.ts +4 -4
- package/src/shared/conflict-fixer.test.ts +124 -0
- package/src/shared/conflict-fixer.ts +34 -28
- package/src/shared/data-path.test.ts +38 -0
- package/src/shared/data-path.ts +99 -0
- package/src/shared/logger.ts +29 -3
- package/src/shared/rpc-client.test.ts +161 -0
- package/src/shared/rpc-client.ts +82 -22
- package/src/shared/rpc-notifications.test.ts +20 -0
- package/src/shared/rpc-notifications.ts +9 -6
- package/src/shared/rpc-server.ts +42 -4
- package/src/shared/rpc-types.ts +1 -0
- package/src/shared/rpc-utils.ts +59 -3
- package/src/shared/stable-json.test.ts +87 -0
- package/src/shared/stable-json.ts +37 -0
- package/src/shared/transcript.ts +2 -2
- package/src/tui/data/context-db.ts +20 -1
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export interface RpcNotification {
|
|
11
|
+
id: number;
|
|
11
12
|
type: string;
|
|
12
13
|
payload: Record<string, unknown>;
|
|
13
14
|
sessionId?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
let queue: RpcNotification[] = [];
|
|
18
|
+
let nextNotificationId = 1;
|
|
17
19
|
// Timestamp of last drain — used to detect if TUI is actively polling.
|
|
18
20
|
// The TUI polls every 500ms; we consider it connected if it polled within
|
|
19
21
|
// the last 3 seconds (6× the poll interval, tolerates transient delays).
|
|
@@ -26,20 +28,21 @@ export function pushNotification(
|
|
|
26
28
|
payload: Record<string, unknown>,
|
|
27
29
|
sessionId?: string,
|
|
28
30
|
): void {
|
|
29
|
-
queue.push({ type, payload, sessionId });
|
|
31
|
+
queue.push({ id: nextNotificationId++, type, payload, sessionId });
|
|
30
32
|
// Cap queue size to prevent unbounded growth if TUI is not polling
|
|
31
33
|
if (queue.length > 100) {
|
|
32
34
|
queue = queue.slice(-50);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
/**
|
|
38
|
+
/** Return pending notifications after acking the client's last received id.
|
|
37
39
|
* Updates lastDrainAt so isTuiConnected() reflects recent activity. */
|
|
38
|
-
export function drainNotifications(): RpcNotification[] {
|
|
40
|
+
export function drainNotifications(lastReceivedId = 0): RpcNotification[] {
|
|
39
41
|
lastDrainAt = Date.now();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
if (lastReceivedId > 0) {
|
|
43
|
+
queue = queue.filter((notification) => notification.id > lastReceivedId);
|
|
44
|
+
}
|
|
45
|
+
return [...queue];
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** Whether a TUI client is actively polling for notifications.
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
2
9
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
10
|
import { dirname } from "node:path";
|
|
4
11
|
import { log } from "./logger";
|
|
5
|
-
import { rpcPortFilePath } from "./rpc-utils";
|
|
12
|
+
import { isPidAlive, parseRpcPortFile, rpcPortDir, rpcPortFilePath } from "./rpc-utils";
|
|
6
13
|
|
|
7
14
|
type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
8
15
|
|
|
@@ -11,9 +18,12 @@ export class MagicContextRpcServer {
|
|
|
11
18
|
private port = 0;
|
|
12
19
|
private handlers = new Map<string, RpcHandler>();
|
|
13
20
|
private portFilePath: string;
|
|
21
|
+
private portDir: string;
|
|
22
|
+
private startedAt = Date.now();
|
|
14
23
|
|
|
15
24
|
constructor(storageDir: string, directory: string) {
|
|
16
25
|
this.portFilePath = rpcPortFilePath(storageDir, directory);
|
|
26
|
+
this.portDir = rpcPortDir(storageDir, directory);
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
/** Register an RPC method handler. */
|
|
@@ -40,12 +50,24 @@ export class MagicContextRpcServer {
|
|
|
40
50
|
this.port = addr.port;
|
|
41
51
|
this.server = server;
|
|
42
52
|
|
|
43
|
-
// Write port file atomically
|
|
53
|
+
// Write a per-process port file atomically. Multi-instance
|
|
54
|
+
// OpenCode is supported: TUI discovery scans all live pid files
|
|
55
|
+
// and picks the most recent instead of cross-wiring via one
|
|
56
|
+
// shared project file.
|
|
44
57
|
try {
|
|
58
|
+
this.warnIfOtherLiveInstance();
|
|
45
59
|
const dir = dirname(this.portFilePath);
|
|
46
60
|
mkdirSync(dir, { recursive: true });
|
|
47
61
|
const tmpPath = `${this.portFilePath}.tmp`;
|
|
48
|
-
writeFileSync(
|
|
62
|
+
writeFileSync(
|
|
63
|
+
tmpPath,
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
port: this.port,
|
|
66
|
+
pid: process.pid,
|
|
67
|
+
started_at: this.startedAt,
|
|
68
|
+
}),
|
|
69
|
+
"utf-8",
|
|
70
|
+
);
|
|
49
71
|
renameSync(tmpPath, this.portFilePath);
|
|
50
72
|
log(`[rpc] server listening on 127.0.0.1:${this.port}`);
|
|
51
73
|
} catch (err) {
|
|
@@ -60,6 +82,22 @@ export class MagicContextRpcServer {
|
|
|
60
82
|
});
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
private warnIfOtherLiveInstance(): void {
|
|
86
|
+
try {
|
|
87
|
+
for (const entry of readdirSync(this.portDir)) {
|
|
88
|
+
if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
|
|
89
|
+
const record = parseRpcPortFile(readFileSync(`${this.portDir}/${entry}`, "utf-8"));
|
|
90
|
+
if (!record || record.pid === process.pid || !isPidAlive(record.pid)) continue;
|
|
91
|
+
log(
|
|
92
|
+
`[rpc] another Magic Context RPC server is active for this project (pid ${record.pid}, port ${record.port}); starting separate instance on a new port`,
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// No discovery directory yet, or unreadable stale file. Not fatal.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
63
101
|
/** Stop the server and clean up port file. */
|
|
64
102
|
stop(): void {
|
|
65
103
|
if (this.server) {
|
package/src/shared/rpc-types.ts
CHANGED
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
export interface RpcPortFileRecord {
|
|
5
|
+
port: number;
|
|
6
|
+
pid: number;
|
|
7
|
+
started_at: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
11
|
* Stable hash for a project directory — scopes RPC port files per-project
|
|
6
12
|
* so multiple OpenCode instances don't collide.
|
|
@@ -10,7 +16,57 @@ export function projectHash(directory: string): string {
|
|
|
10
16
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
export function
|
|
15
|
-
return join(storageDir, "rpc", projectHash(directory)
|
|
19
|
+
/** Directory containing per-process RPC discovery files for a project. */
|
|
20
|
+
export function rpcPortDir(storageDir: string, directory: string): string {
|
|
21
|
+
return join(storageDir, "rpc", projectHash(directory));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Per-process RPC port file path. */
|
|
25
|
+
export function rpcPortFilePath(storageDir: string, directory: string, pid = process.pid): string {
|
|
26
|
+
return join(rpcPortDir(storageDir, directory), `port-${pid}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Legacy single-port file used by v0.18.0 and earlier. */
|
|
30
|
+
export function legacyRpcPortFilePath(storageDir: string, directory: string): string {
|
|
31
|
+
return join(rpcPortDir(storageDir, directory), "port");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isPidAlive(pid: number): boolean {
|
|
35
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, 0);
|
|
38
|
+
return true;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileRecord | null {
|
|
45
|
+
const trimmed = content.trim();
|
|
46
|
+
if (!trimmed) return null;
|
|
47
|
+
|
|
48
|
+
if (trimmed.startsWith("{")) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(trimmed) as Partial<RpcPortFileRecord>;
|
|
51
|
+
const port = Number(parsed.port);
|
|
52
|
+
const pid = Number(parsed.pid);
|
|
53
|
+
const startedAt = Number(parsed.started_at);
|
|
54
|
+
if (!isValidPort(port) || !Number.isInteger(pid) || pid <= 0) return null;
|
|
55
|
+
return {
|
|
56
|
+
port,
|
|
57
|
+
pid,
|
|
58
|
+
started_at: Number.isFinite(startedAt) ? startedAt : 0,
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const port = Number.parseInt(trimmed, 10);
|
|
66
|
+
if (!isValidPort(port)) return null;
|
|
67
|
+
return { port, pid: fallbackPid, started_at: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isValidPort(port: number): boolean {
|
|
71
|
+
return Number.isInteger(port) && port > 0 && port <= 65535;
|
|
16
72
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { stableStringify } from "./stable-json";
|
|
3
|
+
|
|
4
|
+
describe("stableStringify", () => {
|
|
5
|
+
test("primitive values match JSON.stringify", () => {
|
|
6
|
+
expect(stableStringify("hello")).toBe('"hello"');
|
|
7
|
+
expect(stableStringify(42)).toBe("42");
|
|
8
|
+
expect(stableStringify(true)).toBe("true");
|
|
9
|
+
expect(stableStringify(false)).toBe("false");
|
|
10
|
+
expect(stableStringify(null)).toBe("null");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("undefined renders as literal string", () => {
|
|
14
|
+
expect(stableStringify(undefined)).toBe("undefined");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("object keys sort by code-point order, not locale", () => {
|
|
18
|
+
// 'Z' (0x5a) sorts before 'a' (0x61) by code-point.
|
|
19
|
+
// localeCompare would sort 'a' before 'Z' in many locales.
|
|
20
|
+
// We want code-point semantics.
|
|
21
|
+
const input = { Z: 1, a: 2 };
|
|
22
|
+
expect(stableStringify(input)).toBe('{"Z":1,"a":2}');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("nested objects sort recursively", () => {
|
|
26
|
+
const input = { b: { y: 1, x: 2 }, a: { z: 3, w: 4 } };
|
|
27
|
+
expect(stableStringify(input)).toBe('{"a":{"w":4,"z":3},"b":{"x":2,"y":1}}');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("arrays preserve order", () => {
|
|
31
|
+
const input = [3, 1, 2];
|
|
32
|
+
expect(stableStringify(input)).toBe("[3,1,2]");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("arrays of objects sort keys per element", () => {
|
|
36
|
+
const input = [
|
|
37
|
+
{ b: 1, a: 2 },
|
|
38
|
+
{ d: 3, c: 4 },
|
|
39
|
+
];
|
|
40
|
+
expect(stableStringify(input)).toBe('[{"a":2,"b":1},{"c":4,"d":3}]');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("identical objects with different key insertion order produce same string", () => {
|
|
44
|
+
const a = { foo: 1, bar: 2 };
|
|
45
|
+
const b = { bar: 2, foo: 1 };
|
|
46
|
+
expect(stableStringify(a)).toBe(stableStringify(b));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("circular references render as marker, do not throw", () => {
|
|
50
|
+
const a: Record<string, unknown> = { x: 1 };
|
|
51
|
+
a.self = a;
|
|
52
|
+
expect(stableStringify(a)).toBe('{"self":"[Circular]","x":1}');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("mixed cycle through array does not crash", () => {
|
|
56
|
+
const arr: unknown[] = [];
|
|
57
|
+
arr.push(arr);
|
|
58
|
+
expect(stableStringify(arr)).toBe('["[Circular]"]');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("empty object and array", () => {
|
|
62
|
+
expect(stableStringify({})).toBe("{}");
|
|
63
|
+
expect(stableStringify([])).toBe("[]");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("special string characters JSON-escaped in keys", () => {
|
|
67
|
+
const input = { 'with "quotes"': 1 };
|
|
68
|
+
expect(stableStringify(input)).toBe('{"with \\"quotes\\"":1}');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("Unicode key sort by code-point, not by collation", () => {
|
|
72
|
+
// 'ä' (U+00E4) sorts AFTER 'z' (U+007A) by code-point.
|
|
73
|
+
// localeCompare in many locales would put 'ä' near 'a'.
|
|
74
|
+
const input = { z: 1, ä: 2 };
|
|
75
|
+
const result = stableStringify(input);
|
|
76
|
+
expect(result).toBe('{"z":1,"ä":2}');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("deterministic across multiple calls", () => {
|
|
80
|
+
const input = { c: 3, a: 1, b: 2 };
|
|
81
|
+
const first = stableStringify(input);
|
|
82
|
+
const second = stableStringify(input);
|
|
83
|
+
const third = stableStringify(input);
|
|
84
|
+
expect(first).toBe(second);
|
|
85
|
+
expect(second).toBe(third);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local deterministic JSON serialization for JSON-like plain
|
|
3
|
+
* objects. Keys are sorted by code-point order (NOT locale-sensitive).
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* - Stable for plain objects, arrays, primitives, and `null`.
|
|
7
|
+
* - `undefined` serialized as the string "undefined".
|
|
8
|
+
* - Circular references serialized as the string `"[Circular]"`.
|
|
9
|
+
* - **NOT** a canonical cross-runtime / cross-locale JSON serializer.
|
|
10
|
+
* Two different runtimes that disagree on `JSON.stringify` of primitives
|
|
11
|
+
* (none known today) would produce different output.
|
|
12
|
+
*
|
|
13
|
+
* Used for:
|
|
14
|
+
* - `tool_definition_measurements` fingerprint hashing
|
|
15
|
+
* - `pending_compaction_marker_state` CAS comparison
|
|
16
|
+
*
|
|
17
|
+
* If a future use case needs true canonical JSON (e.g. cross-process
|
|
18
|
+
* signing), build a separate utility — do NOT widen this contract.
|
|
19
|
+
*/
|
|
20
|
+
export function stableStringify(value: unknown, seen = new WeakSet<object>()): string {
|
|
21
|
+
if (value === undefined) return "undefined";
|
|
22
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? String(value);
|
|
23
|
+
if (seen.has(value)) return '"[Circular]"';
|
|
24
|
+
seen.add(value);
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
|
|
27
|
+
}
|
|
28
|
+
// Code-point sort (NOT localeCompare). Stable across runtimes/locales.
|
|
29
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => {
|
|
30
|
+
if (a < b) return -1;
|
|
31
|
+
if (a > b) return 1;
|
|
32
|
+
return 0;
|
|
33
|
+
});
|
|
34
|
+
return `{${entries
|
|
35
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child, seen)}`)
|
|
36
|
+
.join(",")}}`;
|
|
37
|
+
}
|
package/src/shared/transcript.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* 3. Lets the shared transform code (tagging, stripping, drops)
|
|
23
23
|
* operate on `TranscriptPart` interface instances without caring
|
|
24
24
|
* whether they're wrapping `Part` from `@opencode-ai/sdk` or
|
|
25
|
-
* `TextContent | ToolCall | ThinkingContent` from `@
|
|
25
|
+
* `TextContent | ToolCall | ThinkingContent` from `@earendil-works/pi-ai`.
|
|
26
26
|
*
|
|
27
27
|
* What this interface deliberately does NOT do:
|
|
28
28
|
*
|
|
@@ -181,7 +181,7 @@ export interface TranscriptMessage {
|
|
|
181
181
|
* messages-transform.ts, Pi's context-event handler). The shared
|
|
182
182
|
* transform code receives a Transcript and operates only through this
|
|
183
183
|
* interface — it never imports from `@opencode-ai/sdk` or
|
|
184
|
-
* `@
|
|
184
|
+
* `@earendil-works/pi-ai`.
|
|
185
185
|
*/
|
|
186
186
|
export interface Transcript {
|
|
187
187
|
/** Ordered messages in the current pass. */
|
|
@@ -10,6 +10,7 @@ import type { RpcNotificationMessage, SidebarSnapshot, StatusDetail } from "../.
|
|
|
10
10
|
export type { SidebarSnapshot, StatusDetail };
|
|
11
11
|
|
|
12
12
|
let rpcClient: MagicContextRpcClient | null = null;
|
|
13
|
+
let lastReceivedNotificationId = 0;
|
|
13
14
|
|
|
14
15
|
function getStorageDir(): string {
|
|
15
16
|
// Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
|
|
@@ -30,6 +31,7 @@ export function initRpcClient(directory: string): void {
|
|
|
30
31
|
export function closeRpc(): void {
|
|
31
32
|
rpcClient?.reset();
|
|
32
33
|
rpcClient = null;
|
|
34
|
+
lastReceivedNotificationId = 0;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const EMPTY_SNAPSHOT: SidebarSnapshot = {
|
|
@@ -103,9 +105,19 @@ function recallSidebarSnapshot(sessionId: string, fallback: SidebarSnapshot): Si
|
|
|
103
105
|
stickySidebarCache.delete(sessionId);
|
|
104
106
|
return fallback;
|
|
105
107
|
}
|
|
108
|
+
if (!hasInFlightEvidence(fallback)) {
|
|
109
|
+
stickySidebarCache.delete(sessionId);
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
106
112
|
return cached.snapshot;
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
function hasInFlightEvidence(snapshot: SidebarSnapshot): boolean {
|
|
116
|
+
return (
|
|
117
|
+
snapshot.compartmentInProgress || snapshot.historianRunning || snapshot.pendingOpsCount > 0
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
/** Fetch sidebar snapshot from the server via RPC. */
|
|
110
122
|
export async function loadSidebarSnapshot(
|
|
111
123
|
sessionId: string,
|
|
@@ -225,8 +237,15 @@ export async function consumeTuiMessages(): Promise<TuiMessage[]> {
|
|
|
225
237
|
try {
|
|
226
238
|
const result = await rpcClient.call<{ messages: RpcNotificationMessage[] }>(
|
|
227
239
|
"pending-notifications",
|
|
240
|
+
{ lastReceivedId: lastReceivedNotificationId },
|
|
228
241
|
);
|
|
229
|
-
|
|
242
|
+
const messages = result.messages ?? [];
|
|
243
|
+
for (const message of messages) {
|
|
244
|
+
if (message.id > lastReceivedNotificationId) {
|
|
245
|
+
lastReceivedNotificationId = message.id;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return messages.map((m) => ({
|
|
230
249
|
type: m.type,
|
|
231
250
|
payload: m.payload,
|
|
232
251
|
sessionId: m.sessionId,
|