@steerable/agent-shell 0.6.56 → 0.6.57
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/cli-install.d.ts +27 -0
- package/dist/cli-install.js +105 -0
- package/dist/goal-store.js +16 -3
- package/dist/host/http-routes.d.ts +8 -0
- package/dist/host/http-routes.js +18 -0
- package/dist/host/pack-cli.d.ts +19 -0
- package/dist/host/pack-cli.js +56 -0
- package/dist/host/runtime.js +24 -4
- package/dist/insights/flush.js +1 -1
- package/dist/json-store.js +17 -1
- package/dist/local-backend/router.d.ts +3 -0
- package/dist/local-backend/router.js +325 -249
- package/dist/local-backend/skill-install.js +11 -2
- package/dist/log.d.ts +13 -0
- package/dist/log.js +21 -0
- package/dist/scenario/pack.d.ts +1 -1
- package/dist/sidecar/reverse-spawn.js +5 -1
- package/dist/storage/driver.d.ts +4 -0
- package/dist/storage/driver.js +4 -0
- package/dist/storage/index.d.ts +8 -3
- package/dist/storage/index.js +59 -20
- package/dist/storage/process-locks.d.ts +32 -0
- package/dist/storage/process-locks.js +99 -0
- package/dist/storage/scoped-store.d.ts +1 -0
- package/dist/storage/sqlite-driver.d.ts +3 -1
- package/dist/storage/sqlite-driver.js +89 -8
- package/dist/storage/write-lease.d.ts +7 -1
- package/dist/storage/write-lease.js +35 -4
- package/package.json +4 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把产品命令行包装脚本写进用户 PATH 里一个可写的目录。
|
|
3
|
+
* 桌面安装包里的入口是 products/<bin>/cli.js;仓库里优先用 cli.mjs。
|
|
4
|
+
*/
|
|
5
|
+
export interface CliInstallResult {
|
|
6
|
+
path: string;
|
|
7
|
+
onPath: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface CliInstallLinkInput {
|
|
10
|
+
binName: string;
|
|
11
|
+
scriptPath: string;
|
|
12
|
+
nodePath: string;
|
|
13
|
+
home: string;
|
|
14
|
+
pathEnv: string;
|
|
15
|
+
platform: NodeJS.Platform;
|
|
16
|
+
exists(target: string): boolean;
|
|
17
|
+
canWrite(dir: string): boolean;
|
|
18
|
+
mkdir(dir: string): void;
|
|
19
|
+
writeFile(file: string, contents: string): void;
|
|
20
|
+
chmod(file: string, mode: number): void;
|
|
21
|
+
}
|
|
22
|
+
/** 在应用根下找命令行入口。 */
|
|
23
|
+
export declare function resolveCliScript(appRoot: string, binName: string, exists: (target: string) => boolean): string | null;
|
|
24
|
+
/** 写包装脚本。目录选用户主目录下、已经在 PATH 里、并且可写的那个。 */
|
|
25
|
+
export declare function installCliLink(input: CliInstallLinkInput): CliInstallResult;
|
|
26
|
+
/** 把名为 `binName` 的产品命令安装到 PATH。调用方传入命令名。 */
|
|
27
|
+
export declare function installProductCli(binName: string): CliInstallResult;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把产品命令行包装脚本写进用户 PATH 里一个可写的目录。
|
|
3
|
+
* 桌面安装包里的入口是 products/<bin>/cli.js;仓库里优先用 cli.mjs。
|
|
4
|
+
*/
|
|
5
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { getAppRootDir } from './runtime.js';
|
|
9
|
+
/** 在应用根下找命令行入口。 */
|
|
10
|
+
export function resolveCliScript(appRoot, binName, exists) {
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(appRoot, 'products', binName, 'cli.mjs'),
|
|
13
|
+
path.join(appRoot, 'products', binName, 'cli.js'),
|
|
14
|
+
path.join(appRoot, 'dist', 'products', binName, 'cli.js'),
|
|
15
|
+
];
|
|
16
|
+
return candidates.find((candidate) => exists(candidate)) ?? null;
|
|
17
|
+
}
|
|
18
|
+
/** 写包装脚本。目录选用户主目录下、已经在 PATH 里、并且可写的那个。 */
|
|
19
|
+
export function installCliLink(input) {
|
|
20
|
+
if (input.binName.includes('/') || input.binName.includes('\\')) {
|
|
21
|
+
throw new Error(`command name "${input.binName}" cannot contain a path separator`);
|
|
22
|
+
}
|
|
23
|
+
const pathApi = input.platform === 'win32' ? path.win32 : path.posix;
|
|
24
|
+
const delimiter = input.platform === 'win32' ? ';' : ':';
|
|
25
|
+
const pathDirs = input.pathEnv.split(delimiter).filter((dir) => dir.length > 0);
|
|
26
|
+
const preferred = [
|
|
27
|
+
pathApi.join(input.home, '.local', 'bin'),
|
|
28
|
+
pathApi.join(input.home, 'bin'),
|
|
29
|
+
];
|
|
30
|
+
const ordered = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const push = (dir, onPath) => {
|
|
33
|
+
if (seen.has(dir))
|
|
34
|
+
return;
|
|
35
|
+
seen.add(dir);
|
|
36
|
+
ordered.push({ dir, onPath });
|
|
37
|
+
};
|
|
38
|
+
for (const dir of preferred) {
|
|
39
|
+
if (pathDirs.includes(dir))
|
|
40
|
+
push(dir, true);
|
|
41
|
+
}
|
|
42
|
+
for (const dir of pathDirs) {
|
|
43
|
+
if (dir === input.home || dir.startsWith(input.home + pathApi.sep))
|
|
44
|
+
push(dir, true);
|
|
45
|
+
}
|
|
46
|
+
push(preferred[0], pathDirs.includes(preferred[0]));
|
|
47
|
+
const chosen = ordered.find((entry) => input.canWrite(entry.dir));
|
|
48
|
+
if (!chosen) {
|
|
49
|
+
throw new Error('no writable directory on PATH under the home directory');
|
|
50
|
+
}
|
|
51
|
+
if (!input.exists(chosen.dir))
|
|
52
|
+
input.mkdir(chosen.dir);
|
|
53
|
+
const fileName = input.platform === 'win32' ? `${input.binName}.cmd` : input.binName;
|
|
54
|
+
const file = pathApi.join(chosen.dir, fileName);
|
|
55
|
+
const contents = input.platform === 'win32'
|
|
56
|
+
? `@echo off\r\n${quoteCmd(input.nodePath)} ${quoteCmd(input.scriptPath)} %*\r\n`
|
|
57
|
+
: `#!/bin/sh\nexec ${quoteShell(input.nodePath)} ${quoteShell(input.scriptPath)} "$@"\n`;
|
|
58
|
+
input.writeFile(file, contents);
|
|
59
|
+
if (input.platform !== 'win32')
|
|
60
|
+
input.chmod(file, 0o755);
|
|
61
|
+
return { path: file, onPath: chosen.onPath };
|
|
62
|
+
}
|
|
63
|
+
/** 把名为 `binName` 的产品命令安装到 PATH。调用方传入命令名。 */
|
|
64
|
+
export function installProductCli(binName) {
|
|
65
|
+
const name = binName.trim();
|
|
66
|
+
if (!name)
|
|
67
|
+
throw new Error('This product has no command-line name.');
|
|
68
|
+
const scriptPath = resolveCliScript(getAppRootDir(), name, existsSync);
|
|
69
|
+
if (!scriptPath)
|
|
70
|
+
throw new Error('The command-line tool is not included in this installation.');
|
|
71
|
+
return installCliLink({
|
|
72
|
+
binName: name,
|
|
73
|
+
scriptPath,
|
|
74
|
+
nodePath: process.execPath,
|
|
75
|
+
home: homedir(),
|
|
76
|
+
pathEnv: process.env.PATH ?? '',
|
|
77
|
+
platform: process.platform,
|
|
78
|
+
exists: existsSync,
|
|
79
|
+
canWrite: canWriteDir,
|
|
80
|
+
mkdir: (dir) => mkdirSync(dir, { recursive: true }),
|
|
81
|
+
writeFile: (file, contents) => writeFileSync(file, contents, 'utf8'),
|
|
82
|
+
chmod: (file, mode) => chmodSync(file, mode),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function canWriteDir(dir) {
|
|
86
|
+
try {
|
|
87
|
+
if (existsSync(dir)) {
|
|
88
|
+
accessSync(dir, constants.W_OK);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
accessSync(path.dirname(dir), constants.W_OK);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// 目录或其父目录不可写。
|
|
96
|
+
void error;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function quoteShell(value) {
|
|
101
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
102
|
+
}
|
|
103
|
+
function quoteCmd(value) {
|
|
104
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
105
|
+
}
|
package/dist/goal-store.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* 修订号做乐观并发:update_goal 必须带上一次读到的 revision。
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { getUserDataDir } from './runtime.js';
|
|
10
|
+
import { fileLockPath } from './storage/process-locks.js';
|
|
11
|
+
import { acquireWriteLease } from './storage/write-lease.js';
|
|
10
12
|
export const GOAL_ACTIONS = [
|
|
11
13
|
'edit',
|
|
12
14
|
'pause',
|
|
@@ -87,7 +89,16 @@ export class GoalStore {
|
|
|
87
89
|
});
|
|
88
90
|
}
|
|
89
91
|
queue(fn) {
|
|
90
|
-
const
|
|
92
|
+
const runLocked = async () => {
|
|
93
|
+
const lease = acquireWriteLease(fileLockPath(getUserDataDir(), 'goals'), 5_000);
|
|
94
|
+
try {
|
|
95
|
+
return await fn();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
lease.release();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const run = this.pending.then(runLocked, runLocked);
|
|
91
102
|
this.pending = run.then(() => undefined, () => undefined);
|
|
92
103
|
return run;
|
|
93
104
|
}
|
|
@@ -111,7 +122,9 @@ export class GoalStore {
|
|
|
111
122
|
}
|
|
112
123
|
async write(data) {
|
|
113
124
|
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
114
|
-
|
|
125
|
+
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
126
|
+
await writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
127
|
+
await rename(tmp, this.filePath);
|
|
115
128
|
}
|
|
116
129
|
}
|
|
117
130
|
function applyAction(current, action, objective, reason) {
|
|
@@ -10,6 +10,14 @@ import type { HttpRouteContribution, HttpRouteRequest } from '../scenario/pack.j
|
|
|
10
10
|
export type { HttpRouteContribution, HttpRouteRequest };
|
|
11
11
|
/** 注册包 HTTP 路由;路径越出包前缀直接抛错(组装期失败,不进运行时)。 */
|
|
12
12
|
export declare function registerPackHttpRoutes(packId: string, routes: readonly HttpRouteContribution[]): void;
|
|
13
|
+
/**
|
|
14
|
+
* 进程内路由器调用包的 `/host/<packId>/*` 路由。
|
|
15
|
+
* 处理器直接返回数据,这里包成 `{ status, data }`。
|
|
16
|
+
*/
|
|
17
|
+
export declare function invokePackHttpRoute(method: string, pathname: string, body: unknown): Promise<{
|
|
18
|
+
status: number;
|
|
19
|
+
data: unknown;
|
|
20
|
+
} | null>;
|
|
13
21
|
/** 测试钩子:清空注册表。 */
|
|
14
22
|
export declare function resetPackHttpRoutes(): void;
|
|
15
23
|
/**
|
package/dist/host/http-routes.js
CHANGED
|
@@ -16,9 +16,27 @@ function assertPackRoutePath(packId, path) {
|
|
|
16
16
|
export function registerPackHttpRoutes(packId, routes) {
|
|
17
17
|
for (const route of routes) {
|
|
18
18
|
assertPackRoutePath(packId, route.path);
|
|
19
|
+
const already = packHttpRoutes.some((existing) => existing.method === route.method && existing.path === route.path);
|
|
20
|
+
if (already)
|
|
21
|
+
continue;
|
|
19
22
|
packHttpRoutes.push(route);
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 进程内路由器调用包的 `/host/<packId>/*` 路由。
|
|
27
|
+
* 处理器直接返回数据,这里包成 `{ status, data }`。
|
|
28
|
+
*/
|
|
29
|
+
export async function invokePackHttpRoute(method, pathname, body) {
|
|
30
|
+
const match = matchPackHttpRoute(method, pathname);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
const data = await match.route.handler({
|
|
34
|
+
params: match.params,
|
|
35
|
+
query: {},
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
return { status: 200, data };
|
|
39
|
+
}
|
|
22
40
|
/** 测试钩子:清空注册表。 */
|
|
23
41
|
export function resetPackHttpRoutes() {
|
|
24
42
|
packHttpRoutes.length = 0;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包命令行注册表。产品组装根在启动 CLI 前注册;agent-cli 在内置命令之后分发。
|
|
3
|
+
* 注册发生在 import 期或 createCli 的当次调用,不依赖宿主装配。
|
|
4
|
+
*/
|
|
5
|
+
import type { CliCommandSpec } from '../scenario/pack.js';
|
|
6
|
+
export type { CliCommandSpec };
|
|
7
|
+
/** 名字必须是小写命令,且不能占用内置命令。 */
|
|
8
|
+
export declare function assertPackCliCommandName(name: string): void;
|
|
9
|
+
/** 注册一个包的命令。同包或同名重复注册抛错。 */
|
|
10
|
+
export declare function registerPackCliCommands(packId: string, commands: readonly CliCommandSpec[]): void;
|
|
11
|
+
/** 已注册命令,按注册序。 */
|
|
12
|
+
export declare function listPackCliCommands(): readonly CliCommandSpec[];
|
|
13
|
+
/**
|
|
14
|
+
* 合并注册表和本次调用额外传入的命令。额外命令不写入注册表,
|
|
15
|
+
* 所以 createCli 可以重复调用。
|
|
16
|
+
*/
|
|
17
|
+
export declare function collectPackCliCommands(extra?: readonly CliCommandSpec[]): CliCommandSpec[];
|
|
18
|
+
/** 测试钩子。 */
|
|
19
|
+
export declare function resetPackCliCommands(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包命令行注册表。产品组装根在启动 CLI 前注册;agent-cli 在内置命令之后分发。
|
|
3
|
+
* 注册发生在 import 期或 createCli 的当次调用,不依赖宿主装配。
|
|
4
|
+
*/
|
|
5
|
+
const RESERVED = new Set(['run', 'chat', 'skills', 'mcp', 'config', 'doctor', 'tui']);
|
|
6
|
+
const NAME = /^[a-z][a-z0-9-]*$/;
|
|
7
|
+
const byPack = new Map();
|
|
8
|
+
/** 名字必须是小写命令,且不能占用内置命令。 */
|
|
9
|
+
export function assertPackCliCommandName(name) {
|
|
10
|
+
if (!NAME.test(name)) {
|
|
11
|
+
throw new Error(`[pack-cli] command name "${name}" must match ${NAME}`);
|
|
12
|
+
}
|
|
13
|
+
if (RESERVED.has(name)) {
|
|
14
|
+
throw new Error(`[pack-cli] command "${name}" is reserved`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** 注册一个包的命令。同包或同名重复注册抛错。 */
|
|
18
|
+
export function registerPackCliCommands(packId, commands) {
|
|
19
|
+
if (byPack.has(packId)) {
|
|
20
|
+
throw new Error(`[pack-cli] duplicate registration for pack: ${packId}`);
|
|
21
|
+
}
|
|
22
|
+
const taken = new Set(listPackCliCommands().map((command) => command.name));
|
|
23
|
+
for (const command of commands) {
|
|
24
|
+
assertPackCliCommandName(command.name);
|
|
25
|
+
if (taken.has(command.name)) {
|
|
26
|
+
throw new Error(`[pack-cli] duplicate command: ${command.name}`);
|
|
27
|
+
}
|
|
28
|
+
taken.add(command.name);
|
|
29
|
+
}
|
|
30
|
+
byPack.set(packId, commands);
|
|
31
|
+
}
|
|
32
|
+
/** 已注册命令,按注册序。 */
|
|
33
|
+
export function listPackCliCommands() {
|
|
34
|
+
return [...byPack.values()].flat();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 合并注册表和本次调用额外传入的命令。额外命令不写入注册表,
|
|
38
|
+
* 所以 createCli 可以重复调用。
|
|
39
|
+
*/
|
|
40
|
+
export function collectPackCliCommands(extra = []) {
|
|
41
|
+
const merged = [...listPackCliCommands()];
|
|
42
|
+
const taken = new Set(merged.map((command) => command.name));
|
|
43
|
+
for (const command of extra) {
|
|
44
|
+
assertPackCliCommandName(command.name);
|
|
45
|
+
if (taken.has(command.name)) {
|
|
46
|
+
throw new Error(`[pack-cli] duplicate command: ${command.name}`);
|
|
47
|
+
}
|
|
48
|
+
taken.add(command.name);
|
|
49
|
+
merged.push(command);
|
|
50
|
+
}
|
|
51
|
+
return merged;
|
|
52
|
+
}
|
|
53
|
+
/** 测试钩子。 */
|
|
54
|
+
export function resetPackCliCommands() {
|
|
55
|
+
byPack.clear();
|
|
56
|
+
}
|
package/dist/host/runtime.js
CHANGED
|
@@ -22,7 +22,9 @@ import { getProductConfig } from '../product-config.js';
|
|
|
22
22
|
import { LocalBackendRouter } from '../local-backend/router.js';
|
|
23
23
|
import { WorktreeService } from '../local-backend/worktree-service.js';
|
|
24
24
|
import { TaskService } from '../local-backend/task-service.js';
|
|
25
|
-
import { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, } from '../storage/driver.js';
|
|
25
|
+
import { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, watchStorageChanges, } from '../storage/driver.js';
|
|
26
|
+
import { getUserDataDir } from '../runtime.js';
|
|
27
|
+
import { acquireInstanceLease, bindStorageInstance, } from '../storage/process-locks.js';
|
|
26
28
|
import { createApprovalBridge } from '../sidecar/reverse-approval.js';
|
|
27
29
|
import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
|
|
28
30
|
import { startHostSidecar, shutdownHostSidecar } from '../sidecar/boot.js';
|
|
@@ -35,10 +37,12 @@ import { recordInsightTurn } from '../insights/record.js';
|
|
|
35
37
|
import { llmService } from '../llm/index.js';
|
|
36
38
|
import { isProductLlmLocked, resolveRuntimeLlmSettings } from '../host-tools-runtime.js';
|
|
37
39
|
import { getPackAssemblies, } from './pack-assembly.js';
|
|
40
|
+
import { registerPackHttpRoutes } from './http-routes.js';
|
|
38
41
|
export async function createHostRuntime(options) {
|
|
39
42
|
const { broadcast, hasWindow, onLog } = options;
|
|
40
43
|
const broadcastMain = options.broadcastMain ?? broadcast;
|
|
41
44
|
await initializeStorage();
|
|
45
|
+
watchStorageChanges(() => broadcast('store:changed', {}));
|
|
42
46
|
const scope = options.scope ?? LOCAL_SCOPE;
|
|
43
47
|
const defaultStore = getScopedStore(scope);
|
|
44
48
|
let localBackendRouter;
|
|
@@ -154,6 +158,11 @@ export async function createHostRuntime(options) {
|
|
|
154
158
|
packHandles.set(packId, handle);
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
for (const [packId, handle] of packHandles) {
|
|
162
|
+
const routes = handle.httpRoutes?.();
|
|
163
|
+
if (routes && routes.length > 0)
|
|
164
|
+
registerPackHttpRoutes(packId, routes);
|
|
165
|
+
}
|
|
157
166
|
const worktreeService = new WorktreeService({ resolveProject: resolveChatProjectRoot });
|
|
158
167
|
const taskService = new TaskService({
|
|
159
168
|
store: () => localBackendRouter?.activeStore ?? defaultStore,
|
|
@@ -189,6 +198,8 @@ export async function createHostRuntime(options) {
|
|
|
189
198
|
onLog: (line) => onLog(line),
|
|
190
199
|
});
|
|
191
200
|
let started = false;
|
|
201
|
+
let instanceLease = null;
|
|
202
|
+
let warmTimer = null;
|
|
192
203
|
return {
|
|
193
204
|
store: defaultStore,
|
|
194
205
|
localExecutor,
|
|
@@ -208,8 +219,9 @@ export async function createHostRuntime(options) {
|
|
|
208
219
|
if (started)
|
|
209
220
|
return;
|
|
210
221
|
started = true;
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
instanceLease = acquireInstanceLease(getUserDataDir());
|
|
223
|
+
bindStorageInstance(instanceLease.instanceId);
|
|
224
|
+
// 只清扫所属进程已经退出的任务。本进程的实例锁还在,不会扫到自己。
|
|
213
225
|
const sweptTasks = await defaultStore.failRunningTasks(options.taskSweepReason);
|
|
214
226
|
if (sweptTasks > 0) {
|
|
215
227
|
onLog(`[task] swept ${sweptTasks} stale running task(s)`);
|
|
@@ -250,7 +262,8 @@ export async function createHostRuntime(options) {
|
|
|
250
262
|
// 后台刷新已启用 MCP 服务的工具列表(连接慢的 server 不阻塞启动)。
|
|
251
263
|
void mcpServerRegistry.refreshAllEnabled();
|
|
252
264
|
// 预热共享可见 PTY:首条 agent 命令不必付 shell 启动成本。
|
|
253
|
-
|
|
265
|
+
// 短命进程在这两秒内退出时必须取消,否则定时器会在关停之后再拉起一个 shell。
|
|
266
|
+
warmTimer = setTimeout(() => {
|
|
254
267
|
try {
|
|
255
268
|
terminalManager.ensurePrimary();
|
|
256
269
|
}
|
|
@@ -260,6 +273,10 @@ export async function createHostRuntime(options) {
|
|
|
260
273
|
}, 2_000);
|
|
261
274
|
},
|
|
262
275
|
async shutdown() {
|
|
276
|
+
if (warmTimer) {
|
|
277
|
+
clearTimeout(warmTimer);
|
|
278
|
+
warmTimer = null;
|
|
279
|
+
}
|
|
263
280
|
terminalManager.killAll();
|
|
264
281
|
// 包装配逆序关停(后装配的先停)。
|
|
265
282
|
for (const handle of [...packHandles.values()].reverse()) {
|
|
@@ -267,6 +284,9 @@ export async function createHostRuntime(options) {
|
|
|
267
284
|
}
|
|
268
285
|
await mcpExecutor.shutdownAll();
|
|
269
286
|
await shutdownHostSidecar();
|
|
287
|
+
instanceLease?.release();
|
|
288
|
+
instanceLease = null;
|
|
289
|
+
bindStorageInstance(null);
|
|
270
290
|
await closeStorage();
|
|
271
291
|
},
|
|
272
292
|
};
|
package/dist/insights/flush.js
CHANGED
|
@@ -88,7 +88,7 @@ export async function flushInsightsOutbox(store) {
|
|
|
88
88
|
return { uploaded: 0, skipped: 0, failed: 0 };
|
|
89
89
|
}
|
|
90
90
|
const settings = await store.ensureInsightsSettings();
|
|
91
|
-
const pending = await store.
|
|
91
|
+
const pending = await store.claimInsightsForUpload(MAX_BATCH);
|
|
92
92
|
const { upload, skipped } = rowsEligibleForAutoUpload(settings, pending);
|
|
93
93
|
let uploaded = 0;
|
|
94
94
|
let failed = 0;
|
package/dist/json-store.js
CHANGED
|
@@ -9,11 +9,27 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import ConfImport from 'conf';
|
|
11
11
|
import { getUserDataDir } from './runtime.js';
|
|
12
|
+
import { fileLockPath } from './storage/process-locks.js';
|
|
13
|
+
import { acquireWriteLease } from './storage/write-lease.js';
|
|
12
14
|
const Conf = ConfImport;
|
|
13
15
|
export function createJsonStore(options) {
|
|
14
|
-
|
|
16
|
+
const store = new Conf({
|
|
15
17
|
cwd: getUserDataDir(),
|
|
16
18
|
name: options.name,
|
|
17
19
|
defaults: options.defaults,
|
|
18
20
|
});
|
|
21
|
+
const lockPath = fileLockPath(getUserDataDir(), options.name);
|
|
22
|
+
return {
|
|
23
|
+
get: store.get.bind(store),
|
|
24
|
+
// set is wrapped below so two processes cannot clobber a read-modify-write.
|
|
25
|
+
set(key, value) {
|
|
26
|
+
const lease = acquireWriteLease(lockPath, 5_000);
|
|
27
|
+
try {
|
|
28
|
+
store.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
lease.release();
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
19
35
|
}
|
|
@@ -169,6 +169,9 @@ export declare class LocalBackendRouter {
|
|
|
169
169
|
*/
|
|
170
170
|
private briefResult;
|
|
171
171
|
private briefScalar;
|
|
172
|
+
private chatBusy;
|
|
173
|
+
private tryChatLock;
|
|
174
|
+
private withChatWriteLock;
|
|
172
175
|
private sse;
|
|
173
176
|
/**
|
|
174
177
|
* A4 CoreLoop turn: the sidecar runs the whole agent loop; this method
|