@kasenri/dsh-orbit 0.5.6 → 0.5.8
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 +24 -19
- package/cordis.patch.yml +2 -2
- package/lib/activation.js +9 -9
- package/lib/capabilities.js +23 -0
- package/lib/client.js +49 -79
- package/lib/decisions.js +4 -4
- package/lib/dsh-host.js +108 -30
- package/lib/evidence.js +24 -0
- package/lib/guard.js +17 -17
- package/lib/index.js +44 -42
- package/lib/kernel.js +33 -21
- package/lib/pipeline-guard.js +16 -4
- package/lib/routes.js +67 -37
- package/lib/sanitize.js +3 -2
- package/lib/service.js +75 -15
- package/lib/settlement.js +4 -1
- package/lib/state-store.js +15 -7
- package/lib/supervisor.js +197 -91
- package/lib/tool.js +10 -11
- package/lib/types.js +6 -5
- package/package.json +1 -1
package/lib/pipeline-guard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { guardBashCommand, guardReason, guardToolPath } from "./guard.js";
|
|
2
|
+
import { mutationTools } from "./capabilities.js";
|
|
2
3
|
const WRITE_PATH_TOOLS = new Set(['write', 'edit', 'str_replace_editor']);
|
|
3
4
|
/** Top-level autonomous mutation drivers that must not run beside an active Orbit run. */
|
|
4
5
|
export const MUTATION_DRIVER_TOOLS = new Set(['create_goal', 'ralph', 'workflow']);
|
|
@@ -16,12 +17,13 @@ export function createOrbitPreExecuteHandler(service, options = {}) {
|
|
|
16
17
|
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
17
18
|
const active = service.hasActiveRun(cwd);
|
|
18
19
|
const args = (exec.arguments ?? {});
|
|
20
|
+
const mutating = mutationTools(service.browserToolNames()).has(exec.name);
|
|
19
21
|
// Orbit owns the workspace: refuse to start another top-level mutation driver.
|
|
20
22
|
if (active && MUTATION_DRIVER_TOOLS.has(exec.name)) {
|
|
21
23
|
return {
|
|
22
24
|
kind: 'deny',
|
|
23
|
-
reason: `ORBIT_MUTATION_DRIVER_CONFLICT:
|
|
24
|
-
'
|
|
25
|
+
reason: `ORBIT_MUTATION_DRIVER_CONFLICT: 当前 workspace 由 Orbit 持有,不能启动 ${exec.name}。` +
|
|
26
|
+
'请先 resume 或 stop 当前 Run,或通过 orbit_controller 继续。',
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
// Another driver already owns the workspace: refuse to start Orbit.
|
|
@@ -32,13 +34,23 @@ export function createOrbitPreExecuteHandler(service, options = {}) {
|
|
|
32
34
|
if (competing) {
|
|
33
35
|
return {
|
|
34
36
|
kind: 'deny',
|
|
35
|
-
reason: `ORBIT_MUTATION_DRIVER_CONFLICT: ${competing}
|
|
37
|
+
reason: `ORBIT_MUTATION_DRIVER_CONFLICT: ${competing} 已持有此 workspace 的修改权,请先停止该 Driver。`,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
if (!active)
|
|
41
43
|
return next();
|
|
44
|
+
// Runtime ownership fence: only the current Orbit Executor child may use
|
|
45
|
+
// mutation-capable tools in an Orbit-owned workspace.
|
|
46
|
+
if (mutating && !service.isMutationAuthorized(exec.agent, exec.name, cwd)) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'deny',
|
|
49
|
+
reason: `ORBIT_MUTATION_DRIVER_CONFLICT: 当前 workspace 由 Orbit 持有;只有当前 Orbit Executor 可以调用 ${exec.name}。`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (options.contentGuards === false)
|
|
53
|
+
return next();
|
|
42
54
|
if (exec.name === 'bash') {
|
|
43
55
|
const command = typeof args['command'] === 'string' ? args['command'] : '';
|
|
44
56
|
const decision = guardBashCommand(command, { github_allowed: service.githubAllowed(cwd) });
|
|
@@ -59,7 +71,7 @@ export function createOrbitPreExecuteHandler(service, options = {}) {
|
|
|
59
71
|
}
|
|
60
72
|
// The browser tool can write its structured result to a caller path; route
|
|
61
73
|
// that path through the same durable-state policy instead of a second copy.
|
|
62
|
-
if (exec.name
|
|
74
|
+
if (service.browserToolNames().includes(exec.name)) {
|
|
63
75
|
const outputPath = args['outputPath'];
|
|
64
76
|
if (typeof outputPath === 'string' && outputPath.length > 0) {
|
|
65
77
|
const decision = guardToolPath('write', outputPath, { cwd });
|
package/lib/routes.js
CHANGED
|
@@ -1,52 +1,82 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
/** Effective route resolution for a new Orbit run. */
|
|
2
|
+
export function projectionRegistryOf(ctx) {
|
|
3
|
+
const reflect = ctx?.reflect;
|
|
4
|
+
const registry = reflect?.get('sessionProjections');
|
|
5
|
+
return registry !== null && typeof registry === 'object' ? registry : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function sessionModelStateOf(ctx, session) {
|
|
8
|
+
const state = projectionRegistryOf(ctx)?.stateOf?.(session, 'modelSelection');
|
|
9
|
+
return state !== null && typeof state === 'object' ? state : undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Read-only access to DSH's deployment model selection. */
|
|
12
|
+
export function agentDefaultSelectionOf(ctx) {
|
|
13
|
+
const reflect = ctx?.reflect;
|
|
14
|
+
const service = reflect?.get('agentDefaultModel');
|
|
15
|
+
if (service?.currentSelection === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const selection = service.currentSelection();
|
|
19
|
+
return selection !== null && typeof selection === 'object' ? selection : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** DSH selection intent: pending choice, last request, then deployment default. */
|
|
26
|
+
export function sessionModelSelectionOf(sources) {
|
|
27
|
+
const pending = sources.sessionModel?.pending;
|
|
28
|
+
if (pending !== undefined && pending !== null)
|
|
29
|
+
return pending;
|
|
30
|
+
const header = sources.requestHeader;
|
|
31
|
+
if (header?.config !== undefined) {
|
|
32
|
+
const effort = header.config.reasoningEffort;
|
|
33
|
+
const adapterDefaultEffort = header.adapterDefaults?.reasoningEffort === true;
|
|
34
|
+
return {
|
|
35
|
+
provider: header.config.provider,
|
|
36
|
+
model: header.config.model,
|
|
37
|
+
...(effort === undefined || effort === '' || adapterDefaultEffort ? {} : { reasoningEffort: effort }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return sources.agentDefault;
|
|
41
|
+
}
|
|
42
|
+
/** Normalize a user-owned selection; whitespace and malformed effort are invalid. */
|
|
16
43
|
export function routeFromSelection(selection) {
|
|
17
44
|
if (selection === undefined)
|
|
18
45
|
return undefined;
|
|
19
|
-
const
|
|
20
|
-
|
|
46
|
+
const provider = typeof selection.provider === 'string' ? selection.provider.trim() : '';
|
|
47
|
+
const model = typeof selection.model === 'string' ? selection.model.trim() : '';
|
|
48
|
+
if (provider === '' || model === '')
|
|
21
49
|
return undefined;
|
|
22
|
-
|
|
50
|
+
const effort = selection.reasoningEffort;
|
|
51
|
+
if (effort !== undefined && typeof effort !== 'string')
|
|
23
52
|
return undefined;
|
|
24
53
|
return {
|
|
25
54
|
provider,
|
|
26
55
|
model,
|
|
27
|
-
...(typeof
|
|
56
|
+
...(typeof effort === 'string' && effort.trim() !== '' ? { reasoningEffort: effort.trim() } : {}),
|
|
28
57
|
};
|
|
29
58
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
59
|
+
const ROLE_LABELS = {
|
|
60
|
+
commander: 'Commander',
|
|
61
|
+
executor: 'Executor',
|
|
62
|
+
watchdog: 'Watchdog',
|
|
63
|
+
};
|
|
64
|
+
/** Resolve all three routes or fail before a durable run is created. */
|
|
37
65
|
export function resolveEffectiveRoutes(input) {
|
|
38
|
-
|
|
39
|
-
commander: routeFromSelection(input.settings?.commander) ?? input.configRoutes
|
|
40
|
-
executor:
|
|
41
|
-
|
|
66
|
+
const routes = {
|
|
67
|
+
commander: routeFromSelection(input.settings?.commander) ?? routeFromSelection(input.configRoutes?.commander),
|
|
68
|
+
executor: input.hasSession === true || input.sessionSelection !== undefined
|
|
69
|
+
? routeFromSelection(input.sessionSelection)
|
|
70
|
+
: routeFromSelection(input.configRoutes?.executor),
|
|
71
|
+
watchdog: routeFromSelection(input.settings?.watchdog) ?? routeFromSelection(input.configRoutes?.watchdog),
|
|
42
72
|
};
|
|
73
|
+
const missing = ['commander', 'executor', 'watchdog'].filter((role) => routes[role] === undefined);
|
|
74
|
+
if (missing.length > 0) {
|
|
75
|
+
throw new Error(`ORBIT_ROLE_MODEL_CONFIGURATION_REQUIRED: Orbit 尚未完成角色模型配置:${missing.map((role) => `${ROLE_LABELS[role]} 未选择`).join(';')}。` +
|
|
76
|
+
'请先在 Orbit 模型菜单中选择;无 Web 设置界面的 profile 可显式配置 routes。');
|
|
77
|
+
}
|
|
78
|
+
return structuredClone(routes);
|
|
43
79
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Read the initiating Session's current model selection from the public
|
|
46
|
-
* request-header seam (`Agent.session.requestHeader().config`).
|
|
47
|
-
* @param agent - initiating agent, or undefined outside a boundary.
|
|
48
|
-
* @returns the selection route, or undefined when no header exists.
|
|
49
|
-
*/
|
|
50
80
|
export function sessionSelectionOf(agent) {
|
|
51
|
-
return
|
|
81
|
+
return agent?.session?.requestHeader?.()?.config;
|
|
52
82
|
}
|
package/lib/sanitize.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Secret redaction shared by Orbit state, logs, and watchdog prompts. */
|
|
2
2
|
const SECRET_PATTERNS = [
|
|
3
3
|
/(authorization\s*:\s*(?:bearer\s+)?)[^\s,;]+/gi,
|
|
4
|
-
/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|cookie|secret|private[_-]?key)\s*[=:]\s*)[^\s,;]+/gi,
|
|
4
|
+
/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|cookie|secret|private[_-]?key)\s*[=:]\s*)[^\s,;]+/gi,
|
|
5
5
|
/(\b(?:sk|pk|ghp|github_pat)_[A-Za-z0-9_-]{8,})/g,
|
|
6
6
|
/(\bBearer\s+)[A-Za-z0-9._-]+/gi,
|
|
7
7
|
];
|
|
@@ -32,7 +32,8 @@ export function truncateSafe(value, max = 4000) {
|
|
|
32
32
|
const redacted = redactText(value);
|
|
33
33
|
if (redacted.length <= max)
|
|
34
34
|
return redacted;
|
|
35
|
-
|
|
35
|
+
const marker = '\n...[truncated]';
|
|
36
|
+
return `${redacted.slice(0, Math.max(0, max - marker.length))}${marker}`;
|
|
36
37
|
}
|
|
37
38
|
export function looksLikeSecretKey(key) {
|
|
38
39
|
return SENSITIVE_KEY.test(key);
|
package/lib/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { accessSync, constants,
|
|
1
|
+
import { accessSync, constants, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { Service } from '@deepseek-ai/cordis';
|
|
3
4
|
import { DshOrbitHost } from "./dsh-host.js";
|
|
4
5
|
import { OrbitStateStore } from "./state-store.js";
|
|
@@ -6,6 +7,7 @@ import { OrbitSupervisor } from "./supervisor.js";
|
|
|
6
7
|
export class OrbitService extends Service {
|
|
7
8
|
host;
|
|
8
9
|
config;
|
|
10
|
+
executions = new Map();
|
|
9
11
|
constructor(ctx, config) {
|
|
10
12
|
super(ctx, 'orbit');
|
|
11
13
|
this.host = new DshOrbitHost(ctx);
|
|
@@ -15,6 +17,7 @@ export class OrbitService extends Service {
|
|
|
15
17
|
return new OrbitSupervisor(new OrbitStateStore(projectDir), this.host, {
|
|
16
18
|
defaultRoutes: this.config.routes,
|
|
17
19
|
...(this.config.resolveRoutes ? { resolveRoutes: this.config.resolveRoutes } : {}),
|
|
20
|
+
...(this.config.resolveOwnerSessionId ? { resolveOwnerSessionId: this.config.resolveOwnerSessionId } : {}),
|
|
18
21
|
browserTools: this.config.browserTools,
|
|
19
22
|
commanderReadOnlyTools: this.config.commanderReadOnlyTools,
|
|
20
23
|
watchdogTools: this.config.watchdogTools,
|
|
@@ -23,21 +26,67 @@ export class OrbitService extends Service {
|
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
28
|
resolveProjectDir(projectDir) {
|
|
26
|
-
return projectDir ?? this.config.projectDir ?? process.cwd();
|
|
29
|
+
return resolve(projectDir ?? this.config.projectDir ?? process.cwd());
|
|
27
30
|
}
|
|
28
31
|
run(input, projectDir, signal) {
|
|
29
|
-
|
|
32
|
+
const dir = this.resolveProjectDir(projectDir);
|
|
33
|
+
return this.execute(dir, signal, (activeSignal) => this.supervisorFor(dir).bootstrap(input, activeSignal));
|
|
34
|
+
}
|
|
35
|
+
async execute(dir, signal, operation) {
|
|
36
|
+
if (this.executions.has(dir))
|
|
37
|
+
return { ok: false, action: 'run', message: 'ORBIT_MUTATION_DRIVER_CONFLICT: 此 workspace 已有 Orbit 执行中的调用。' };
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const abort = () => controller.abort();
|
|
40
|
+
if (signal?.aborted)
|
|
41
|
+
controller.abort();
|
|
42
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
43
|
+
const result = Promise.resolve().then(() => operation(controller.signal));
|
|
44
|
+
this.executions.set(dir, { controller, result });
|
|
45
|
+
try {
|
|
46
|
+
const settled = await result;
|
|
47
|
+
if (settled.phase === 'SUCCESS' || settled.phase === 'STOPPED' || settled.phase === 'BUDGET_EXHAUSTED' || settled.message === 'ORBIT_ABORTED') {
|
|
48
|
+
await this.host.revokeWorkspace(dir);
|
|
49
|
+
}
|
|
50
|
+
return settled;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await this.host.revokeWorkspace(dir);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
signal?.removeEventListener('abort', abort);
|
|
58
|
+
this.executions.delete(dir);
|
|
59
|
+
}
|
|
30
60
|
}
|
|
31
61
|
async resume(input, projectDir, signal) {
|
|
32
62
|
const dir = this.resolveProjectDir(projectDir);
|
|
33
63
|
const state = new OrbitStateStore(dir).readState();
|
|
34
64
|
if (!state)
|
|
35
|
-
return { ok: false, action: 'resume', message: 'ORBIT_RUN_NOT_FOUND:
|
|
65
|
+
return { ok: false, action: 'resume', message: 'ORBIT_RUN_NOT_FOUND: 没有可继续的持久化 Run。' };
|
|
36
66
|
const supervisor = this.supervisorFor(dir);
|
|
37
|
-
|
|
67
|
+
if (input.run_id && input.run_id !== state.run_id)
|
|
68
|
+
return { ok: false, action: 'resume', message: 'ORBIT_RUN_NOT_FOUND: Run id 不匹配。' };
|
|
69
|
+
if (state.phase === 'NEEDS_USER') {
|
|
70
|
+
if (state.owner_session_id !== undefined)
|
|
71
|
+
return this.run(input, dir, signal);
|
|
72
|
+
// Explicit resume is the only ownerless compatibility path; it never
|
|
73
|
+
// adopts the caller or stores its message as a user reply.
|
|
74
|
+
state.phase = state.plan.steps.length === 0 ? 'PLAN' : 'EXECUTE';
|
|
75
|
+
state.status = 'running';
|
|
76
|
+
}
|
|
77
|
+
return this.execute(dir, signal, (activeSignal) => supervisor.run(state, activeSignal));
|
|
38
78
|
}
|
|
39
|
-
stop(runId, projectDir) {
|
|
40
|
-
|
|
79
|
+
async stop(runId, projectDir) {
|
|
80
|
+
const dir = this.resolveProjectDir(projectDir);
|
|
81
|
+
const state = new OrbitStateStore(dir).readState();
|
|
82
|
+
if (runId && state?.run_id !== runId)
|
|
83
|
+
return { ok: false, action: 'stop', message: 'ORBIT_RUN_NOT_FOUND: Run id 不匹配。' };
|
|
84
|
+
const active = this.executions.get(dir);
|
|
85
|
+
active?.controller.abort();
|
|
86
|
+
if (active)
|
|
87
|
+
await active.result.catch(() => undefined);
|
|
88
|
+
await this.host.revokeWorkspace(dir);
|
|
89
|
+
return this.supervisorFor(dir).stop('stop', runId);
|
|
41
90
|
}
|
|
42
91
|
status(projectDir) {
|
|
43
92
|
return this.supervisorFor(this.resolveProjectDir(projectDir)).status();
|
|
@@ -49,6 +98,12 @@ export class OrbitService extends Service {
|
|
|
49
98
|
const state = new OrbitStateStore(this.resolveProjectDir(projectDir)).readState();
|
|
50
99
|
return state !== null && state.driver_ownership !== 'CLOSED';
|
|
51
100
|
}
|
|
101
|
+
isMutationAuthorized(agent, tool, projectDir) {
|
|
102
|
+
return this.host.isMutationAuthorized(agent, this.resolveProjectDir(projectDir), tool);
|
|
103
|
+
}
|
|
104
|
+
browserToolNames() {
|
|
105
|
+
return this.config.browserTools;
|
|
106
|
+
}
|
|
52
107
|
githubAllowed(projectDir) {
|
|
53
108
|
const state = new OrbitStateStore(this.resolveProjectDir(projectDir)).readState();
|
|
54
109
|
return state?.github_allowed === true;
|
|
@@ -57,9 +112,8 @@ export class OrbitService extends Service {
|
|
|
57
112
|
const dir = this.resolveProjectDir(projectDir);
|
|
58
113
|
const checks = [];
|
|
59
114
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
checks.push({ name: 'state-storage', status: 'pass', detail: `${dir}/.cx is writable` });
|
|
115
|
+
accessSync(existsSync(`${dir}/.cx`) ? `${dir}/.cx` : dir, constants.W_OK);
|
|
116
|
+
checks.push({ name: 'state-storage', status: 'pass', detail: '状态存储目录可写' });
|
|
63
117
|
}
|
|
64
118
|
catch (error) {
|
|
65
119
|
checks.push({ name: 'state-storage', status: 'fail', detail: String(error) });
|
|
@@ -92,11 +146,17 @@ export class OrbitService extends Service {
|
|
|
92
146
|
status: 'pass',
|
|
93
147
|
detail: 'DshOrbitHost provides cancel/dispose/runtimeSnapshot for every role handle',
|
|
94
148
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
const reflect = this.ctx.reflect;
|
|
150
|
+
checks.push({ name: 'model-registry', status: reflect.get('llm') ? 'pass' : 'fail', detail: '使用 DSH 当前 LLM registry 校验模型,不内置角色模型。' });
|
|
151
|
+
checks.push({ name: 'orbit-settings', status: reflect.get('settings') ? 'pass' : 'warn', detail: 'Commander / Watchdog 使用用户 Orbit 设置或显式 profile routes。' });
|
|
152
|
+
try {
|
|
153
|
+
const routes = this.config.resolveRoutes?.();
|
|
154
|
+
const issues = routes ? await this.host.validateRoutes(routes) : ['未配置模型解析来源'];
|
|
155
|
+
checks.push({ name: 'role-model-configuration', status: issues.length ? 'warn' : 'pass', detail: issues.join(';') || '三角色用户模型配置可用。' });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
checks.push({ name: 'role-model-configuration', status: 'warn', detail: String(error) });
|
|
159
|
+
}
|
|
100
160
|
checks.push({ name: 'tested-dsh-version', status: 'pass', detail: 'tested against @deepseek-ai/dsh 0.1.5-rc.2' });
|
|
101
161
|
const status = checks.some((check) => check.status === 'fail') ? 'fail' : checks.some((check) => check.status === 'warn') ? 'warn' : 'pass';
|
|
102
162
|
return { status, generatedAt: new Date().toISOString(), checks };
|
package/lib/settlement.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `completed | aborted | error | blocked | max-tokens` (`interrupted` only
|
|
6
6
|
* appears on cold-read synthesis). A child that is merely `idle` is NOT success.
|
|
7
7
|
*/
|
|
8
|
+
import { truncateSafe } from "./sanitize.js";
|
|
8
9
|
export function classifyTurnSettlement(events) {
|
|
9
10
|
let end;
|
|
10
11
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
@@ -22,11 +23,13 @@ export function classifyTurnSettlement(events) {
|
|
|
22
23
|
case 'aborted':
|
|
23
24
|
return { settlement: 'aborted', ...(reason.reason?.kind ? { cancelCause: reason.reason.kind } : {}) };
|
|
24
25
|
case 'error':
|
|
25
|
-
return { settlement: 'error', ...(reason.error?.message ? { errorMessage: reason.error.message } : {}) };
|
|
26
|
+
return { settlement: 'error', ...(reason.error?.message ? { errorMessage: truncateSafe(reason.error.message, 500) } : {}) };
|
|
26
27
|
case 'blocked':
|
|
27
28
|
return { settlement: 'blocked' };
|
|
28
29
|
case 'max-tokens':
|
|
29
30
|
return { settlement: 'max-tokens' };
|
|
31
|
+
case 'interrupted':
|
|
32
|
+
return { settlement: 'interrupted' };
|
|
30
33
|
default:
|
|
31
34
|
return { settlement: 'open' };
|
|
32
35
|
}
|
package/lib/state-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -63,15 +63,23 @@ export class OrbitStateStore {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
readRawState() {
|
|
66
|
-
|
|
67
|
-
const parsed = JSON.parse(readFileSync(this.statePath, 'utf8'));
|
|
68
|
-
if (parsed !== null && typeof parsed === 'object')
|
|
69
|
-
return parsed;
|
|
66
|
+
if (!existsSync(this.statePath))
|
|
70
67
|
return null;
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(readFileSync(this.statePath, 'utf8'));
|
|
71
71
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(`ORBIT_STATE_INVALID: 无法解析 ${this.statePath}:${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
76
|
+
throw new Error(`ORBIT_STATE_INVALID: ${this.statePath} 不是有效的对象状态。`);
|
|
77
|
+
}
|
|
78
|
+
const schema = parsed['schema_version'];
|
|
79
|
+
if (typeof schema === 'number' && schema > ORBIT_SCHEMA_VERSION) {
|
|
80
|
+
throw new Error(`ORBIT_STATE_SCHEMA_UNSUPPORTED: ${this.statePath} 使用未来 schema_version=${schema}。`);
|
|
74
81
|
}
|
|
82
|
+
return parsed;
|
|
75
83
|
}
|
|
76
84
|
readState() {
|
|
77
85
|
const raw = this.readRawState();
|