@adhdev/daemon-core 0.9.82-rc.11 → 0.9.82-rc.111
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-adapters/provider-cli-adapter.d.ts +21 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.js +6001 -1225
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5967 -1212
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +26 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +176 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +45 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +453 -17
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +32 -10
- package/src/commands/chat-commands.ts +626 -20
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2820 -437
- package/src/config/chat-history.ts +37 -9
- package/src/config/mesh-config.ts +245 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/index.ts +30 -4
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +31 -8
- package/src/mesh/mesh-active-work.ts +255 -0
- package/src/mesh/mesh-events.ts +389 -47
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +356 -0
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +143 -18
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +50 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
|
@@ -43,6 +43,20 @@ type ReadChatPayload = {
|
|
|
43
43
|
[key: string]: unknown;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
new Promise<never>((_, reject) => {
|
|
52
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (timer) clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export class IdeProviderInstance implements ProviderInstance {
|
|
47
61
|
readonly type: string;
|
|
48
62
|
readonly category = 'ide' as const;
|
|
@@ -320,7 +334,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
320
334
|
if (webviewScript) {
|
|
321
335
|
const matchText = this.provider.webviewMatchText;
|
|
322
336
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
323
|
-
const webviewRaw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
|
|
337
|
+
const webviewRaw = await withTimeout(cdp.evaluateInWebviewFrame(webviewScript, matchFn), 30000, 'evaluateInWebviewFrame');
|
|
324
338
|
if (webviewRaw) {
|
|
325
339
|
raw = typeof webviewRaw === 'string' ? (() => { try { return JSON.parse(webviewRaw); } catch { return null; } })() : webviewRaw;
|
|
326
340
|
}
|
|
@@ -331,7 +345,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
331
345
|
if (!raw) {
|
|
332
346
|
const readChatScript = this.getReadChatScript();
|
|
333
347
|
if (!readChatScript) return;
|
|
334
|
-
raw = await cdp.evaluate(readChatScript, 30000);
|
|
348
|
+
raw = await withTimeout(cdp.evaluate(readChatScript, 30000), 30000, 'evaluate.readChatScript');
|
|
335
349
|
if (typeof raw === 'string') {
|
|
336
350
|
try { raw = JSON.parse(raw); } catch { return; }
|
|
337
351
|
}
|
|
@@ -706,7 +720,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
706
720
|
);
|
|
707
721
|
|
|
708
722
|
LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
|
|
709
|
-
let rawResult = await cdp.evaluate(script, 10000);
|
|
723
|
+
let rawResult = await withTimeout(cdp.evaluate(script, 10000), 10000, 'evaluate.autoApprove');
|
|
710
724
|
if (typeof rawResult === 'string') {
|
|
711
725
|
try { rawResult = JSON.parse(rawResult); } catch { }
|
|
712
726
|
}
|
|
@@ -1067,13 +1067,17 @@ export class ProviderLoader {
|
|
|
1067
1067
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
1068
1068
|
});
|
|
1069
1069
|
|
|
1070
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1070
1071
|
const handleChange = (filePath: string) => {
|
|
1071
1072
|
if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
|
|
1072
1073
|
return;
|
|
1073
1074
|
}
|
|
1074
1075
|
if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1076
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1077
|
+
reloadTimer = setTimeout(() => {
|
|
1078
|
+
this.log(`File changed: ${path.basename(filePath)}, reloading...`);
|
|
1079
|
+
this.reload();
|
|
1080
|
+
}, 300);
|
|
1077
1081
|
}
|
|
1078
1082
|
};
|
|
1079
1083
|
|
|
@@ -1130,7 +1134,9 @@ export class ProviderLoader {
|
|
|
1130
1134
|
return { updated: false };
|
|
1131
1135
|
}
|
|
1132
1136
|
const https = require('https') as typeof import('https');
|
|
1133
|
-
const {
|
|
1137
|
+
const { exec } = require('child_process') as typeof import('child_process');
|
|
1138
|
+
const { promisify } = require('util');
|
|
1139
|
+
const execAsync = promisify(exec);
|
|
1134
1140
|
|
|
1135
1141
|
const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
|
|
1136
1142
|
let prevEtag = '';
|
|
@@ -1207,7 +1213,7 @@ export class ProviderLoader {
|
|
|
1207
1213
|
|
|
1208
1214
|
// Extract
|
|
1209
1215
|
fs.mkdirSync(tmpExtract, { recursive: true });
|
|
1210
|
-
|
|
1216
|
+
await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
|
|
1211
1217
|
|
|
1212
1218
|
// Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
|
|
1213
1219
|
const extracted = fs.readdirSync(tmpExtract);
|
|
@@ -2,7 +2,7 @@ import type { MessagePart, ModalInfo, ReadChatResult } from './contracts.js'
|
|
|
2
2
|
import { normalizeMessageParts } from './contracts.js'
|
|
3
3
|
import type { ChatBubbleState, ChatMessage } from '../types.js'
|
|
4
4
|
|
|
5
|
-
const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'streaming', 'long_generating'] as const
|
|
5
|
+
const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'starting', 'streaming', 'long_generating'] as const
|
|
6
6
|
const VALID_ROLES = ['user', 'assistant', 'system', 'human'] as const
|
|
7
7
|
const VALID_BUBBLE_STATES = ['draft', 'streaming', 'final', 'removed'] as const
|
|
8
8
|
const VALID_TURN_STATUSES = ['open', 'waiting_approval', 'complete', 'error'] as const
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
|
-
|
|
15
|
+
// Removed execSync import
|
|
16
16
|
import { platform } from 'os';
|
|
17
17
|
import type { ProviderLoader } from './provider-loader.js';
|
|
18
18
|
import type { ProviderModule } from './contracts.js';
|
|
@@ -117,22 +117,40 @@ export class VersionArchive {
|
|
|
117
117
|
|
|
118
118
|
// ─── Version Detection ──────────────────────────────
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
import { exec } from 'child_process';
|
|
121
|
+
|
|
122
|
+
async function runCommand(cmd: string, timeout = 10000): Promise<string | null> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
exec(cmd, {
|
|
123
125
|
encoding: 'utf-8',
|
|
124
126
|
timeout,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
127
|
+
}, (error, stdout) => {
|
|
128
|
+
if (error) return resolve(null);
|
|
129
|
+
resolve(stdout.trim());
|
|
130
|
+
});
|
|
131
|
+
});
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
function findBinary(name: string): string | null {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
135
|
+
const isWin = platform() === 'win32';
|
|
136
|
+
const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
|
|
137
|
+
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
138
|
+
|
|
139
|
+
for (const p of paths) {
|
|
140
|
+
if (!p) continue;
|
|
141
|
+
for (const ext of exes) {
|
|
142
|
+
const fullPath = path.join(p, name + ext);
|
|
143
|
+
try {
|
|
144
|
+
if (fs.existsSync(fullPath)) {
|
|
145
|
+
const stat = fs.statSync(fullPath);
|
|
146
|
+
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
|
|
147
|
+
return fullPath;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch { }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
/** Extract version string from CLI output */
|
|
@@ -162,16 +180,16 @@ function getPlatformVersionCommand(
|
|
|
162
180
|
return undefined;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
function getVersion(binary: string, versionCommand?: string): string | null {
|
|
183
|
+
async function getVersion(binary: string, versionCommand?: string): Promise<string | null> {
|
|
166
184
|
// Custom version command from provider.json
|
|
167
185
|
if (versionCommand) {
|
|
168
|
-
const raw = runCommand(versionCommand);
|
|
186
|
+
const raw = await runCommand(versionCommand);
|
|
169
187
|
return raw ? parseVersion(raw) : null;
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
// Default: try --version, then -V, then -v
|
|
173
191
|
for (const flag of ['--version', '-V', '-v']) {
|
|
174
|
-
const raw = runCommand(`"${binary}" ${flag}`);
|
|
192
|
+
const raw = await runCommand(`"${binary}" ${flag}`);
|
|
175
193
|
if (raw && raw.length < 500) return parseVersion(raw);
|
|
176
194
|
}
|
|
177
195
|
return null;
|
|
@@ -191,11 +209,11 @@ function checkPathExists(paths: string[]): string | null {
|
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
/** macOS: Get app version from Info.plist */
|
|
194
|
-
function getMacAppVersion(appPath: string): string | null {
|
|
212
|
+
async function getMacAppVersion(appPath: string): Promise<string | null> {
|
|
195
213
|
if (platform() !== 'darwin' || !appPath.endsWith('.app')) return null;
|
|
196
214
|
const plistPath = path.join(appPath, 'Contents', 'Info.plist');
|
|
197
215
|
if (!fs.existsSync(plistPath)) return null;
|
|
198
|
-
const raw = runCommand(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${plistPath}"`);
|
|
216
|
+
const raw = await runCommand(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${plistPath}"`);
|
|
199
217
|
return raw || null;
|
|
200
218
|
}
|
|
201
219
|
|
|
@@ -242,10 +260,10 @@ export async function detectAllVersions(
|
|
|
242
260
|
|
|
243
261
|
// Version: try CLI first, then plist
|
|
244
262
|
if (resolvedBin) {
|
|
245
|
-
info.version = getVersion(resolvedBin, versionCommand);
|
|
263
|
+
info.version = await getVersion(resolvedBin, versionCommand);
|
|
246
264
|
}
|
|
247
265
|
if (!info.version && appPath) {
|
|
248
|
-
info.version = getMacAppVersion(appPath);
|
|
266
|
+
info.version = await getMacAppVersion(appPath);
|
|
249
267
|
}
|
|
250
268
|
|
|
251
269
|
} else if (provider.category === 'cli' || provider.category === 'acp') {
|
|
@@ -256,7 +274,7 @@ export async function detectAllVersions(
|
|
|
256
274
|
info.binary = binPath || null;
|
|
257
275
|
|
|
258
276
|
if (binPath) {
|
|
259
|
-
info.version = getVersion(binPath, versionCommand);
|
|
277
|
+
info.version = await getVersion(binPath, versionCommand);
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
} else if (provider.category === 'extension') {
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -23,11 +23,42 @@ export interface RepoMesh {
|
|
|
23
23
|
defaultBranch?: string;
|
|
24
24
|
policy: RepoMeshPolicy;
|
|
25
25
|
coordinator: RepoMeshCoordinatorConfig;
|
|
26
|
+
meshHost?: RepoMeshHostMetadata;
|
|
26
27
|
projectContext: ProjectContextSnapshot;
|
|
27
28
|
nodes: RepoMeshNode[];
|
|
28
29
|
status: 'active' | 'archived' | 'deleted';
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export type RepoMeshDaemonRole = 'host' | 'member';
|
|
33
|
+
|
|
34
|
+
export interface RepoMeshHostPairingMetadata {
|
|
35
|
+
status: 'not_configured' | 'pairing' | 'paired' | 'rejected' | 'revoked';
|
|
36
|
+
tokenId?: string;
|
|
37
|
+
joinedAt?: string;
|
|
38
|
+
lastPairedAt?: string;
|
|
39
|
+
lastRejectedAt?: string;
|
|
40
|
+
expiresAt?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RepoMeshHostMetadata {
|
|
44
|
+
/** Local daemon role for this mesh. Missing metadata defaults to host for standalone compatibility. */
|
|
45
|
+
role: RepoMeshDaemonRole;
|
|
46
|
+
/** Daemon that owns mesh truth/status/git/queue/session/ledger/coordinator ownership. */
|
|
47
|
+
hostDaemonId?: string;
|
|
48
|
+
/** Mesh node that represents the host daemon, when known. */
|
|
49
|
+
hostNodeId?: string;
|
|
50
|
+
/** Future standalone manual pairing endpoint entered by member daemons. */
|
|
51
|
+
hostAddress?: string;
|
|
52
|
+
/** Redacted pairing state only; raw join tokens must not be persisted here. */
|
|
53
|
+
pairing?: RepoMeshHostPairingMetadata;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RepoMeshHostStatus extends RepoMeshHostMetadata {
|
|
57
|
+
canOwnCoordinator: boolean;
|
|
58
|
+
canOwnQueue: boolean;
|
|
59
|
+
defaulted: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
export interface RepoMeshNode {
|
|
32
63
|
id: string;
|
|
33
64
|
daemonId: string;
|
|
@@ -42,6 +73,7 @@ export interface RepoMeshNode {
|
|
|
42
73
|
effectiveCapabilities: RepoMeshNodeCapabilities;
|
|
43
74
|
policy: RepoMeshNodePolicy;
|
|
44
75
|
health: RepoMeshNodeHealth;
|
|
76
|
+
role?: RepoMeshDaemonRole;
|
|
45
77
|
status: 'enabled' | 'disabled' | 'removed';
|
|
46
78
|
}
|
|
47
79
|
|
|
@@ -62,6 +94,13 @@ export interface RepoMeshPolicy {
|
|
|
62
94
|
requirePreTaskCheckpoint: boolean;
|
|
63
95
|
requirePostTaskCheckpoint: boolean;
|
|
64
96
|
requireApprovalForPush: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Narrow Refinery opt-in: when validation and patch-equivalence have passed,
|
|
99
|
+
* allow Refinery to publish submodule gitlink commits to each submodule's
|
|
100
|
+
* configured remote main branch with a non-force push, then verify reachability.
|
|
101
|
+
* Defaults to false; root branch pushes/merges are not affected.
|
|
102
|
+
*/
|
|
103
|
+
allowAutoPublishSubmoduleMainCommits?: boolean;
|
|
65
104
|
requireApprovalForDestructiveGit: boolean;
|
|
66
105
|
dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
|
|
67
106
|
maxParallelTasks: number;
|
|
@@ -126,6 +165,7 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
|
126
165
|
requirePreTaskCheckpoint: false,
|
|
127
166
|
requirePostTaskCheckpoint: true,
|
|
128
167
|
requireApprovalForPush: true,
|
|
168
|
+
allowAutoPublishSubmoduleMainCommits: false,
|
|
129
169
|
requireApprovalForDestructiveGit: true,
|
|
130
170
|
dirtyWorkspaceBehavior: 'warn',
|
|
131
171
|
maxParallelTasks: 2,
|
|
@@ -229,6 +269,7 @@ export interface LocalMeshEntry {
|
|
|
229
269
|
defaultBranch?: string;
|
|
230
270
|
policy: RepoMeshPolicy;
|
|
231
271
|
coordinator: RepoMeshCoordinatorConfig;
|
|
272
|
+
meshHost?: RepoMeshHostMetadata;
|
|
232
273
|
nodes: LocalMeshNodeEntry[];
|
|
233
274
|
createdAt: string;
|
|
234
275
|
updatedAt: string;
|
|
@@ -251,6 +292,7 @@ export interface LocalMeshNodeEntry {
|
|
|
251
292
|
clonedFromNodeId?: string;
|
|
252
293
|
/** Optional associated/external repos configured as node metadata. */
|
|
253
294
|
relatedRepos?: RepoMeshRelatedRepo[];
|
|
295
|
+
role?: RepoMeshDaemonRole;
|
|
254
296
|
}
|
|
255
297
|
|
|
256
298
|
// ─── Mesh Status (runtime, not persisted) ───────
|
|
@@ -261,6 +303,7 @@ export interface RepoMeshStatus {
|
|
|
261
303
|
repoIdentity: string;
|
|
262
304
|
defaultBranch?: string;
|
|
263
305
|
refreshedAt: string;
|
|
306
|
+
meshHost?: RepoMeshHostStatus;
|
|
264
307
|
nodes: RepoMeshNodeStatus[];
|
|
265
308
|
queue?: RepoMeshQueueStatus;
|
|
266
309
|
ledger?: RepoMeshLedgerStatus;
|
|
@@ -301,11 +344,18 @@ export interface RepoMeshNodeStatus {
|
|
|
301
344
|
repoRoot?: string;
|
|
302
345
|
daemonId?: string;
|
|
303
346
|
machineId?: string;
|
|
347
|
+
role?: RepoMeshDaemonRole;
|
|
304
348
|
machineStatus?: string;
|
|
305
349
|
isLocalWorktree?: boolean;
|
|
306
350
|
worktreeBranch?: string;
|
|
307
351
|
health: RepoMeshNodeHealth;
|
|
308
352
|
git?: GitRepoStatus;
|
|
353
|
+
/**
|
|
354
|
+
* True when the selected coordinator has evidence that a peer git probe is still
|
|
355
|
+
* in flight or just timed out during initial mesh handshake, so callers should
|
|
356
|
+
* treat missing git data as pending instead of authoritative absence.
|
|
357
|
+
*/
|
|
358
|
+
gitProbePending?: boolean;
|
|
309
359
|
providers: string[];
|
|
310
360
|
activeSessions: string[];
|
|
311
361
|
activeSessionDetails?: RepoMeshSessionStatus[];
|
package/src/status/reporter.ts
CHANGED
|
@@ -51,6 +51,8 @@ export class DaemonStatusReporter {
|
|
|
51
51
|
private lastStatusSentAt = 0;
|
|
52
52
|
private statusPendingThrottle = false;
|
|
53
53
|
private lastP2PStatusHash = '';
|
|
54
|
+
private lastP2PStatusSentAt: number = 0;
|
|
55
|
+
private p2pDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
56
|
private lastServerStatusHash = '';
|
|
55
57
|
private lastStatusSummary = '';
|
|
56
58
|
|
|
@@ -355,7 +357,20 @@ export class DaemonStatusReporter {
|
|
|
355
357
|
: { ...hashTarget, sessions };
|
|
356
358
|
const h = this.simpleHash(JSON.stringify(hashPayload));
|
|
357
359
|
if (h !== this.lastP2PStatusHash) {
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
// Rate limit: max 1 per 500ms
|
|
362
|
+
if (this.lastP2PStatusSentAt && now - this.lastP2PStatusSentAt < 500) {
|
|
363
|
+
if (!this.p2pDebounceTimer) {
|
|
364
|
+
this.p2pDebounceTimer = setTimeout(() => {
|
|
365
|
+
this.p2pDebounceTimer = null;
|
|
366
|
+
this.sendUnifiedStatusReport({ reason: 'p2p_debounce' });
|
|
367
|
+
}, 500);
|
|
368
|
+
}
|
|
369
|
+
return false; // Dropped for now, but will trigger later
|
|
370
|
+
}
|
|
371
|
+
|
|
358
372
|
this.lastP2PStatusHash = h;
|
|
373
|
+
this.lastP2PStatusSentAt = now;
|
|
359
374
|
this.deps.p2p?.sendStatus(payload);
|
|
360
375
|
return true;
|
|
361
376
|
}
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import * as os from 'os';
|
|
14
|
-
import {
|
|
14
|
+
import { exec } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
15
18
|
|
|
16
19
|
export interface HostMemorySnapshot {
|
|
17
20
|
totalMem: number;
|
|
@@ -21,19 +24,22 @@ export interface HostMemorySnapshot {
|
|
|
21
24
|
availableMem: number;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
let cachedDarwinAvail: number | null = null;
|
|
28
|
+
let darwinMemoryInterval: NodeJS.Timeout | null = null;
|
|
29
|
+
|
|
30
|
+
async function updateDarwinMemoryCache() {
|
|
31
|
+
if (os.platform() !== 'darwin') return;
|
|
26
32
|
try {
|
|
27
|
-
const
|
|
33
|
+
const { stdout } = await execAsync('vm_stat', {
|
|
28
34
|
encoding: 'utf-8',
|
|
29
35
|
timeout: 4000,
|
|
30
36
|
maxBuffer: 256 * 1024,
|
|
31
37
|
});
|
|
32
|
-
const pageSizeMatch =
|
|
38
|
+
const pageSizeMatch = stdout.match(/page size of (\d+)\s*bytes/i);
|
|
33
39
|
const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 4096;
|
|
34
40
|
|
|
35
41
|
const counts: Record<string, number> = {};
|
|
36
|
-
for (const line of
|
|
42
|
+
for (const line of stdout.split('\n')) {
|
|
37
43
|
const m = line.match(/^\s*Pages\s+([^:]+):\s+([\d,]+)\s*\.?/);
|
|
38
44
|
if (!m) continue;
|
|
39
45
|
const key = m[1].trim().toLowerCase().replace(/\s+/g, '_');
|
|
@@ -49,17 +55,28 @@ function parseDarwinAvailableBytes(totalMem: number): number | null {
|
|
|
49
55
|
|
|
50
56
|
const availPages = free + inactive + speculative + purgeable + fileBacked;
|
|
51
57
|
const bytes = availPages * pageSize;
|
|
52
|
-
|
|
53
|
-
return Math.min(bytes, totalMem);
|
|
58
|
+
cachedDarwinAvail = Number.isFinite(bytes) && bytes >= 0 ? Math.min(bytes, os.totalmem()) : null;
|
|
54
59
|
} catch {
|
|
55
|
-
|
|
60
|
+
// silently fallback
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export function getHostMemorySnapshot(): HostMemorySnapshot {
|
|
65
|
+
if (os.platform() === 'darwin' && !darwinMemoryInterval) {
|
|
66
|
+
updateDarwinMemoryCache();
|
|
67
|
+
darwinMemoryInterval = setInterval(updateDarwinMemoryCache, 3000);
|
|
68
|
+
darwinMemoryInterval.unref();
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
const totalMem = os.totalmem();
|
|
61
72
|
const freeMem = os.freemem();
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const availableMem = os.platform() === 'darwin'
|
|
74
|
+
? (cachedDarwinAvail ?? freeMem)
|
|
75
|
+
: freeMem;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
totalMem,
|
|
79
|
+
freeMem,
|
|
80
|
+
availableMem,
|
|
81
|
+
};
|
|
65
82
|
}
|