@logicseek/hello-cc 0.1.9 → 1.0.0
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/CHANGELOG.md +59 -0
- package/README.md +18 -3
- package/README.zh-CN.md +17 -3
- package/bin/hcc.mjs +3 -5656
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +13 -4
- package/docs/commands.zh-CN.md +13 -4
- package/docs/current-state-review-2026-08.md +341 -0
- package/docs/defect-review-2026-07.md +382 -0
- package/docs/guide.md +39 -3
- package/docs/guide.zh-CN.md +34 -2
- package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
- package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
- package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
- package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
- package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
- package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
- package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
- package/lib/cli/app.mjs +642 -0
- package/lib/cli/commands/coordination.mjs +247 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +852 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +61 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +233 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +13 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +111 -9
- package/lib/process/identity.mjs +192 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +47 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +60 -7
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +227 -27
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +370 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +201 -0
- package/lib/terminal/tmux.mjs +78 -4
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +135 -6
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +178 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +714 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +333 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +5 -2
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5397 -263
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Web cookie/session authentication helpers extracted from cmdWeb.
|
|
2
|
+
// Factory pattern: the webSessions Map and config are created by the factory
|
|
3
|
+
// and shared across all returned functions.
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export function createCookieAuth({
|
|
8
|
+
now,
|
|
9
|
+
ttlSec,
|
|
10
|
+
maxSessions,
|
|
11
|
+
requestIsSecure,
|
|
12
|
+
trustProxy,
|
|
13
|
+
proxyOrigin,
|
|
14
|
+
authOk,
|
|
15
|
+
token
|
|
16
|
+
}) {
|
|
17
|
+
const webSessions = new Map();
|
|
18
|
+
|
|
19
|
+
function parseCookieSid(req) {
|
|
20
|
+
const header = req.headers.cookie || '';
|
|
21
|
+
for (const part of header.split(';')) {
|
|
22
|
+
const [k, ...rest] = part.trim().split('=');
|
|
23
|
+
if (k === 'hcc_sid') {
|
|
24
|
+
const raw = rest.join('=');
|
|
25
|
+
if (raw.length > 128) return '';
|
|
26
|
+
try { return decodeURIComponent(raw); } catch { return ''; }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function closeWebSession(sid, reason = 'session revoked') {
|
|
33
|
+
const session = webSessions.get(sid);
|
|
34
|
+
webSessions.delete(sid);
|
|
35
|
+
for (const ws of session?.sockets || []) {
|
|
36
|
+
try { ws.close(4001, reason); } catch {}
|
|
37
|
+
}
|
|
38
|
+
session?.sockets?.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pruneWebSessions(t = now()) {
|
|
42
|
+
for (const [sid, session] of webSessions) {
|
|
43
|
+
if (session.expiresAt <= t) closeWebSession(sid, 'session expired');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function issueSession() {
|
|
48
|
+
pruneWebSessions();
|
|
49
|
+
while (webSessions.size >= maxSessions) {
|
|
50
|
+
const oldest = webSessions.keys().next().value;
|
|
51
|
+
if (!oldest) break;
|
|
52
|
+
closeWebSession(oldest, 'session limit reached');
|
|
53
|
+
}
|
|
54
|
+
const sid = randomBytes(24).toString('base64url');
|
|
55
|
+
webSessions.set(sid, { expiresAt: now() + ttlSec, sockets: new Set() });
|
|
56
|
+
return sid;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sessionCookieHeader(sid, req) {
|
|
60
|
+
const parts = [`hcc_sid=${sid}`, 'Path=/', 'HttpOnly', 'SameSite=Lax', `Max-Age=${ttlSec}`];
|
|
61
|
+
if (requestIsSecure(req, { trustProxy, proxyOrigin })) parts.push('Secure');
|
|
62
|
+
return parts.join('; ');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function expiredSessionCookieHeader(req) {
|
|
66
|
+
const parts = ['hcc_sid=', 'Path=/', 'HttpOnly', 'SameSite=Lax', 'Max-Age=0'];
|
|
67
|
+
if (requestIsSecure(req, { trustProxy, proxyOrigin })) parts.push('Secure');
|
|
68
|
+
return parts.join('; ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cookieSessionRecord(req) {
|
|
72
|
+
const sid = parseCookieSid(req);
|
|
73
|
+
if (!sid) return null;
|
|
74
|
+
const session = webSessions.get(sid);
|
|
75
|
+
if (!session || session.expiresAt <= now()) {
|
|
76
|
+
if (session) closeWebSession(sid, 'session expired');
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return { sid, session };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cookieSessionOk(req) {
|
|
83
|
+
return Boolean(cookieSessionRecord(req));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function cookieSocketValid(ws) {
|
|
87
|
+
const auth = ws?.hccCookieAuth;
|
|
88
|
+
if (!auth) return true;
|
|
89
|
+
const current = webSessions.get(auth.sid);
|
|
90
|
+
if (current === auth.session && current.expiresAt > now()) return true;
|
|
91
|
+
const reason = current === auth.session ? 'session expired' : 'session revoked';
|
|
92
|
+
if (current === auth.session) {
|
|
93
|
+
closeWebSession(auth.sid, reason);
|
|
94
|
+
} else {
|
|
95
|
+
auth.session.sockets.delete(ws);
|
|
96
|
+
try { ws.close(4001, reason); } catch {}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function webAuthMode(url, req) {
|
|
102
|
+
if (authOk(url, req, token)) return 'token';
|
|
103
|
+
return cookieSessionOk(req) ? 'cookie' : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
webSessions,
|
|
108
|
+
parseCookieSid,
|
|
109
|
+
closeWebSession,
|
|
110
|
+
pruneWebSessions,
|
|
111
|
+
issueSession,
|
|
112
|
+
sessionCookieHeader,
|
|
113
|
+
expiredSessionCookieHeader,
|
|
114
|
+
cookieSessionRecord,
|
|
115
|
+
cookieSessionOk,
|
|
116
|
+
cookieSocketValid,
|
|
117
|
+
webAuthMode
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// External buffer-file session adoption, extracted from lib/web/runtime-main.mjs.
|
|
2
|
+
// Adopts hcc-run PTY bridges from .hello-cc/bufs/<id>.* files: owner-evidence
|
|
3
|
+
// checks under the producer lease, output polling, exit reconciliation, and
|
|
4
|
+
// per-project directory watchers.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { withBufferDirectoryLease } from '../runtime/buffer-directory-lease.mjs';
|
|
9
|
+
import {
|
|
10
|
+
externalBufferEvidence,
|
|
11
|
+
externalBufferOwnerKey,
|
|
12
|
+
externalBufferSessionIds,
|
|
13
|
+
readExternalBufferMetadata
|
|
14
|
+
} from '../runtime/buffer-evidence.mjs';
|
|
15
|
+
import { inspectProcessIdentity } from '../process/identity.mjs';
|
|
16
|
+
|
|
17
|
+
export function createExternalSessions(deps) {
|
|
18
|
+
const {
|
|
19
|
+
ctx, sessions,
|
|
20
|
+
sessionKey, broadcast,
|
|
21
|
+
now, tx, connectWebProject,
|
|
22
|
+
bufferDirectory, runtimeProjectContexts,
|
|
23
|
+
refreshPeerIoHeartbeat, redactedLogText, BUFS_DIR_NAME
|
|
24
|
+
} = deps;
|
|
25
|
+
|
|
26
|
+
// ── Optional external buffer-file session adoption ───────────────────────
|
|
27
|
+
const bufsDir = path.join(ctx.root, '.hello-cc', BUFS_DIR_NAME);
|
|
28
|
+
fs.mkdirSync(bufsDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
function removeExternalBufferFiles(id, directory = bufsDir) {
|
|
31
|
+
for (const suffix of ['out', 'in', 'resize', 'meta']) {
|
|
32
|
+
fs.rmSync(path.join(directory, `${id}.${suffix}`), { force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function adoptExternalSession(id, pctx = ctx, directory = bufsDir) {
|
|
37
|
+
const key = sessionKey(pctx, id);
|
|
38
|
+
if (sessions.has(key)) return;
|
|
39
|
+
const outFile = path.join(directory, `${id}.out`);
|
|
40
|
+
const inFile = path.join(directory, `${id}.in`);
|
|
41
|
+
const resizeFile = path.join(directory, `${id}.resize`);
|
|
42
|
+
const metaFile = path.join(directory, `${id}.meta`);
|
|
43
|
+
let adopted = null;
|
|
44
|
+
try {
|
|
45
|
+
adopted = withBufferDirectoryLease(directory, () => {
|
|
46
|
+
if (!fs.existsSync(outFile) || !fs.existsSync(metaFile)) return null;
|
|
47
|
+
let meta;
|
|
48
|
+
try { meta = readExternalBufferMetadata(metaFile); } catch { return null; }
|
|
49
|
+
// The producer publishes wrapper evidence before the PTY child identity
|
|
50
|
+
// is complete. Do not cache that transitional snapshot as a session;
|
|
51
|
+
// the next scan adopts the final metadata instead.
|
|
52
|
+
if (meta.publishing === true) return null;
|
|
53
|
+
const ownerKey = externalBufferOwnerKey(meta);
|
|
54
|
+
if (!ownerKey) return null;
|
|
55
|
+
const evidence = externalBufferEvidence(meta, inspectProcessIdentity);
|
|
56
|
+
// Re-read and remove in one producer-coordinated lease. A new producer
|
|
57
|
+
// with the same id cannot publish between this decision and deletion.
|
|
58
|
+
if (evidence.state === 'dead') {
|
|
59
|
+
removeExternalBufferFiles(id, directory);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return { meta, ownerKey };
|
|
63
|
+
});
|
|
64
|
+
} catch { return; }
|
|
65
|
+
if (!adopted) return;
|
|
66
|
+
const { meta, ownerKey } = adopted;
|
|
67
|
+
const wrapperOwnerPid = meta.wrapper_pid || meta.wrapperPid || null;
|
|
68
|
+
const wrapperOwnerIdentity = meta.wrapper_identity || meta.wrapperIdentity || null;
|
|
69
|
+
const dbOwnerPid = wrapperOwnerPid || meta.pid || null;
|
|
70
|
+
const dbOwnerIdentity = wrapperOwnerPid
|
|
71
|
+
? wrapperOwnerIdentity
|
|
72
|
+
: meta.child_identity || meta.childIdentity || null;
|
|
73
|
+
|
|
74
|
+
const session = {
|
|
75
|
+
id,
|
|
76
|
+
peerId: id,
|
|
77
|
+
actionTokens: new Set(),
|
|
78
|
+
root: pctx.root,
|
|
79
|
+
ctx: pctx,
|
|
80
|
+
kind: meta.kind || 'external',
|
|
81
|
+
role: meta.role || 'peer',
|
|
82
|
+
command: meta.command || '(shim)',
|
|
83
|
+
cwd: meta.cwd || pctx.root,
|
|
84
|
+
pid: meta.pid || null,
|
|
85
|
+
wrapperPid: meta.wrapper_pid || null,
|
|
86
|
+
childIdentity: meta.child_identity || null,
|
|
87
|
+
wrapperIdentity: meta.wrapper_identity || null,
|
|
88
|
+
externalOwnerKey: ownerKey,
|
|
89
|
+
externalDbOwner: Number.isInteger(Number(dbOwnerPid)) && dbOwnerIdentity?.startToken &&
|
|
90
|
+
dbOwnerIdentity?.commandHash
|
|
91
|
+
? {
|
|
92
|
+
pid: Number(dbOwnerPid),
|
|
93
|
+
startToken: dbOwnerIdentity.startToken,
|
|
94
|
+
commandHash: dbOwnerIdentity.commandHash
|
|
95
|
+
}
|
|
96
|
+
: null,
|
|
97
|
+
type: 'external',
|
|
98
|
+
outFile, inFile, resizeFile, metaFile,
|
|
99
|
+
status: 'running',
|
|
100
|
+
createdAt: now(),
|
|
101
|
+
exitedAt: null,
|
|
102
|
+
buffer: '',
|
|
103
|
+
clients: new Set(),
|
|
104
|
+
outputPoller: null,
|
|
105
|
+
outputFd: null,
|
|
106
|
+
exitPoller: null,
|
|
107
|
+
};
|
|
108
|
+
// Load existing output as initial snapshot
|
|
109
|
+
try { session.buffer = fs.readFileSync(outFile, 'utf8'); } catch {}
|
|
110
|
+
sessions.set(key, session);
|
|
111
|
+
|
|
112
|
+
// Open a persistent fd for polling output; fstatSync is cheap.
|
|
113
|
+
let outputOffset = 0;
|
|
114
|
+
try {
|
|
115
|
+
session.outputFd = fs.openSync(outFile, 'r');
|
|
116
|
+
outputOffset = fs.fstatSync(session.outputFd).size;
|
|
117
|
+
} catch {}
|
|
118
|
+
session.outputPoller = setInterval(() => {
|
|
119
|
+
try {
|
|
120
|
+
if (session.outputFd === null) return;
|
|
121
|
+
const stat = fs.fstatSync(session.outputFd);
|
|
122
|
+
if (stat.size < outputOffset) outputOffset = 0;
|
|
123
|
+
if (stat.size <= outputOffset) return;
|
|
124
|
+
const buf = Buffer.alloc(stat.size - outputOffset);
|
|
125
|
+
fs.readSync(session.outputFd, buf, 0, buf.length, outputOffset);
|
|
126
|
+
outputOffset = stat.size;
|
|
127
|
+
const data = buf.toString();
|
|
128
|
+
session.buffer += data;
|
|
129
|
+
if (session.buffer.length > 250000) session.buffer = session.buffer.slice(-200000);
|
|
130
|
+
broadcast(session, { type: 'data', data });
|
|
131
|
+
refreshPeerIoHeartbeat(session);
|
|
132
|
+
} catch {
|
|
133
|
+
// File removed or truncated — close and stop polling
|
|
134
|
+
if (session.outputFd) { try { fs.closeSync(session.outputFd); } catch {} session.outputFd = null; }
|
|
135
|
+
}
|
|
136
|
+
}, 100);
|
|
137
|
+
|
|
138
|
+
function finalizeExternalSession({ updateDatabase }) {
|
|
139
|
+
session.status = 'exited';
|
|
140
|
+
session.exitedAt = now();
|
|
141
|
+
broadcast(session, { type: 'exit', event: {} });
|
|
142
|
+
if (updateDatabase) {
|
|
143
|
+
try {
|
|
144
|
+
const exitDb = connectWebProject(session.ctx || ctx);
|
|
145
|
+
try {
|
|
146
|
+
const exitPeerId = session.peerId || session.id;
|
|
147
|
+
if (session.externalDbOwner) {
|
|
148
|
+
tx(exitDb, () => {
|
|
149
|
+
const mutation = exitDb.prepare(`
|
|
150
|
+
UPDATE peers SET status = ?
|
|
151
|
+
WHERE id = ? AND pid = ? AND pid_start_token = ? AND pid_command_hash = ?
|
|
152
|
+
`).run(
|
|
153
|
+
'exited',
|
|
154
|
+
exitPeerId,
|
|
155
|
+
session.externalDbOwner.pid,
|
|
156
|
+
session.externalDbOwner.startToken,
|
|
157
|
+
session.externalDbOwner.commandHash
|
|
158
|
+
);
|
|
159
|
+
if (Number(mutation.changes || 0) > 0) {
|
|
160
|
+
exitDb.prepare('UPDATE peer_bindings SET runtime_target = NULL, updated_at = ? WHERE peer = ?')
|
|
161
|
+
.run(now(), exitPeerId);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
} finally { exitDb.close(); }
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
if (session.outputFd) { try { fs.closeSync(session.outputFd); } catch {} session.outputFd = null; }
|
|
169
|
+
if (session.outputPoller) clearInterval(session.outputPoller);
|
|
170
|
+
if (session.exitPoller) clearInterval(session.exitPoller);
|
|
171
|
+
sessions.delete(key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reconcile and mutate the external group under the same lease used by its
|
|
175
|
+
// producer. Owner changes detach only this stale in-memory view; they never
|
|
176
|
+
// mark the replacement producer exited or remove its files.
|
|
177
|
+
session.exitPoller = setInterval(() => {
|
|
178
|
+
try {
|
|
179
|
+
withBufferDirectoryLease(directory, () => {
|
|
180
|
+
const outExists = fs.existsSync(outFile);
|
|
181
|
+
let currentMeta;
|
|
182
|
+
try {
|
|
183
|
+
currentMeta = readExternalBufferMetadata(metaFile);
|
|
184
|
+
} catch {
|
|
185
|
+
// A producer removes the whole group in one lease. No out and no
|
|
186
|
+
// metadata is therefore a clean exit; an unreadable metadata file
|
|
187
|
+
// is unknown and must not mutate DB ownership.
|
|
188
|
+
if (!outExists && !fs.existsSync(metaFile)) {
|
|
189
|
+
finalizeExternalSession({ updateDatabase: true });
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const currentOwnerKey = externalBufferOwnerKey(currentMeta);
|
|
194
|
+
if (!currentOwnerKey) return;
|
|
195
|
+
if (currentOwnerKey !== session.externalOwnerKey) {
|
|
196
|
+
finalizeExternalSession({ updateDatabase: false });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!outExists) {
|
|
200
|
+
finalizeExternalSession({ updateDatabase: true });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (externalBufferEvidence(currentMeta, inspectProcessIdentity).state !== 'dead') return;
|
|
204
|
+
removeExternalBufferFiles(id, directory);
|
|
205
|
+
finalizeExternalSession({ updateDatabase: true });
|
|
206
|
+
});
|
|
207
|
+
} catch {}
|
|
208
|
+
}, 2000);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Adopt any already-running external sessions
|
|
212
|
+
function scanExternalSessions() {
|
|
213
|
+
for (const projectCtx of runtimeProjectContexts()) {
|
|
214
|
+
const directory = bufferDirectory(projectCtx);
|
|
215
|
+
for (const id of externalBufferSessionIds(directory)) {
|
|
216
|
+
adoptExternalSession(id, projectCtx, directory);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
scanExternalSessions();
|
|
222
|
+
|
|
223
|
+
const externalScanPoller = setInterval(scanExternalSessions, 1000);
|
|
224
|
+
|
|
225
|
+
// bg-04: watch for new external sessions appearing in EVERY registered
|
|
226
|
+
// project's bufsDir, not just the primary. New projects discovered by
|
|
227
|
+
// scanExternalSessions get their own watcher on the next scan tick.
|
|
228
|
+
const bufsWatchers = new Map();
|
|
229
|
+
function ensureBufsWatchers() {
|
|
230
|
+
for (const projectCtx of runtimeProjectContexts()) {
|
|
231
|
+
const root = path.resolve(projectCtx.root);
|
|
232
|
+
if (bufsWatchers.has(root)) continue;
|
|
233
|
+
const directory = bufferDirectory(projectCtx);
|
|
234
|
+
try {
|
|
235
|
+
const watcher = fs.watch(directory, { persistent: false }, (event, filename) => {
|
|
236
|
+
if (filename?.endsWith('.out')) {
|
|
237
|
+
setTimeout(() => adoptExternalSession(path.basename(filename, '.out'), projectCtx, directory), 300);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
watcher.on('error', (err) => {
|
|
241
|
+
console.error(redactedLogText(`[${new Date().toISOString()}] bufs watcher error for ${root}: ${err?.message || err}`));
|
|
242
|
+
});
|
|
243
|
+
bufsWatchers.set(root, watcher);
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ensureBufsWatchers();
|
|
248
|
+
const bufsWatcherSyncPoller = setInterval(ensureBufsWatchers, 5000);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
bufsWatchers, externalScanPoller, bufsWatcherSyncPoller,
|
|
252
|
+
scanExternalSessions, adoptExternalSession, removeExternalBufferFiles
|
|
253
|
+
};
|
|
254
|
+
}
|