@logicseek/hello-cc 0.1.9 → 1.0.1
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 +136 -0
- package/README.md +50 -3
- package/README.zh-CN.md +48 -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/guide.md +97 -3
- package/docs/guide.zh-CN.md +89 -2
- package/lib/cli/app.mjs +644 -0
- package/lib/cli/commands/coordination.mjs +249 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +1002 -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 +76 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +249 -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 +222 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +48 -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 +63 -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 +377 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +250 -0
- package/lib/terminal/tmux.mjs +131 -27
- 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 +183 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +717 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +335 -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 +6 -3
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5404 -273
- package/docs/architecture-split-review.md +0 -217
- package/docs/layout-split-stack-audit.md +0 -298
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Up command extracted from bin/hcc.mjs.
|
|
2
|
+
// hcc up bootstraps the local coordination bus: guidance, provider hooks,
|
|
3
|
+
// optional shims, and provider-session discovery.
|
|
4
|
+
|
|
5
|
+
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
6
|
+
import { printResult } from '../../format.mjs';
|
|
7
|
+
import { commandPath } from '../../cli-runtime.mjs';
|
|
8
|
+
import { detectBranch } from '../../project-context.mjs';
|
|
9
|
+
import { bindingFromDetected } from '../../core/peers/bindings.mjs';
|
|
10
|
+
|
|
11
|
+
export function createUpCommand(deps) {
|
|
12
|
+
const {
|
|
13
|
+
connect, helpUp, PRODUCT_NAME,
|
|
14
|
+
loadSetup, loadDiscover,
|
|
15
|
+
writeGuidance, sameResolvedPath,
|
|
16
|
+
upsertPeer, upsertCanonicalPeerBinding
|
|
17
|
+
} = deps;
|
|
18
|
+
|
|
19
|
+
async function cmdUp(ctx, args) {
|
|
20
|
+
if (args[0] === '--help' || args[0] === '-h') return helpUp();
|
|
21
|
+
const opts = parseOpts(args, { booleans: ['no-guidance', 'no-discover'] });
|
|
22
|
+
validateOpts('up', opts, ['no-guidance', 'no-discover']);
|
|
23
|
+
const result = await prepareLocalBus(ctx, opts);
|
|
24
|
+
return printResult(ctx, result, (r) => {
|
|
25
|
+
const lines = [
|
|
26
|
+
`${PRODUCT_NAME} local coordination ready`,
|
|
27
|
+
`project: ${r.root}`,
|
|
28
|
+
`database: ${r.db}`
|
|
29
|
+
];
|
|
30
|
+
if (r.guidance) lines.push(`guidance: ${r.guidance}`);
|
|
31
|
+
if (r.hooks.claudeInstalled) lines.push('Claude Code hooks installed');
|
|
32
|
+
if (r.hooks.codexInstalled) lines.push('Codex hooks installed');
|
|
33
|
+
if (r.detected.length) lines.push(`detected: ${r.detected.map((s) => s.peerId).join(', ')}`);
|
|
34
|
+
if (r.warnings?.length) lines.push(...r.warnings.map((warning) => `warning: ${warning}`));
|
|
35
|
+
lines.push('web: run hcc web when you need browser terminal control');
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function prepareLocalBus(ctx, opts = {}) {
|
|
41
|
+
let guidance = null;
|
|
42
|
+
const db = connect(ctx);
|
|
43
|
+
try {
|
|
44
|
+
guidance = opts['no-guidance'] ? null : writeGuidance(ctx);
|
|
45
|
+
} finally {
|
|
46
|
+
db.close();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hooks = { claudeInstalled: false, codexInstalled: false };
|
|
50
|
+
const shims = { installed: [], skipped: [], pathUpdated: false, rcFile: null };
|
|
51
|
+
const warnings = [];
|
|
52
|
+
try {
|
|
53
|
+
const setup = await loadSetup();
|
|
54
|
+
try {
|
|
55
|
+
if (!setup.verifyClaudeHooks()) {
|
|
56
|
+
setup.installClaudeHooks(commandPath());
|
|
57
|
+
hooks.claudeInstalled = true;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
warnings.push(`Claude Code hooks installation failed: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (!setup.verifyCodexHooks()) {
|
|
64
|
+
setup.installCodexHooks(commandPath());
|
|
65
|
+
hooks.codexInstalled = true;
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
warnings.push(`Codex hooks installation failed: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
if (opts.installShims) {
|
|
71
|
+
try {
|
|
72
|
+
const result = setup.installShims(commandPath());
|
|
73
|
+
shims.installed = result.installed;
|
|
74
|
+
shims.skipped = result.skipped;
|
|
75
|
+
if (result.installed.length) {
|
|
76
|
+
const pathResult = setup.installPathEntry();
|
|
77
|
+
shims.pathUpdated = !pathResult.alreadyPresent;
|
|
78
|
+
shims.rcFile = pathResult.rcFile;
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
warnings.push(`shim installation failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
warnings.push(`local integration setup failed: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const detected = [];
|
|
89
|
+
if (!opts['no-discover']) {
|
|
90
|
+
try {
|
|
91
|
+
const { scanClaudeSessions, scanCodexSessions, scanProcesses } = await loadDiscover();
|
|
92
|
+
const found = [
|
|
93
|
+
...scanClaudeSessions(),
|
|
94
|
+
...scanCodexSessions(),
|
|
95
|
+
...scanProcesses(),
|
|
96
|
+
].filter((s) => sameResolvedPath(s.hccRoot, ctx.root));
|
|
97
|
+
const byId = new Map();
|
|
98
|
+
for (const s of found) {
|
|
99
|
+
if (!byId.has(s.peerId)) byId.set(s.peerId, s);
|
|
100
|
+
}
|
|
101
|
+
if (byId.size > 0) {
|
|
102
|
+
const db2 = connect(ctx);
|
|
103
|
+
try {
|
|
104
|
+
for (const s of byId.values()) {
|
|
105
|
+
detected.push(s);
|
|
106
|
+
upsertPeer(db2, {
|
|
107
|
+
id: s.peerId, kind: s.kind, role: 'peer',
|
|
108
|
+
worktree: s.cwd,
|
|
109
|
+
branch: detectBranch(s.cwd),
|
|
110
|
+
pid: s.pid,
|
|
111
|
+
status: 'running',
|
|
112
|
+
capabilities: 'detected'
|
|
113
|
+
});
|
|
114
|
+
upsertCanonicalPeerBinding(db2, bindingFromDetected(s, s.transport || 'detected'), true);
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
db2.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
root: ctx.root,
|
|
125
|
+
db: ctx.dbPath,
|
|
126
|
+
guidance,
|
|
127
|
+
hooks,
|
|
128
|
+
shims,
|
|
129
|
+
detected,
|
|
130
|
+
warnings
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { cmdUp, prepareLocalBus };
|
|
135
|
+
}
|
package/lib/cli-args.mjs
CHANGED
|
@@ -83,7 +83,21 @@ export function required(opts, key, envName = null) {
|
|
|
83
83
|
|
|
84
84
|
export function intOpt(opts, key, fallback = null) {
|
|
85
85
|
if (opts[key] === undefined || opts[key] === null || opts[key] === '') return fallback;
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
86
|
+
const text = String(opts[key]);
|
|
87
|
+
if (!/^-?[0-9]+$/.test(text)) {
|
|
88
|
+
throw new CliError('BAD_ARGS', `--${key} must be a safe integer`);
|
|
89
|
+
}
|
|
90
|
+
const value = Number(text);
|
|
91
|
+
if (!Number.isSafeInteger(value)) {
|
|
92
|
+
throw new CliError('BAD_ARGS', `--${key} must be a safe integer`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function positiveSafeIntOpt(opts, key, fallback = null) {
|
|
98
|
+
const value = intOpt(opts, key, fallback);
|
|
99
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
100
|
+
throw new CliError('BAD_ARGS', `--${key} must be a positive safe integer`);
|
|
101
|
+
}
|
|
88
102
|
return value;
|
|
89
103
|
}
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
} from './core/peers/liveness.mjs';
|
|
9
9
|
import { lockLabel } from './core/coordination/locks.mjs';
|
|
10
10
|
import { timelineFromRows } from './core/coordination/timeline.mjs';
|
|
11
|
+
import { readTx } from './db/schema.mjs';
|
|
12
|
+
import { clockGraceSuppressed, readClockGraceUntil } from './shared/clock-grace.mjs';
|
|
13
|
+
import { classifyPeerActivity } from './core/peers/evidence.mjs';
|
|
11
14
|
|
|
12
15
|
const DEFAULT_ACTIVE_PEER_TTL = 600;
|
|
13
16
|
const DEFAULT_LOCK_TTL = 900;
|
|
@@ -28,57 +31,97 @@ export function createCoordinationState(deps = {}) {
|
|
|
28
31
|
const connect = requireFn(deps.connect, 'connect');
|
|
29
32
|
const defaultLockTtl = Number(deps.defaultLockTtl ?? DEFAULT_LOCK_TTL);
|
|
30
33
|
const now = typeof deps.now === 'function' ? deps.now : defaultNow;
|
|
34
|
+
const observePeerEvidence = requireFn(deps.observePeerEvidence, 'observePeerEvidence');
|
|
31
35
|
const queryInbox = requireFn(deps.queryInbox, 'queryInbox');
|
|
32
36
|
const queryOpenTasks = requireFn(deps.queryOpenTasks, 'queryOpenTasks');
|
|
33
37
|
const queryTimelineMessages = requireFn(deps.queryTimelineMessages, 'queryTimelineMessages');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
function annotatePeerEvidence(ctx, peers, bindings, graceActive = false) {
|
|
39
|
+
const bindingByPeer = new Map();
|
|
40
|
+
for (const binding of bindings || []) {
|
|
41
|
+
if (!bindingByPeer.has(binding.peer)) bindingByPeer.set(binding.peer, binding);
|
|
42
|
+
}
|
|
43
|
+
return (peers || []).map((peer) => {
|
|
44
|
+
const evidence = observePeerEvidence(ctx, peer, bindingByPeer.get(peer.id) || null);
|
|
45
|
+
const { pid_start_token: _startToken, pid_command_hash: _commandHash, ...publicPeer } = peer;
|
|
46
|
+
const observed = {
|
|
47
|
+
...publicPeer,
|
|
48
|
+
evidence_state: evidence.state,
|
|
49
|
+
evidence_reason: evidence.reason
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
...observed,
|
|
53
|
+
...classifyPeerActivity(observed, { activePeerTtl, graceActive })
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function visibleLocks(allLocks, peers, t, graceActive) {
|
|
59
|
+
if (graceActive) return allLocks;
|
|
60
|
+
const peerById = new Map((peers || []).map((peer) => [peer.id, peer]));
|
|
61
|
+
return (allLocks || []).filter((lock) =>
|
|
62
|
+
Number(lock.expires_at) > t || peerById.get(lock.owner)?.evidence_state === 'live'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
37
65
|
|
|
38
66
|
function collectStateSnapshot(db, ctx, peer = null, opts = {}) {
|
|
39
67
|
const t = now();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
// Read every table inside one deferred transaction for a consistent
|
|
69
|
+
// snapshot (conc-06): without this, a heartbeat between reads can make
|
|
70
|
+
// counts drift and produce a transiently wrong takeover_ready.
|
|
71
|
+
const { peerRows, bindings, taskRows, allLocks, messages, timelineMessages, handoffs, events, graceUntil } = readTx(db, () => {
|
|
72
|
+
const peers = db.prepare(`
|
|
73
|
+
SELECT id, kind, role, status, worktree, branch, pid,
|
|
74
|
+
pid_start_token, pid_command_hash, capabilities,
|
|
75
|
+
created_at, last_seen_at, (? - last_seen_at) AS age_sec
|
|
76
|
+
FROM peers
|
|
77
|
+
ORDER BY last_seen_at DESC, id ASC
|
|
78
|
+
`).all(t);
|
|
79
|
+
const bindings = db.prepare(`
|
|
80
|
+
SELECT peer, transport, runtime_target, updated_at, created_at
|
|
81
|
+
FROM peer_bindings
|
|
82
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
83
|
+
`).all();
|
|
84
|
+
const taskRows = queryOpenTasks(db, 200);
|
|
85
|
+
const graceUntil = readClockGraceUntil(db);
|
|
86
|
+
const allLocks = db.prepare(`
|
|
87
|
+
SELECT resource, base_resource, scope, owner, task_id, reason, expires_at, created_at, ttl_sec
|
|
88
|
+
FROM locks
|
|
89
|
+
ORDER BY resource ASC
|
|
90
|
+
`).all();
|
|
91
|
+
const messages = peer
|
|
92
|
+
? queryInbox(db, peer, false, 50)
|
|
93
|
+
: db.prepare(`
|
|
94
|
+
SELECT id, sender, recipient, task_id, kind, body, reply_to, thread_id, created_at
|
|
95
|
+
FROM messages
|
|
96
|
+
ORDER BY id DESC
|
|
97
|
+
LIMIT 50
|
|
98
|
+
`).all().reverse();
|
|
99
|
+
const timelineMessages = queryTimelineMessages(db, peer, 80);
|
|
100
|
+
const handoffs = db.prepare(`
|
|
101
|
+
SELECT id, task_id, from_peer, to_peer, summary, changed_files, tests, risks, created_at
|
|
102
|
+
FROM handoffs
|
|
103
|
+
ORDER BY id DESC
|
|
104
|
+
LIMIT 50
|
|
105
|
+
`).all().reverse();
|
|
106
|
+
const events = db.prepare(`
|
|
107
|
+
SELECT id, type, actor, task_id, payload, created_at
|
|
108
|
+
FROM events
|
|
109
|
+
ORDER BY id DESC
|
|
110
|
+
LIMIT 80
|
|
111
|
+
`).all().reverse();
|
|
112
|
+
return { peerRows: peers, bindings, taskRows, allLocks, messages, timelineMessages, handoffs, events, graceUntil };
|
|
113
|
+
});
|
|
114
|
+
const graceActive = clockGraceSuppressed(t, graceUntil);
|
|
115
|
+
const peers = annotatePeerEvidence(ctx, peerRows, bindings, graceActive);
|
|
116
|
+
const locks = visibleLocks(allLocks, peers, t, graceActive);
|
|
117
|
+
const tasks = annotateTasksWithLiveness(taskRows, peers, locks, t, activePeerTtl, graceUntil);
|
|
77
118
|
const snapshot = {
|
|
78
119
|
root: ctx.root,
|
|
79
120
|
db: ctx.dbPath,
|
|
80
121
|
now: t,
|
|
81
122
|
active_peer_ttl: activePeerTtl,
|
|
123
|
+
clock_grace_active: graceActive,
|
|
124
|
+
clock_grace_until: graceUntil || null,
|
|
82
125
|
peers,
|
|
83
126
|
tasks,
|
|
84
127
|
locks,
|
|
@@ -114,8 +157,7 @@ export function createCoordinationState(deps = {}) {
|
|
|
114
157
|
if (peers.length > 0) {
|
|
115
158
|
parts.push('[hello-cc known peers]');
|
|
116
159
|
parts.push(...peers.map((peer) => {
|
|
117
|
-
const
|
|
118
|
-
const active = age <= activePeerTtl ? 'active' : 'stale';
|
|
160
|
+
const active = peer.active ? 'active' : 'stale';
|
|
119
161
|
return `- ${peer.id} ${peer.kind || 'other'} ${peer.status || 'idle'} ${active}`;
|
|
120
162
|
}));
|
|
121
163
|
} else {
|
|
@@ -169,24 +211,43 @@ export function createCoordinationState(deps = {}) {
|
|
|
169
211
|
}
|
|
170
212
|
}
|
|
171
213
|
|
|
172
|
-
function statusSummary(ctx, peer = null,
|
|
214
|
+
function statusSummary(ctx, peer = null, _identity = null) {
|
|
173
215
|
const db = connect(ctx);
|
|
174
216
|
try {
|
|
175
|
-
if (identity) touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
176
217
|
const t = now();
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
218
|
+
const { peerRows, bindings, taskRows, allLocks, unread, recent, graceUntil } = readTx(db, () => ({
|
|
219
|
+
peerRows: db.prepare(`
|
|
220
|
+
SELECT id, kind, role, status, worktree, branch, pid,
|
|
221
|
+
pid_start_token, pid_command_hash, capabilities,
|
|
222
|
+
created_at, last_seen_at, (? - last_seen_at) AS age_sec
|
|
223
|
+
FROM peers
|
|
224
|
+
ORDER BY last_seen_at DESC, id ASC
|
|
225
|
+
`).all(t),
|
|
226
|
+
bindings: db.prepare(`
|
|
227
|
+
SELECT peer, transport, runtime_target, updated_at, created_at
|
|
228
|
+
FROM peer_bindings
|
|
229
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
230
|
+
`).all(),
|
|
231
|
+
taskRows: db.prepare('SELECT status, COUNT(*) AS n FROM tasks GROUP BY status ORDER BY status').all(),
|
|
232
|
+
allLocks: db.prepare('SELECT * FROM locks').all(),
|
|
233
|
+
unread: peer ? queryInbox(db, peer, false, 1000).length : null,
|
|
234
|
+
recent: db.prepare('SELECT * FROM events ORDER BY id DESC LIMIT 8').all().reverse(),
|
|
235
|
+
graceUntil: readClockGraceUntil(db)
|
|
236
|
+
}));
|
|
237
|
+
const graceActive = clockGraceSuppressed(t, graceUntil);
|
|
238
|
+
const peers = annotatePeerEvidence(ctx, peerRows, bindings, graceActive);
|
|
239
|
+
const activePeers = peers.filter((row) => row.active).length;
|
|
240
|
+
const stalePeers = peers.length - activePeers;
|
|
241
|
+
const locks = visibleLocks(allLocks, peers, t, graceActive);
|
|
183
242
|
return {
|
|
184
243
|
root: ctx.root,
|
|
185
244
|
db: ctx.dbPath,
|
|
186
245
|
active_peers: activePeers,
|
|
187
246
|
stale_peers: stalePeers,
|
|
247
|
+
clock_grace_active: graceActive,
|
|
248
|
+
clock_grace_until: graceUntil || null,
|
|
188
249
|
tasks: taskRows,
|
|
189
|
-
active_locks: locks,
|
|
250
|
+
active_locks: locks.length,
|
|
190
251
|
unread,
|
|
191
252
|
recent_events: recent
|
|
192
253
|
};
|
|
@@ -13,15 +13,12 @@ import {
|
|
|
13
13
|
uniqueList
|
|
14
14
|
} from './timeline.mjs';
|
|
15
15
|
import { summarizeTask } from '../peers/liveness.mjs';
|
|
16
|
+
import { classifyPeerActivity } from '../peers/evidence.mjs';
|
|
16
17
|
|
|
17
18
|
const DEFAULT_CLI_NAME = 'hcc';
|
|
18
19
|
const DEFAULT_ACTIVE_PEER_TTL = 600;
|
|
19
20
|
const DEFAULT_LOCK_TTL = 900;
|
|
20
21
|
|
|
21
|
-
function nowSec() {
|
|
22
|
-
return Math.floor(Date.now() / 1000);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
22
|
export function actionCommand(argv, cliName = DEFAULT_CLI_NAME) {
|
|
26
23
|
if (!argv?.length) return '';
|
|
27
24
|
return [cliName, ...argv].map(shellQuoteArg).join(' ');
|
|
@@ -67,7 +64,7 @@ export function deriveAutomation(snapshot, peer = null, opts = {}, config = {})
|
|
|
67
64
|
makeAction(kind, argv, reason, mutates, extra, { cliName });
|
|
68
65
|
|
|
69
66
|
const peerId = peer || '';
|
|
70
|
-
const
|
|
67
|
+
const graceActive = Boolean(snapshot.clock_grace_active);
|
|
71
68
|
const peerRow = peerId ? snapshot.peers.find((row) => row.id === peerId) : null;
|
|
72
69
|
const openTasks = (snapshot.tasks || []).filter((task) => !['done', 'abandoned'].includes(task.status));
|
|
73
70
|
const assignedTasks = peerId ? openTasks.filter((task) => !task.owner && task.assignee === peerId) : [];
|
|
@@ -82,8 +79,8 @@ export function deriveAutomation(snapshot, peer = null, opts = {}, config = {})
|
|
|
82
79
|
const ownLocks = peerId ? (snapshot.locks || []).filter((lock) => lock.owner === peerId) : [];
|
|
83
80
|
const requestedLocks = resources.map((resource) => {
|
|
84
81
|
const requested = scopedLockResource(resource, scope);
|
|
85
|
-
const lock = (snapshot.locks || []).find((row) =>
|
|
86
|
-
const exact = (snapshot.locks || []).find((row) =>
|
|
82
|
+
const lock = (snapshot.locks || []).find((row) => locksConflict(row, requested));
|
|
83
|
+
const exact = (snapshot.locks || []).find((row) => lockBaseResource(row) === requested.base_resource && lockScope(row) === requested.scope);
|
|
87
84
|
return { ...requested, lock, exact };
|
|
88
85
|
});
|
|
89
86
|
const conflict = requestedLocks.find((entry) => entry.lock && entry.lock.owner !== peerId);
|
|
@@ -100,7 +97,7 @@ export function deriveAutomation(snapshot, peer = null, opts = {}, config = {})
|
|
|
100
97
|
peer: peerId ? {
|
|
101
98
|
id: peerId,
|
|
102
99
|
known: Boolean(peerRow),
|
|
103
|
-
active: Boolean(peerRow &&
|
|
100
|
+
active: Boolean(peerRow && classifyPeerActivity(peerRow, { activePeerTtl, graceActive }).active),
|
|
104
101
|
age_sec: peerRow ? Number(peerRow.age_sec || 0) : null
|
|
105
102
|
} : null,
|
|
106
103
|
current_task: summarizeTask(ownedTask),
|