@hyperdrive.bot/fleet-server 0.3.164 → 0.3.166
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/server/extensions/daemon-backend.js +25 -0
- package/dist/server/server/agent/agent-storage.d.ts +67 -12
- package/dist/server/server/agent/agent-storage.js +208 -132
- package/dist/server/server/agent/providers/claude/transport/pty-query.js +10 -0
- package/dist/server/server/bootstrap.d.ts +5 -0
- package/dist/server/server/bootstrap.js +26 -3
- package/dist/server/server/chat/chat-service.d.ts +22 -0
- package/dist/server/server/chat/chat-service.js +46 -3
- package/dist/server/server/daemon-worker.js +1 -0
- package/dist/server/server/fleet/decision-service.d.ts +5 -0
- package/dist/server/server/fleet/decision-service.js +30 -0
- package/dist/server/server/loop-service.d.ts +95 -0
- package/dist/server/server/loop-service.js +64 -3
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +3 -2
- package/dist/server/server/push/token-store.d.ts +5 -1
- package/dist/server/server/push/token-store.js +26 -5
- package/dist/server/server/schedule/service.js +4 -1
- package/dist/server/server/schedule/store.d.ts +21 -1
- package/dist/server/server/schedule/store.js +283 -2
- package/dist/server/server/search/indexer.d.ts +10 -0
- package/dist/server/server/search/indexer.js +51 -2
- package/dist/server/server/session.js +12 -13
- package/dist/server/server/state/agent-state.d.ts +39 -0
- package/dist/server/server/state/agent-state.js +94 -0
- package/dist/server/server/state/daemon-state.d.ts +30 -0
- package/dist/server/server/state/daemon-state.js +99 -0
- package/dist/server/server/state/legacy-import.d.ts +124 -0
- package/dist/server/server/state/legacy-import.js +682 -0
- package/dist/server/server/state/legacy-manifest.d.ts +73 -0
- package/dist/server/server/state/legacy-manifest.js +112 -0
- package/dist/server/server/state/legacy-mirror.d.ts +57 -0
- package/dist/server/server/state/legacy-mirror.js +114 -0
- package/dist/server/server/state/legacy-sources.d.ts +9 -0
- package/dist/server/server/state/legacy-sources.js +163 -0
- package/dist/server/server/state/state-db.d.ts +263 -0
- package/dist/server/server/state/state-db.js +772 -0
- package/dist/server/server/state/state-schema.d.ts +39 -0
- package/dist/server/server/state/state-schema.js +125 -0
- package/dist/server/server/state/state-worker.d.ts +48 -0
- package/dist/server/server/state/state-worker.js +165 -0
- package/dist/server/server/websocket-server.js +2 -1
- package/dist/server/server/workspace/session-openable.js +7 -0
- package/dist/server/server/workspace-reconciliation-service.js +14 -3
- package/dist/server/server/workspace-registry.d.ts +45 -2
- package/dist/server/server/workspace-registry.js +75 -4
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-491bc812b0659658291d67bc77c9e697.js → desktop-attachment-bridge-eafa6217277b83ec33d4967049da7697.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-eafa6217277b83ec33d4967049da7697.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-eafa6217277b83ec33d4967049da7697.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-eafa6217277b83ec33d4967049da7697.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-eafa6217277b83ec33d4967049da7697.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-6d63da0fd45a8d1001f62f227484f116.js → desktop-attachment-store-bdcb16b0ac512f03771e9926602de68f.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-bdcb16b0ac512f03771e9926602de68f.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-bdcb16b0ac512f03771e9926602de68f.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-bdcb16b0ac512f03771e9926602de68f.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-bdcb16b0ac512f03771e9926602de68f.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js → index-1ff4c43fd657ba6473b02d56dae75a25.js} +1530 -1527
- package/dist/server/web-ui/_expo/static/js/web/index-1ff4c43fd657ba6473b02d56dae75a25.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-1ff4c43fd657ba6473b02d56dae75a25.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-1ff4c43fd657ba6473b02d56dae75a25.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.map.gz → index-1ff4c43fd657ba6473b02d56dae75a25.js.map.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-040c97c7cac8a3ed86c75baa2ce39d61.js → indexeddb-attachment-store-c83a6261ece654d4dfc3566e3b929e73.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-c83a6261ece654d4dfc3566e3b929e73.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-c83a6261ece654d4dfc3566e3b929e73.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-c83a6261ece654d4dfc3566e3b929e73.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-c83a6261ece654d4dfc3566e3b929e73.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-ac3bbd454eba677c4fd092227d5b5b89.js → native-file-attachment-store-ff6e2b6baeb643378628c82f177b35c9.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ff6e2b6baeb643378628c82f177b35c9.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ff6e2b6baeb643378628c82f177b35c9.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ff6e2b6baeb643378628c82f177b35c9.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ff6e2b6baeb643378628c82f177b35c9.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{profiler-b509d7fffd6cf457a9c12588b80560cb.js → profiler-a28d140b5874532b9eef737c77fde610.js} +11 -11
- package/dist/server/web-ui/_expo/static/js/web/profiler-a28d140b5874532b9eef737c77fde610.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-a28d140b5874532b9eef737c77fde610.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-a28d140b5874532b9eef737c77fde610.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-a28d140b5874532b9eef737c77fde610.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-9c90e555a5ecdc643e277f9a98913aaf.js → startRecording-f05b37fde4fc248dba4cb646dda2fc3d.js} +60 -60
- package/dist/server/web-ui/_expo/static/js/web/startRecording-f05b37fde4fc248dba4cb646dda2fc3d.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-f05b37fde4fc248dba4cb646dda2fc3d.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-f05b37fde4fc248dba4cb646dda2fc3d.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-9c90e555a5ecdc643e277f9a98913aaf.js.map.gz → startRecording-f05b37fde4fc248dba4cb646dda2fc3d.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-491bc812b0659658291d67bc77c9e697.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-491bc812b0659658291d67bc77c9e697.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-491bc812b0659658291d67bc77c9e697.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-491bc812b0659658291d67bc77c9e697.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6d63da0fd45a8d1001f62f227484f116.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6d63da0fd45a8d1001f62f227484f116.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6d63da0fd45a8d1001f62f227484f116.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6d63da0fd45a8d1001f62f227484f116.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-2ac1a7249c20c322a1f9a54563cfee62.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-2ac1a7249c20c322a1f9a54563cfee62.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-2ac1a7249c20c322a1f9a54563cfee62.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-040c97c7cac8a3ed86c75baa2ce39d61.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-040c97c7cac8a3ed86c75baa2ce39d61.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-040c97c7cac8a3ed86c75baa2ce39d61.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-040c97c7cac8a3ed86c75baa2ce39d61.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ac3bbd454eba677c4fd092227d5b5b89.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ac3bbd454eba677c4fd092227d5b5b89.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ac3bbd454eba677c4fd092227d5b5b89.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-ac3bbd454eba677c4fd092227d5b5b89.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b509d7fffd6cf457a9c12588b80560cb.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b509d7fffd6cf457a9c12588b80560cb.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b509d7fffd6cf457a9c12588b80560cb.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b509d7fffd6cf457a9c12588b80560cb.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-9c90e555a5ecdc643e277f9a98913aaf.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-9c90e555a5ecdc643e277f9a98913aaf.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-9c90e555a5ecdc643e277f9a98913aaf.js.map.br +0 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { stateDbFor } from "../server/state/state-db.js";
|
|
11
12
|
const STATUS_POLL_INTERVAL_MS = 2000;
|
|
12
13
|
function mapStatus(agent) {
|
|
13
14
|
if (agent.pendingPermissions.size > 0)
|
|
@@ -57,7 +58,19 @@ export function createDaemonExtensionBackend(deps) {
|
|
|
57
58
|
.listAgents()
|
|
58
59
|
.filter((agent) => !agent.internal)
|
|
59
60
|
.map((agent) => toSummary(agent, serverId));
|
|
61
|
+
// Extension storage lives in state.sqlite's `extension_kv` (one row per
|
|
62
|
+
// extension and key) when the daemon has it; the per-extension JSON file is
|
|
63
|
+
// mirrored. Without it, the file is the store, as before.
|
|
64
|
+
const stateDb = stateDbFor(paseoHome);
|
|
65
|
+
const kv = stateDb?.doc("extension_kv") ?? null;
|
|
60
66
|
async function readStore(extensionId) {
|
|
67
|
+
if (kv) {
|
|
68
|
+
const store = {};
|
|
69
|
+
for (const row of kv.loadAll(extensionId)) {
|
|
70
|
+
store[row.id] = JSON.parse(row.body);
|
|
71
|
+
}
|
|
72
|
+
return store;
|
|
73
|
+
}
|
|
61
74
|
try {
|
|
62
75
|
const raw = await readFile(storePath(paseoHome, extensionId), "utf8");
|
|
63
76
|
return JSON.parse(raw);
|
|
@@ -120,6 +133,18 @@ export function createDaemonExtensionBackend(deps) {
|
|
|
120
133
|
return store[key];
|
|
121
134
|
},
|
|
122
135
|
set: async (extensionId, key, value) => {
|
|
136
|
+
if (kv && stateDb) {
|
|
137
|
+
await kv.put(extensionId, key, value ?? null);
|
|
138
|
+
const file = storePath(paseoHome, extensionId);
|
|
139
|
+
stateDb.mirror.write(`extension-kv:${file}`, async () => {
|
|
140
|
+
const rev = stateDb.mirrorRev();
|
|
141
|
+
const store = await readStore(extensionId);
|
|
142
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
143
|
+
await writeFile(file, JSON.stringify(store, null, 2));
|
|
144
|
+
await stateDb.manifest.record("extension_kv", file, rev);
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
123
148
|
const store = await readStore(extensionId);
|
|
124
149
|
store[key] = value;
|
|
125
150
|
const file = storePath(paseoHome, extensionId);
|
|
@@ -3,6 +3,7 @@ import type { Logger } from "pino";
|
|
|
3
3
|
import { type SessionDigest } from "../messages.js";
|
|
4
4
|
import type { ManagedAgent } from "./agent-manager.js";
|
|
5
5
|
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
|
6
|
+
import type { ChangedRow } from "../state/state-db.js";
|
|
6
7
|
declare const STORED_AGENT_SCHEMA: z.ZodObject<{
|
|
7
8
|
id: z.ZodString;
|
|
8
9
|
provider: z.ZodString;
|
|
@@ -172,18 +173,30 @@ export declare const SERIALIZABLE_CONFIG_KEYS: ReadonlyArray<keyof SerializableA
|
|
|
172
173
|
export type SerializableAgentConfig = Pick<AgentSessionConfig, "modeId" | "model" | "thinkingOptionId" | "featureValues" | "extra" | "systemPrompt" | "outputLanguage" | "goal" | "jobToBeDone" | "mcpServers" | "allowedTools">;
|
|
173
174
|
export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
|
|
174
175
|
export declare function parseStoredAgentRecord(value: unknown): StoredAgentRecord;
|
|
176
|
+
/**
|
|
177
|
+
* Where agent records are durably kept. Two implementations: the legacy
|
|
178
|
+
* per-agent JSON files (`FileAgentRecordBackend`) and `state.sqlite`
|
|
179
|
+
* (`state/agent-state.ts`). `AgentStorage` keeps the in-memory Map as the only
|
|
180
|
+
* read path either way; a backend is only loaded once and then written to.
|
|
181
|
+
*/
|
|
182
|
+
export interface AgentRecordBackend {
|
|
183
|
+
loadAll(): Promise<StoredAgentRecord[]>;
|
|
184
|
+
/** `previous` is the record this one replaces, when there was one. */
|
|
185
|
+
write(record: StoredAgentRecord, previous: StoredAgentRecord | null): Promise<void>;
|
|
186
|
+
remove(agentId: string, previous: StoredAgentRecord | null): Promise<void>;
|
|
187
|
+
/** Changes since a daemon-wide revision, or null when the backend has no revisions. */
|
|
188
|
+
listChangedSince?(rev: number, limit?: number): ChangedRow[] | null;
|
|
189
|
+
}
|
|
175
190
|
export declare class AgentStorage {
|
|
176
191
|
private cache;
|
|
177
|
-
private pathById;
|
|
178
|
-
private pathsById;
|
|
179
192
|
private pendingWrites;
|
|
180
193
|
private deleting;
|
|
181
194
|
private loaded;
|
|
182
|
-
private baseDir;
|
|
183
195
|
private loadPromise;
|
|
184
196
|
private logger;
|
|
197
|
+
private readonly backend;
|
|
185
198
|
private readonly changeListeners;
|
|
186
|
-
constructor(baseDir: string, logger: Logger);
|
|
199
|
+
constructor(baseDir: string, logger: Logger, backend?: AgentRecordBackend);
|
|
187
200
|
/**
|
|
188
201
|
* Called after a record lands in (or leaves) the cache, so a listener that
|
|
189
202
|
* reads the record back sees the new state. Feeds the agent directory
|
|
@@ -194,6 +207,11 @@ export declare class AgentStorage {
|
|
|
194
207
|
initialize(): Promise<void>;
|
|
195
208
|
list(): Promise<StoredAgentRecord[]>;
|
|
196
209
|
get(agentId: string): Promise<StoredAgentRecord | null>;
|
|
210
|
+
/**
|
|
211
|
+
* Agent rows changed after `rev` (tombstones included), oldest first. Null on
|
|
212
|
+
* the file backend, which has no revisions. Internal until the delta protocol.
|
|
213
|
+
*/
|
|
214
|
+
listChangedSince(rev: number, limit?: number): ChangedRow[] | null;
|
|
197
215
|
upsert(record: StoredAgentRecord): Promise<void>;
|
|
198
216
|
private queueRecordWrite;
|
|
199
217
|
private writeRecord;
|
|
@@ -203,24 +221,61 @@ export declare class AgentStorage {
|
|
|
203
221
|
title?: string | null;
|
|
204
222
|
internal?: boolean;
|
|
205
223
|
}): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* Read-modify-write of one stored agent inside its write queue. `build` gets
|
|
226
|
+
* the latest record (after every earlier write for the agent landed) and
|
|
227
|
+
* returns the next one, or null to leave it untouched. Resolves with what was
|
|
228
|
+
* written, or null.
|
|
229
|
+
*/
|
|
230
|
+
update(agentId: string, build: (record: StoredAgentRecord) => StoredAgentRecord | null): Promise<StoredAgentRecord | null>;
|
|
206
231
|
setTitle(agentId: string, title: string): Promise<void>;
|
|
207
232
|
setGeneratedTitle(agentId: string, title: string): Promise<StoredAgentRecord>;
|
|
208
233
|
/**
|
|
209
|
-
* Targeted digest-only update.
|
|
210
|
-
* and updates ONLY the digest field
|
|
211
|
-
* clobber a concurrent title/label write (which a full-record
|
|
212
|
-
*
|
|
234
|
+
* Targeted digest-only update. Reads the latest record inside the per-agent
|
|
235
|
+
* write queue and updates ONLY the digest field, so an async digest refresh
|
|
236
|
+
* can never clobber a concurrent title/label write (which a full-record
|
|
237
|
+
* persist would). See docs/session-digest.md §5.
|
|
213
238
|
*/
|
|
214
239
|
setDigest(agentId: string, digest: SessionDigest): Promise<StoredAgentRecord>;
|
|
240
|
+
/**
|
|
241
|
+
* Read-modify-write inside the per-agent write queue. The read happens when
|
|
242
|
+
* the previous write for this agent has landed, so two updates racing (an
|
|
243
|
+
* archive teardown persisting its final snapshot while the user renames the
|
|
244
|
+
* agent) can no longer both start from the same old record and have the later
|
|
245
|
+
* one silently drop the earlier one's change.
|
|
246
|
+
*/
|
|
247
|
+
private queueRecordUpdate;
|
|
215
248
|
flush(): Promise<void>;
|
|
216
249
|
private load;
|
|
217
250
|
private doLoad;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The legacy layout: one `<baseDir>/<project-dir>/<agentId>.json` per agent.
|
|
254
|
+
* Also what the sqlite backend mirrors to, so it must work without a prior
|
|
255
|
+
* `loadAll` (the mirror never scans): a path it has not indexed is derived from
|
|
256
|
+
* the previous record's cwd.
|
|
257
|
+
*/
|
|
258
|
+
export declare class FileAgentRecordBackend implements AgentRecordBackend {
|
|
259
|
+
private readonly baseDir;
|
|
260
|
+
private readonly logger;
|
|
261
|
+
private readonly observer?;
|
|
262
|
+
private pathById;
|
|
263
|
+
private pathsById;
|
|
264
|
+
constructor(baseDir: string, logger: Logger, observer?: {
|
|
265
|
+
written(filePath: string): Promise<void>;
|
|
266
|
+
removed(filePath: string): Promise<void>;
|
|
267
|
+
} | undefined);
|
|
268
|
+
/** Tell the backend a file for `agentId` exists (the sqlite mirror seeds this from its manifest). */
|
|
269
|
+
indexPath(agentId: string, filePath: string): void;
|
|
270
|
+
loadAll(): Promise<StoredAgentRecord[]>;
|
|
271
|
+
write(record: StoredAgentRecord, previous: StoredAgentRecord | null): Promise<void>;
|
|
272
|
+
remove(agentId: string, previous: StoredAgentRecord | null): Promise<void>;
|
|
221
273
|
private addIndexedPath;
|
|
222
274
|
private removeIndexedPath;
|
|
223
|
-
private waitForPendingWrite;
|
|
224
275
|
}
|
|
276
|
+
/** Every `*.json` directly under `baseDir` or one project directory below it. */
|
|
277
|
+
export declare function listAgentRecordFiles(baseDir: string): Promise<string[]>;
|
|
278
|
+
export declare function readAgentRecordFile(filePath: string, logger: Logger): Promise<StoredAgentRecord | null>;
|
|
279
|
+
export declare function agentRecordPath(baseDir: string, record: StoredAgentRecord): string;
|
|
225
280
|
export {};
|
|
226
281
|
//# sourceMappingURL=agent-storage.d.ts.map
|
|
@@ -90,17 +90,15 @@ export function parseStoredAgentRecord(value) {
|
|
|
90
90
|
return STORED_AGENT_SCHEMA.parse(value);
|
|
91
91
|
}
|
|
92
92
|
export class AgentStorage {
|
|
93
|
-
constructor(baseDir, logger) {
|
|
93
|
+
constructor(baseDir, logger, backend) {
|
|
94
94
|
this.cache = new Map();
|
|
95
|
-
this.pathById = new Map();
|
|
96
|
-
this.pathsById = new Map();
|
|
97
95
|
this.pendingWrites = new Map();
|
|
98
96
|
this.deleting = new Set();
|
|
99
97
|
this.loaded = false;
|
|
100
98
|
this.loadPromise = null;
|
|
101
99
|
this.changeListeners = new Set();
|
|
102
|
-
this.baseDir = baseDir;
|
|
103
100
|
this.logger = logger.child({ module: "agent", component: "agent-storage" });
|
|
101
|
+
this.backend = backend ?? new FileAgentRecordBackend(baseDir, this.logger);
|
|
104
102
|
}
|
|
105
103
|
/**
|
|
106
104
|
* Called after a record lands in (or leaves) the cache, so a listener that
|
|
@@ -134,6 +132,13 @@ export class AgentStorage {
|
|
|
134
132
|
await this.load();
|
|
135
133
|
return this.cache.get(agentId) ?? null;
|
|
136
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Agent rows changed after `rev` (tombstones included), oldest first. Null on
|
|
137
|
+
* the file backend, which has no revisions. Internal until the delta protocol.
|
|
138
|
+
*/
|
|
139
|
+
listChangedSince(rev, limit) {
|
|
140
|
+
return this.backend.listChangedSince?.(rev, limit) ?? null;
|
|
141
|
+
}
|
|
137
142
|
async upsert(record) {
|
|
138
143
|
await this.load();
|
|
139
144
|
await this.queueRecordWrite(record);
|
|
@@ -157,23 +162,9 @@ export class AgentStorage {
|
|
|
157
162
|
return tracked;
|
|
158
163
|
}
|
|
159
164
|
async writeRecord(record) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await writeJsonFileAtomic(nextPath, record);
|
|
164
|
-
this.addIndexedPath(agentId, nextPath);
|
|
165
|
-
if (previousPath && previousPath !== nextPath) {
|
|
166
|
-
try {
|
|
167
|
-
await fs.unlink(previousPath);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// ignore cleanup errors
|
|
171
|
-
}
|
|
172
|
-
this.removeIndexedPath(agentId, previousPath);
|
|
173
|
-
}
|
|
174
|
-
this.cache.set(agentId, record);
|
|
175
|
-
this.pathById.set(agentId, nextPath);
|
|
176
|
-
this.notifyChanged(agentId, "upsert");
|
|
165
|
+
await this.backend.write(record, this.cache.get(record.id) ?? null);
|
|
166
|
+
this.cache.set(record.id, record);
|
|
167
|
+
this.notifyChanged(record.id, "upsert");
|
|
177
168
|
}
|
|
178
169
|
beginDelete(agentId) {
|
|
179
170
|
this.deleting.add(agentId);
|
|
@@ -182,88 +173,109 @@ export class AgentStorage {
|
|
|
182
173
|
await this.load();
|
|
183
174
|
this.beginDelete(agentId);
|
|
184
175
|
await (this.pendingWrites.get(agentId) ?? Promise.resolve());
|
|
185
|
-
|
|
186
|
-
await Promise.all(paths.map(async (filePath) => {
|
|
187
|
-
try {
|
|
188
|
-
await fs.unlink(filePath);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
const code = error.code;
|
|
192
|
-
if (code && code !== "ENOENT") {
|
|
193
|
-
this.logger.warn({ err: error, agentId, filePath }, "Failed to remove agent record file");
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}));
|
|
176
|
+
await this.backend.remove(agentId, this.cache.get(agentId) ?? null);
|
|
197
177
|
const existed = this.cache.delete(agentId);
|
|
198
|
-
this.pathById.delete(agentId);
|
|
199
|
-
this.pathsById.delete(agentId);
|
|
200
178
|
if (existed) {
|
|
201
179
|
this.notifyChanged(agentId, "remove");
|
|
202
180
|
}
|
|
203
181
|
}
|
|
204
182
|
async applySnapshot(agent, options) {
|
|
205
183
|
await this.load();
|
|
206
|
-
await this.waitForPendingWrite(agent.id);
|
|
207
|
-
const existing = (await this.get(agent.id)) ?? null;
|
|
208
184
|
const hasTitleOverride = options !== undefined && Object.prototype.hasOwnProperty.call(options, "title");
|
|
209
185
|
const hasInternalOverride = options !== undefined && Object.prototype.hasOwnProperty.call(options, "internal");
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
186
|
+
await this.queueRecordUpdate(agent.id, (existing) => {
|
|
187
|
+
const record = toStoredAgentRecord(agent, {
|
|
188
|
+
title: hasTitleOverride ? (options?.title ?? null) : (existing?.title ?? null),
|
|
189
|
+
createdAt: existing?.createdAt,
|
|
190
|
+
internal: hasInternalOverride ? options?.internal : (agent.internal ?? existing?.internal),
|
|
191
|
+
});
|
|
192
|
+
// Preserve soft-delete/archive status across snapshot flushes.
|
|
193
|
+
// `archivedAt` is not part of the ManagedAgent snapshot, so a naive projection
|
|
194
|
+
// would wipe it during normal persistence (including on daemon restart).
|
|
195
|
+
if (existing && existing.archivedAt !== undefined) {
|
|
196
|
+
record.archivedAt = existing.archivedAt;
|
|
197
|
+
}
|
|
198
|
+
return record;
|
|
214
199
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Read-modify-write of one stored agent inside its write queue. `build` gets
|
|
203
|
+
* the latest record (after every earlier write for the agent landed) and
|
|
204
|
+
* returns the next one, or null to leave it untouched. Resolves with what was
|
|
205
|
+
* written, or null.
|
|
206
|
+
*/
|
|
207
|
+
async update(agentId, build) {
|
|
208
|
+
await this.load();
|
|
209
|
+
let skipped = false;
|
|
210
|
+
const written = await this.queueRecordUpdate(agentId, (record) => {
|
|
211
|
+
const next = record ? build(record) : null;
|
|
212
|
+
if (!next) {
|
|
213
|
+
skipped = true;
|
|
214
|
+
// Nothing to write: hand the queue the current record untouched.
|
|
215
|
+
return record;
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
}, () => skipped);
|
|
219
|
+
return skipped ? null : written;
|
|
222
220
|
}
|
|
223
221
|
async setTitle(agentId, title) {
|
|
224
222
|
await this.load();
|
|
225
|
-
await this.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
await this.queueRecordUpdate(agentId, (record) => {
|
|
224
|
+
if (!record) {
|
|
225
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
226
|
+
}
|
|
227
|
+
return { ...record, title };
|
|
228
|
+
});
|
|
231
229
|
}
|
|
232
230
|
async setGeneratedTitle(agentId, title) {
|
|
233
231
|
await this.load();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
...record,
|
|
241
|
-
title,
|
|
242
|
-
updatedAt: new Date().toISOString(),
|
|
243
|
-
};
|
|
244
|
-
await this.queueRecordWrite(nextRecord);
|
|
245
|
-
return nextRecord;
|
|
232
|
+
return this.queueRecordUpdate(agentId, (record) => {
|
|
233
|
+
if (!record) {
|
|
234
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
235
|
+
}
|
|
236
|
+
return { ...record, title, updatedAt: new Date().toISOString() };
|
|
237
|
+
});
|
|
246
238
|
}
|
|
247
239
|
/**
|
|
248
|
-
* Targeted digest-only update.
|
|
249
|
-
* and updates ONLY the digest field
|
|
250
|
-
* clobber a concurrent title/label write (which a full-record
|
|
251
|
-
*
|
|
240
|
+
* Targeted digest-only update. Reads the latest record inside the per-agent
|
|
241
|
+
* write queue and updates ONLY the digest field, so an async digest refresh
|
|
242
|
+
* can never clobber a concurrent title/label write (which a full-record
|
|
243
|
+
* persist would). See docs/session-digest.md §5.
|
|
252
244
|
*/
|
|
253
245
|
async setDigest(agentId, digest) {
|
|
254
246
|
await this.load();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
247
|
+
return this.queueRecordUpdate(agentId, (record) => {
|
|
248
|
+
if (!record) {
|
|
249
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
250
|
+
}
|
|
251
|
+
return { ...record, digest, updatedAt: new Date().toISOString() };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Read-modify-write inside the per-agent write queue. The read happens when
|
|
256
|
+
* the previous write for this agent has landed, so two updates racing (an
|
|
257
|
+
* archive teardown persisting its final snapshot while the user renames the
|
|
258
|
+
* agent) can no longer both start from the same old record and have the later
|
|
259
|
+
* one silently drop the earlier one's change.
|
|
260
|
+
*/
|
|
261
|
+
queueRecordUpdate(agentId, build, skip = () => false) {
|
|
262
|
+
let built = null;
|
|
263
|
+
const prev = (this.pendingWrites.get(agentId) ?? Promise.resolve()).catch(() => undefined);
|
|
264
|
+
const next = prev.then(async () => {
|
|
265
|
+
built = build(this.cache.get(agentId) ?? null);
|
|
266
|
+
if (skip() || this.deleting.has(agentId)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
await this.writeRecord(built);
|
|
270
|
+
return undefined;
|
|
271
|
+
});
|
|
272
|
+
const tracked = next.finally(() => {
|
|
273
|
+
if (this.pendingWrites.get(agentId) === tracked) {
|
|
274
|
+
this.pendingWrites.delete(agentId);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
this.pendingWrites.set(agentId, tracked);
|
|
278
|
+
return tracked.then(() => built);
|
|
267
279
|
}
|
|
268
280
|
async flush() {
|
|
269
281
|
await this.load().catch(() => undefined);
|
|
@@ -281,10 +293,11 @@ export class AgentStorage {
|
|
|
281
293
|
}
|
|
282
294
|
async doLoad() {
|
|
283
295
|
this.cache.clear();
|
|
284
|
-
this.pathById.clear();
|
|
285
|
-
this.pathsById.clear();
|
|
286
296
|
try {
|
|
287
|
-
const records = await this.
|
|
297
|
+
const records = await this.backend.loadAll();
|
|
298
|
+
for (const record of records) {
|
|
299
|
+
this.cache.set(record.id, record);
|
|
300
|
+
}
|
|
288
301
|
this.loaded = true;
|
|
289
302
|
return records;
|
|
290
303
|
}
|
|
@@ -298,65 +311,85 @@ export class AgentStorage {
|
|
|
298
311
|
return [];
|
|
299
312
|
}
|
|
300
313
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
}));
|
|
330
|
-
const allFilePaths = [...rootRecordPaths, ...projectFileLists.flat()];
|
|
331
|
-
const loaded = await Promise.all(allFilePaths.map(async (filePath) => {
|
|
332
|
-
const record = await this.readRecordFile(filePath);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The legacy layout: one `<baseDir>/<project-dir>/<agentId>.json` per agent.
|
|
317
|
+
* Also what the sqlite backend mirrors to, so it must work without a prior
|
|
318
|
+
* `loadAll` (the mirror never scans): a path it has not indexed is derived from
|
|
319
|
+
* the previous record's cwd.
|
|
320
|
+
*/
|
|
321
|
+
export class FileAgentRecordBackend {
|
|
322
|
+
constructor(baseDir, logger, observer) {
|
|
323
|
+
this.baseDir = baseDir;
|
|
324
|
+
this.logger = logger;
|
|
325
|
+
this.observer = observer;
|
|
326
|
+
this.pathById = new Map();
|
|
327
|
+
this.pathsById = new Map();
|
|
328
|
+
}
|
|
329
|
+
/** Tell the backend a file for `agentId` exists (the sqlite mirror seeds this from its manifest). */
|
|
330
|
+
indexPath(agentId, filePath) {
|
|
331
|
+
this.addIndexedPath(agentId, filePath);
|
|
332
|
+
if (!this.pathById.has(agentId))
|
|
333
|
+
this.pathById.set(agentId, filePath);
|
|
334
|
+
}
|
|
335
|
+
async loadAll() {
|
|
336
|
+
this.pathById.clear();
|
|
337
|
+
this.pathsById.clear();
|
|
338
|
+
const files = await listAgentRecordFiles(this.baseDir);
|
|
339
|
+
const loaded = await Promise.all(files.map(async (filePath) => {
|
|
340
|
+
const record = await readAgentRecordFile(filePath, this.logger);
|
|
333
341
|
return record ? { record, filePath } : null;
|
|
334
342
|
}));
|
|
343
|
+
const records = [];
|
|
335
344
|
for (const item of loaded) {
|
|
336
345
|
if (!item)
|
|
337
346
|
continue;
|
|
338
347
|
const { record, filePath } = item;
|
|
339
348
|
records.push(record);
|
|
340
|
-
this.cache.set(record.id, record);
|
|
341
349
|
this.pathById.set(record.id, filePath);
|
|
342
350
|
this.addIndexedPath(record.id, filePath);
|
|
343
351
|
}
|
|
344
352
|
return records;
|
|
345
353
|
}
|
|
346
|
-
async
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
354
|
+
async write(record, previous) {
|
|
355
|
+
const agentId = record.id;
|
|
356
|
+
const nextPath = agentRecordPath(this.baseDir, record);
|
|
357
|
+
const previousPath = this.pathById.get(agentId) ??
|
|
358
|
+
(previous ? agentRecordPath(this.baseDir, previous) : undefined);
|
|
359
|
+
await writeJsonFileAtomic(nextPath, record);
|
|
360
|
+
this.addIndexedPath(agentId, nextPath);
|
|
361
|
+
await this.observer?.written(nextPath);
|
|
362
|
+
if (previousPath && previousPath !== nextPath) {
|
|
363
|
+
try {
|
|
364
|
+
await fs.unlink(previousPath);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// ignore cleanup errors
|
|
368
|
+
}
|
|
369
|
+
this.removeIndexedPath(agentId, previousPath);
|
|
370
|
+
await this.observer?.removed(previousPath);
|
|
355
371
|
}
|
|
372
|
+
this.pathById.set(agentId, nextPath);
|
|
356
373
|
}
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
374
|
+
async remove(agentId, previous) {
|
|
375
|
+
const paths = new Set(this.pathsById.get(agentId) ?? []);
|
|
376
|
+
if (previous) {
|
|
377
|
+
paths.add(agentRecordPath(this.baseDir, previous));
|
|
378
|
+
}
|
|
379
|
+
await Promise.all(Array.from(paths).map(async (filePath) => {
|
|
380
|
+
try {
|
|
381
|
+
await fs.unlink(filePath);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
const code = error.code;
|
|
385
|
+
if (code && code !== "ENOENT") {
|
|
386
|
+
this.logger.warn({ err: error, agentId, filePath }, "Failed to remove agent record file");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await this.observer?.removed(filePath);
|
|
390
|
+
}));
|
|
391
|
+
this.pathById.delete(agentId);
|
|
392
|
+
this.pathsById.delete(agentId);
|
|
360
393
|
}
|
|
361
394
|
addIndexedPath(agentId, filePath) {
|
|
362
395
|
const paths = this.pathsById.get(agentId) ?? new Set();
|
|
@@ -373,10 +406,53 @@ export class AgentStorage {
|
|
|
373
406
|
this.pathsById.delete(agentId);
|
|
374
407
|
}
|
|
375
408
|
}
|
|
376
|
-
|
|
377
|
-
|
|
409
|
+
}
|
|
410
|
+
/** Every `*.json` directly under `baseDir` or one project directory below it. */
|
|
411
|
+
export async function listAgentRecordFiles(baseDir) {
|
|
412
|
+
let entries = [];
|
|
413
|
+
try {
|
|
414
|
+
entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (error.code === "ENOENT") {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
const rootRecordPaths = entries
|
|
423
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
424
|
+
.map((entry) => path.join(baseDir, entry.name));
|
|
425
|
+
const projectDirs = entries
|
|
426
|
+
.filter((entry) => entry.isDirectory())
|
|
427
|
+
.map((entry) => path.join(baseDir, entry.name));
|
|
428
|
+
const projectFileLists = await Promise.all(projectDirs.map(async (projectDir) => {
|
|
429
|
+
try {
|
|
430
|
+
const files = await fs.readdir(projectDir, { withFileTypes: true });
|
|
431
|
+
return files
|
|
432
|
+
.filter((file) => file.isFile() && file.name.endsWith(".json"))
|
|
433
|
+
.map((file) => path.join(projectDir, file.name));
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}));
|
|
439
|
+
return [...rootRecordPaths, ...projectFileLists.flat()];
|
|
440
|
+
}
|
|
441
|
+
export async function readAgentRecordFile(filePath, logger) {
|
|
442
|
+
try {
|
|
443
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
444
|
+
const parsed = JSON.parse(content);
|
|
445
|
+
return parseStoredAgentRecord(parsed);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
logger.error({ err: error, filePath }, "Skipping invalid agent record");
|
|
449
|
+
return null;
|
|
378
450
|
}
|
|
379
451
|
}
|
|
452
|
+
export function agentRecordPath(baseDir, record) {
|
|
453
|
+
const projectDir = projectDirNameFromCwd(record.cwd);
|
|
454
|
+
return path.join(baseDir, projectDir, `${record.id}.json`);
|
|
455
|
+
}
|
|
380
456
|
function projectDirNameFromCwd(cwd) {
|
|
381
457
|
// path.win32.parse handles drive letters, UNC roots, and Unix roots on all platforms
|
|
382
458
|
const { root } = path.win32.parse(cwd);
|
|
@@ -166,6 +166,14 @@ const INVISIBLE_STRIP_GATE_RE = /removed\d+invisiblecharacters?·(?:(reviewandpr
|
|
|
166
166
|
const INVISIBLE_STRIP_SCAN_CHARS = 400;
|
|
167
167
|
/** How long after an Enter to keep looking for the gate before trusting the reaction. */
|
|
168
168
|
const INVISIBLE_STRIP_GATE_WAIT_MS = 1000;
|
|
169
|
+
/**
|
|
170
|
+
* Pause between seeing the gate and pressing Enter again. The CLI ignores an Enter that
|
|
171
|
+
* lands right after it paints the gate: live on 0.3.163 paseo pressed ~30ms later and
|
|
172
|
+
* nothing was sent for six attempts (session 977f4263, 2026-09-26). Measured against
|
|
173
|
+
* claude 2.1.283 in tmux: a second Enter 30ms after the gate submitted nothing; 100ms,
|
|
174
|
+
* 200ms, 300ms and 1.5s each submitted. 500ms is five times the smallest delay that worked.
|
|
175
|
+
*/
|
|
176
|
+
const INVISIBLE_STRIP_SETTLE_MS = 500;
|
|
169
177
|
/**
|
|
170
178
|
* Code points the CLI deletes before it records a prompt, so the transcript `user` record
|
|
171
179
|
* no longer contains them. Measured against claude 2.1.283 by pasting each candidate into a
|
|
@@ -948,6 +956,7 @@ export class PtyQuery {
|
|
|
948
956
|
await pollUntil(() => this.invisibleStripGateAt > enterAt, INVISIBLE_STRIP_GATE_WAIT_MS, 100);
|
|
949
957
|
if (this.invisibleStripGateAt > enterAt && !this.invisibleStripEmpty) {
|
|
950
958
|
this.recordLine("--- CLI stripped invisible characters from the answer; pressing Enter again ---");
|
|
959
|
+
await delay(INVISIBLE_STRIP_SETTLE_MS);
|
|
951
960
|
await this.transport.writeRaw?.("\r");
|
|
952
961
|
}
|
|
953
962
|
// Same reason as sendDialogAnswer: with more than one question the picker advances
|
|
@@ -1516,6 +1525,7 @@ export class PtyQuery {
|
|
|
1516
1525
|
}
|
|
1517
1526
|
this.recordLine("--- CLI stripped invisible characters and is waiting; pressing Enter again ---");
|
|
1518
1527
|
this.logger.info({ sessionId: this.sessionId }, "PtyQuery: CLI held the prompt after stripping invisible characters; confirming");
|
|
1528
|
+
await delay(INVISIBLE_STRIP_SETTLE_MS);
|
|
1519
1529
|
await this.transport.writeRaw?.("\r");
|
|
1520
1530
|
return "pressed";
|
|
1521
1531
|
}
|
|
@@ -148,6 +148,11 @@ export interface PaseoDaemon {
|
|
|
148
148
|
browserToolsBroker: BrowserToolsBroker;
|
|
149
149
|
start(): Promise<void>;
|
|
150
150
|
stop(): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* First thing on SIGTERM/SIGINT: commit queued state writes and start every
|
|
153
|
+
* pending legacy file write, before the slower service teardown.
|
|
154
|
+
*/
|
|
155
|
+
beginStateShutdown(): void;
|
|
151
156
|
getListenTarget(): ListenTarget | null;
|
|
152
157
|
}
|
|
153
158
|
/**
|