zigmini 0.9.0-beta.1 → 0.9.0-beta.3
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.ko.md +23 -4
- package/README.md +23 -4
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +7 -0
- package/docs/manual.md +7 -0
- package/host/omo-bridge/ascii-json.ts +5 -0
- package/host/omo-bridge/bridge.ts +50 -44
- package/host/omo-bridge/model-control.ts +15 -3
- package/host/omo-check/patches/omo-5.1.48-compaction-port.patch +2629 -0
- package/host/omo-check/patches/omo-5.1.48-compaction-recovery.patch +912 -0
- package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +356 -0
- package/host/omo-check/patches/omo-5.1.48-provider-port.patch +440 -0
- package/host/omo-check/patches/omo-5.1.48-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.48-runtime-compat.patch +31 -0
- package/package.json +1 -1
- package/src/cli/setup.ts +6 -2
- package/src/engine/compat.json +4 -4
- package/src/engine/install.ts +68 -20
- package/src/engine/rpc-socket.ts +36 -0
- package/src/engine/schema-guard.ts +34 -2
- package/src/hub.ts +195 -15
- package/src/omo/client.ts +18 -3
- package/src/omo/frame-socket.ts +330 -0
- package/src/update/handoff-system.ts +22 -0
- package/src/update/handoff.ts +10 -2
- package/src/update/helper.ts +11 -1
- package/src/update/request.ts +15 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
2
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
3
|
+
@@ -124,6 +124,7 @@
|
|
4
|
+
headers,
|
|
5
|
+
extraBody: options.auth.extraBody,
|
|
6
|
+
env: options.auth.env,
|
|
7
|
+
+ cwd: options.context.cwd,
|
|
8
|
+
streamKind: "auxiliary",
|
|
9
|
+
onPayload: async (payload, model) => {
|
|
10
|
+
const sanitized = model.api === "anthropic-messages" ? sanitizeAnthropicPayload(payload) : payload;
|
|
11
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/multi-session-host.js
|
|
12
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/multi-session-host.js
|
|
13
|
+
@@ -227,7 +227,7 @@
|
|
14
|
+
onHandoffParked: async (ids) => {
|
|
15
|
+
await Promise.all(ids.map(async (id) => {
|
|
16
|
+
await writer.flushConnection(id);
|
|
17
|
+
- connections.get(id)?.close();
|
|
18
|
+
+ await connections.get(id)?.finish();
|
|
19
|
+
}));
|
|
20
|
+
},
|
|
21
|
+
}, hostContext, {
|
|
22
|
+
@@ -342,7 +342,8 @@
|
|
23
|
+
process.stderr.write(`senpi rpc connection ${id} release failed: ${errorMessage(cause)}\n`);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
- connections.set(id, { id, sink, detach, close: () => socket.destroy() });
|
|
27
|
+
+ connections.set(id, { id, sink, detach, close: () => socket.destroy(),
|
|
28
|
+
+ finish: () => new Promise((resolve) => socket.end(resolve)) });
|
|
29
|
+
socket.once("close", detach);
|
|
30
|
+
socket.once("error", () => detach());
|
|
31
|
+
};
|
package/package.json
CHANGED
package/src/cli/setup.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface SetupDeps {
|
|
|
49
49
|
readonly env: Env;
|
|
50
50
|
readonly paths: RuntimePaths;
|
|
51
51
|
readonly bunPath: string;
|
|
52
|
-
readonly installEngine: (options: { home: string; acceptLicense: boolean; engineOnly: boolean; confirmLicense?: (summary: string, path: string) => Promise<boolean> }) => Promise<string>;
|
|
52
|
+
readonly installEngine: (options: { home: string; agentDir: string; acceptLicense: boolean; engineOnly: boolean; confirmLicense?: (summary: string, path: string) => Promise<boolean> }) => Promise<string>;
|
|
53
53
|
readonly globalOmoVersion: () => Promise<string | null>;
|
|
54
54
|
readonly installGlobalOmo: (version: string) => Promise<void>;
|
|
55
55
|
/** The executable checked or installed above; injected installers may keep the plain omo command. */
|
|
@@ -221,7 +221,11 @@ async function engineStep(options: SetupOptions, deps: SetupDeps): Promise<numbe
|
|
|
221
221
|
return answer?.trim().toLowerCase() === "y";
|
|
222
222
|
};
|
|
223
223
|
try {
|
|
224
|
-
|
|
224
|
+
// Findings 214-217: the candidate is checked against the store of the agent folder the bridge will use.
|
|
225
|
+
const configFile = join(deps.paths.home, "config.env");
|
|
226
|
+
const dedicated = options.agentDir ?? (existsSync(configFile) ? parseConfigEnv(readFileSync(configFile, "utf8")).ZGW_OMO_AGENT_DIR : undefined);
|
|
227
|
+
const agentDir = dedicated ? resolve(dedicated) : defaultAgentDir(deps.env);
|
|
228
|
+
const target = await deps.installEngine({ home: deps.paths.home, agentDir, acceptLicense: options.acceptLicense, engineOnly: options.engineOnly, confirmLicense });
|
|
225
229
|
deps.out(message(lang, "engineDone", { target }));
|
|
226
230
|
return 0;
|
|
227
231
|
} catch (error) {
|
package/src/engine/compat.json
CHANGED
package/src/engine/install.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import type { Socket } from "node:net";
|
|
4
|
+
import { connectEngineSocket } from "./rpc-socket.ts";
|
|
4
5
|
import type { ServerWebSocket } from "bun";
|
|
5
6
|
import { OmoClient } from "../omo/client.ts";
|
|
6
7
|
import { applyPatch, patchPath } from "./apply-patch.ts";
|
|
7
8
|
import { engineBuildId, engineRevision } from "./build-id.ts";
|
|
9
|
+
import { readStoreSchema, storeIncompatibility } from "./schema-guard.ts";
|
|
10
|
+
import { supportedSchema } from "./thread-schema.ts";
|
|
8
11
|
|
|
9
12
|
const PATCHES = new URL("../../host/omo-check/patches/", import.meta.url).pathname;
|
|
10
13
|
const LICENSE = "Sustainable Use License 1.0";
|
|
@@ -219,34 +222,54 @@ export async function smokeEngine(home: string, candidate: string, options: { re
|
|
|
219
222
|
void proc.exited.then((code) => ready.reject(new Error(`engine_smoke_exit:${code}`)));
|
|
220
223
|
let server: ReturnType<typeof Bun.serve> | undefined;
|
|
221
224
|
let client: OmoClient | undefined;
|
|
222
|
-
const sockets = new Set<
|
|
225
|
+
const sockets = new Set<Socket>();
|
|
226
|
+
const handshake = Promise.withResolvers<void>();
|
|
223
227
|
try {
|
|
224
228
|
await bounded(ready.promise, timeout);
|
|
225
229
|
server = Bun.serve({
|
|
226
230
|
hostname: "127.0.0.1", port: options.port ?? 0,
|
|
227
|
-
fetch: (req, srv) => srv.upgrade(req, { data: {} }) ? undefined : new Response("", { status: 400 }),
|
|
231
|
+
fetch: (req, srv) => srv.upgrade(req, { data: { queue: [], closed: false } }) ? undefined : new Response("", { status: 400 }),
|
|
228
232
|
websocket: {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let newline: number;
|
|
237
|
-
while ((newline = pending.indexOf("\n")) !== -1) {
|
|
238
|
-
ws.send(pending.slice(0, newline));
|
|
239
|
-
pending = pending.slice(newline + 1);
|
|
233
|
+
// The engine's own authenticated connection (socket_auth, key root = the smoke agent folder). Frames the client
|
|
234
|
+
// sends while authentication runs are queued in order; a failed authentication fails the smoke, never falls back.
|
|
235
|
+
open(ws: ServerWebSocket<{ upstream?: Socket; queue: string[]; closed: boolean }>) {
|
|
236
|
+
void connectEngineSocket({ socketPath: socket, binary: join(candidate, "node_modules/.bin/omo"), agentDir: join(scratch, "agent") }).then((upstream) => {
|
|
237
|
+
if (ws.data.closed) {
|
|
238
|
+
upstream.destroy();
|
|
239
|
+
return;
|
|
240
240
|
}
|
|
241
|
+
sockets.add(upstream);
|
|
242
|
+
ws.data.upstream = upstream;
|
|
243
|
+
relay(ws, upstream);
|
|
244
|
+
for (const frame of ws.data.queue.splice(0)) upstream.write(`${frame}\n`);
|
|
245
|
+
}, (error: unknown) => {
|
|
246
|
+
handshake.reject(error instanceof Error ? error : new Error(String(error)));
|
|
247
|
+
ws.close();
|
|
241
248
|
});
|
|
242
|
-
upstream.once("error", () => ws.close());
|
|
243
|
-
upstream.once("close", () => { sockets.delete(upstream); ws.close(); });
|
|
244
249
|
},
|
|
245
|
-
message(ws, message) {
|
|
246
|
-
|
|
250
|
+
message(ws, message) {
|
|
251
|
+
if (ws.data.upstream) ws.data.upstream.write(`${message}\n`);
|
|
252
|
+
else ws.data.queue.push(String(message));
|
|
253
|
+
},
|
|
254
|
+
close(ws) {
|
|
255
|
+
ws.data.closed = true;
|
|
256
|
+
ws.data.upstream?.destroy();
|
|
257
|
+
},
|
|
247
258
|
},
|
|
248
259
|
});
|
|
249
|
-
const
|
|
260
|
+
const relay = (ws: ServerWebSocket<{ upstream?: Socket; queue: string[]; closed: boolean }>, upstream: Socket) => {
|
|
261
|
+
let pending = "";
|
|
262
|
+
upstream.on("data", (chunk) => {
|
|
263
|
+
pending += chunk.toString();
|
|
264
|
+
let newline: number;
|
|
265
|
+
while ((newline = pending.indexOf("\n")) !== -1) {
|
|
266
|
+
ws.send(pending.slice(0, newline));
|
|
267
|
+
pending = pending.slice(newline + 1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
upstream.once("error", () => ws.close());
|
|
271
|
+
upstream.once("close", () => { sockets.delete(upstream); ws.close(); });
|
|
272
|
+
};
|
|
250
273
|
client = new OmoClient(`ws://127.0.0.1:${server.port}`, "", {
|
|
251
274
|
onEvent() {},
|
|
252
275
|
onStatus(status) {
|
|
@@ -271,8 +294,27 @@ export async function smokeEngine(home: string, candidate: string, options: { re
|
|
|
271
294
|
}
|
|
272
295
|
}
|
|
273
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Findings 214-217: the candidate must read the agent folder's shared store before it is selected. The store is read
|
|
299
|
+
* through the guard's temp copy; an unknown support value or an unreadable store is not a known incompatibility.
|
|
300
|
+
*/
|
|
301
|
+
export function assertStoreCompatible(agentDir: string, candidate: string): void {
|
|
302
|
+
let supported: number | null = null;
|
|
303
|
+
try {
|
|
304
|
+
supported = supportedSchema(candidate);
|
|
305
|
+
} catch {
|
|
306
|
+
supported = null;
|
|
307
|
+
}
|
|
308
|
+
const found = storeIncompatibility(readStoreSchema(agentDir), supported);
|
|
309
|
+
if (found !== null) throw new Error(`${ENGINE_SCHEMA_INCOMPATIBLE}:store=${found.store},supported=${found.supported}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const ENGINE_SCHEMA_INCOMPATIBLE = "engine_schema_incompatible";
|
|
313
|
+
|
|
274
314
|
export async function installEngine(options: {
|
|
275
315
|
readonly home: string;
|
|
316
|
+
/** The agent folder the bridge uses; when set, a candidate that cannot read its store is never selected. */
|
|
317
|
+
readonly agentDir?: string;
|
|
276
318
|
readonly version?: string;
|
|
277
319
|
readonly patchDir?: string;
|
|
278
320
|
readonly registry?: string;
|
|
@@ -281,11 +323,17 @@ export async function installEngine(options: {
|
|
|
281
323
|
readonly confirmLicense?: (summary: string, path: string) => Promise<boolean>;
|
|
282
324
|
readonly smoke?: { readonly port?: number; readonly timeoutMs?: number };
|
|
283
325
|
}): Promise<string> {
|
|
284
|
-
const
|
|
326
|
+
const pin: { readonly omo: string; readonly threadSchema?: number } = JSON.parse(readFileSync(new URL("./compat.json", import.meta.url), "utf8"));
|
|
327
|
+
const version = options.version ?? pin.omo;
|
|
328
|
+
if (options.agentDir !== undefined && version === pin.omo) {
|
|
329
|
+
const found = storeIncompatibility(readStoreSchema(options.agentDir), pin.threadSchema ?? null);
|
|
330
|
+
if (found !== null) throw new Error(`${ENGINE_SCHEMA_INCOMPATIBLE}:store=${found.store},supported=${found.supported}`);
|
|
331
|
+
}
|
|
285
332
|
const omoDir = join(resolve(options.home), "omo");
|
|
286
333
|
await installStock(version, omoDir, options.registry);
|
|
287
334
|
const target = buildPatched(version, omoDir, options.patchDir);
|
|
288
335
|
const candidate = join(omoDir, target);
|
|
336
|
+
if (options.agentDir !== undefined) assertStoreCompatible(options.agentDir, candidate);
|
|
289
337
|
await acceptEngineLicense(options.home, candidate, options);
|
|
290
338
|
await smokeEngine(options.home, candidate, options.smoke);
|
|
291
339
|
candidateExecutable(candidate);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { createConnection, Socket } from "node:net";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
export interface EngineSocketOptions {
|
|
7
|
+
readonly socketPath: string;
|
|
8
|
+
readonly binary: string;
|
|
9
|
+
readonly agentDir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Use the selected engine's key-record validation and challenge-response implementation. */
|
|
13
|
+
export async function connectEngineSocket(options: EngineSocketOptions): Promise<Socket> {
|
|
14
|
+
const modules = dirname(dirname(dirname(realpathSync(options.binary))));
|
|
15
|
+
const authPath = join(modules, "@code-yeongyu/senpi/dist/modes/rpc/rpc-auth-client.js");
|
|
16
|
+
if (existsSync(authPath)) {
|
|
17
|
+
const auth: { readonly connectAuthenticatedRpcSocket?: unknown } = await import(pathToFileURL(authPath).href);
|
|
18
|
+
if (typeof auth.connectAuthenticatedRpcSocket !== "function") {
|
|
19
|
+
throw new Error("engine_rpc_auth_module_invalid");
|
|
20
|
+
}
|
|
21
|
+
const result: unknown = await auth.connectAuthenticatedRpcSocket(options.socketPath, undefined, { keyRoot: options.agentDir });
|
|
22
|
+
if (typeof result !== "object" || result === null || !("socket" in result) || !(result.socket instanceof Socket)) {
|
|
23
|
+
throw new Error("engine_rpc_auth_socket_invalid");
|
|
24
|
+
}
|
|
25
|
+
return result.socket;
|
|
26
|
+
}
|
|
27
|
+
// Retained pre-auth engines and the native transport fixture have no authentication module.
|
|
28
|
+
return await new Promise<Socket>((resolve, reject) => {
|
|
29
|
+
const socket = createConnection(options.socketPath);
|
|
30
|
+
socket.once("error", reject);
|
|
31
|
+
socket.once("connect", () => {
|
|
32
|
+
socket.off("error", reject);
|
|
33
|
+
resolve(socket);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -38,6 +38,15 @@ export interface SchemaCheck {
|
|
|
38
38
|
export interface SavedGuard extends SchemaCheck {
|
|
39
39
|
readonly mode: GuardMode;
|
|
40
40
|
readonly at: string;
|
|
41
|
+
/** The refusing bridge's runtime slot and pinned engine folder; null for a bridge that did not say (findings 214-217). */
|
|
42
|
+
readonly slot?: string | null;
|
|
43
|
+
readonly engine?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Which bridge start ran the guard: a slot bridge passes its slot and pinned engine folder. */
|
|
47
|
+
export interface GuardOrigin {
|
|
48
|
+
readonly slot: string | null;
|
|
49
|
+
readonly engine: string | null;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export interface GuardInput {
|
|
@@ -48,6 +57,7 @@ export interface GuardInput {
|
|
|
48
57
|
readonly supported: () => number;
|
|
49
58
|
readonly bunInstall: string;
|
|
50
59
|
readonly now: () => Date;
|
|
60
|
+
readonly origin?: GuardOrigin;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
export interface GuardResult {
|
|
@@ -116,11 +126,23 @@ export function compareVersions(a: string, b: string): number {
|
|
|
116
126
|
return preA < preB ? -1 : 1;
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
/** Findings 214-217: the installed global version is an observation, not the proven actor of the schema change. */
|
|
119
130
|
export function tooNewMessage(schema: number, globalVersion: string | null): string {
|
|
120
|
-
return
|
|
131
|
+
return `공유 폴더 스키마가 ${schema}이라 이 게이트웨이 엔진이 읽지 못합니다(지금 설치된 전역 OMO: ${globalVersion ?? "버전 알 수 없음"}, 스키마를 올린 주체는 확인하지 않음). ` +
|
|
121
132
|
"`zigmini update`로 맞는 버전을 받거나, 이 게이트웨이 전용 에이전트 폴더를 쓰세요(`zigmini setup --agent-dir`)";
|
|
122
133
|
}
|
|
123
134
|
|
|
135
|
+
export const SCHEMA_INCOMPATIBLE = "schema_incompatible";
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Findings 214-217: a known incompatibility is a readable store newer than what the engine supports. A missing or
|
|
139
|
+
* unreadable store, or an engine whose support is unknown, is not known to be incompatible.
|
|
140
|
+
*/
|
|
141
|
+
export function storeIncompatibility(store: StoreReading, supported: number | null): { readonly store: number; readonly supported: number } | null {
|
|
142
|
+
if (store.state !== "ok" || supported === null || store.version <= supported) return null;
|
|
143
|
+
return { store: store.version, supported };
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
export function checkSchema(store: StoreReading, supported: number | null, globalVersion: string | null): SchemaCheck {
|
|
125
147
|
const version = store.state === "ok" ? store.version : null;
|
|
126
148
|
if (store.state === "missing") return { state: "missing", store: null, supported, globalVersion, message: null };
|
|
@@ -147,7 +169,7 @@ export function guardStart(input: GuardInput): GuardResult {
|
|
|
147
169
|
const file = join(input.run, GUARD_FILE);
|
|
148
170
|
if (refuse) {
|
|
149
171
|
mkdirSync(input.run, { recursive: true, mode: 0o700 });
|
|
150
|
-
const saved: SavedGuard = { ...check, mode: input.mode, at: input.now().toISOString() };
|
|
172
|
+
const saved: SavedGuard = { ...check, mode: input.mode, at: input.now().toISOString(), slot: input.origin?.slot ?? null, engine: input.origin?.engine ?? null };
|
|
151
173
|
writeFileSync(file, `${JSON.stringify(saved)}\n`, { mode: 0o600 });
|
|
152
174
|
} else rmSync(file, { force: true });
|
|
153
175
|
if (check.state === "too_new") {
|
|
@@ -170,6 +192,16 @@ export function readGuardState(run: string): SavedGuard | null {
|
|
|
170
192
|
}
|
|
171
193
|
}
|
|
172
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Findings 214-217: the refusal of this start attempt only. The shared run/schema-guard.json may be left by another slot,
|
|
197
|
+
* engine or an earlier start, which must not fail (or explain) the current one.
|
|
198
|
+
*/
|
|
199
|
+
export function attemptRefusal(saved: SavedGuard | null, attempt: { readonly slot: string; readonly engine: string | null; readonly since: string }): string | null {
|
|
200
|
+
if (saved === null || saved.mode !== "enforce" || saved.slot !== attempt.slot || saved.engine !== attempt.engine) return null;
|
|
201
|
+
if (!(Date.parse(saved.at) >= Date.parse(attempt.since))) return null;
|
|
202
|
+
return `저장소 스키마 ${saved.store} > 엔진 ${attempt.engine ?? "current"} 지원 ${saved.supported}로 브리지가 시작을 거부했습니다. ${saved.message}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
/** `/api/health` `omo`: a connected bridge wins over a stale file; otherwise an enforced refusal shows as schema_too_new. */
|
|
174
206
|
export function healthOmo(status: string, saved: SavedGuard | null): string {
|
|
175
207
|
return status !== "ready" && saved?.mode === "enforce" ? SCHEMA_TOO_NEW : status;
|