gitdone-agent 0.7.4 → 0.7.5
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/index.js +61 -9
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from 'node:fs'
|
|
17
17
|
import { resolve, join } from 'node:path'
|
|
18
18
|
import { homedir, hostname, tmpdir } from 'node:os'
|
|
19
|
-
import { randomUUID } from 'node:crypto'
|
|
19
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
20
20
|
|
|
21
21
|
// ─── Stable agent dir, config + logging ────────────────────────────────────────
|
|
22
22
|
// Everything persistent lives here: a stable copy of the agent script (so
|
|
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
27
27
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
28
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
29
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.7.
|
|
30
|
+
const AGENT_VERSION = '0.7.5'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -1834,22 +1834,69 @@ async function readClaudeUsage() {
|
|
|
1834
1834
|
}
|
|
1835
1835
|
}
|
|
1836
1836
|
|
|
1837
|
+
// The discovered repo list barely ever changes, but re-upserting every repo's
|
|
1838
|
+
// row on the server each tick is pure idle write load at scale. So we ship the
|
|
1839
|
+
// full `repos` list only when the discovered set actually changed, or every
|
|
1840
|
+
// SYNC_FULL_TICKS as a reconciliation (so a repo the user "forgot" server-side
|
|
1841
|
+
// reappears within ~10 min, and a fresh row is recreated if one went missing).
|
|
1842
|
+
// In between we send a liveness-only sync (no repos) — the server still returns
|
|
1843
|
+
// the tracked list from the DB, so nothing downstream notices (gd-459).
|
|
1844
|
+
const SYNC_FULL_TICKS = 20
|
|
1845
|
+
let syncRepoSig = null
|
|
1846
|
+
let syncFullCountdown = 0
|
|
1847
|
+
|
|
1837
1848
|
async function sync(cfg, discovered) {
|
|
1838
1849
|
const usage = await readClaudeUsage()
|
|
1850
|
+
const sig = createHash('sha1').update(JSON.stringify(discovered)).digest('hex')
|
|
1851
|
+
const full = sig !== syncRepoSig || syncFullCountdown <= 0
|
|
1839
1852
|
const data = await api(cfg, '/api/v1/agent/sync', {
|
|
1840
1853
|
machineId: cfg.machineId,
|
|
1841
1854
|
hostname: cfg.hostname,
|
|
1842
1855
|
agentVersion: AGENT_VERSION,
|
|
1843
1856
|
roots: cfg.roots,
|
|
1844
|
-
repos: discovered,
|
|
1857
|
+
repos: full ? discovered : [],
|
|
1845
1858
|
...(usage ? { usage } : {}),
|
|
1846
1859
|
})
|
|
1860
|
+
if (full) { syncRepoSig = sig; syncFullCountdown = SYNC_FULL_TICKS }
|
|
1861
|
+
else syncFullCountdown--
|
|
1847
1862
|
return data.tracked ?? []
|
|
1848
1863
|
}
|
|
1849
1864
|
|
|
1850
|
-
//
|
|
1851
|
-
|
|
1865
|
+
// Per-repo cache of the last snapshot we actually SENT, so an idle repo whose
|
|
1866
|
+
// git state hasn't moved doesn't re-POST an identical snapshot every 30s tick
|
|
1867
|
+
// (gd-457). At scale that unchanged-snapshot flood is the dominant idle load on
|
|
1868
|
+
// the server; skipping it drops idle traffic by ~an order of magnitude. We still
|
|
1869
|
+
// resend at least every SNAPSHOT_HEARTBEAT_TICKS to refresh the repo's
|
|
1870
|
+
// lastSeenAt (used only for ordering — no tight online threshold reads it) and
|
|
1871
|
+
// as a last-ditch command-drain backstop. Kept long: the SSE stream is the fast
|
|
1872
|
+
// path and it reconnects every ~5 min (re-waking a drain), so the snapshot only
|
|
1873
|
+
// has to backstop the pathological "SSE totally dead" case (gd-459).
|
|
1874
|
+
const SNAPSHOT_HEARTBEAT_TICKS = 40
|
|
1875
|
+
const snapshotSigCache = new Map() // path → { sig, skipped }
|
|
1876
|
+
|
|
1877
|
+
// Cheap content signature of everything the server persists for a repo. Includes
|
|
1878
|
+
// diffs, so an edit to a working-tree file (which leaves modified/staged the
|
|
1879
|
+
// same but changes content) still counts as a change and is re-sent.
|
|
1880
|
+
function snapshotSignature(snapshot) {
|
|
1881
|
+
return createHash('sha1').update(JSON.stringify(snapshot)).digest('hex')
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// Push one tracked repo's snapshot and run any pending commands for it. `force`
|
|
1885
|
+
// bypasses the unchanged-skip — used right after a command mutates git state, so
|
|
1886
|
+
// the UI reflects it instantly regardless of the signature cache.
|
|
1887
|
+
async function pushSnapshot(cfg, repo, force = false) {
|
|
1852
1888
|
const snapshot = getSnapshot(repo.path)
|
|
1889
|
+
|
|
1890
|
+
// Skip the network POST (and all the server-side DB work it triggers) when the
|
|
1891
|
+
// repo is byte-for-byte unchanged since we last sent it — unless we're due a
|
|
1892
|
+
// heartbeat resend or the caller forced it (gd-457).
|
|
1893
|
+
const sig = snapshotSignature(snapshot)
|
|
1894
|
+
const cached = snapshotSigCache.get(repo.path)
|
|
1895
|
+
if (!force && cached && cached.sig === sig && cached.skipped < SNAPSHOT_HEARTBEAT_TICKS) {
|
|
1896
|
+
cached.skipped++
|
|
1897
|
+
return false // unchanged — nothing sent
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1853
1900
|
const data = await api(cfg, '/api/v1/agent/snapshot', {
|
|
1854
1901
|
machineId: cfg.machineId,
|
|
1855
1902
|
path: repo.path,
|
|
@@ -1859,6 +1906,10 @@ async function pushSnapshot(cfg, repo) {
|
|
|
1859
1906
|
hasGithubAuth: !!cfg.auth?.[repo.path],
|
|
1860
1907
|
...snapshot,
|
|
1861
1908
|
})
|
|
1909
|
+
// Sent successfully — remember this signature so identical follow-up ticks are
|
|
1910
|
+
// skipped until the next real change or heartbeat (gd-457). A thrown POST never
|
|
1911
|
+
// reaches here, so a failure just retries next tick.
|
|
1912
|
+
snapshotSigCache.set(repo.path, { sig, skipped: 0 })
|
|
1862
1913
|
// Server issued a (fresh) push token — cache it on disk so we ask only once.
|
|
1863
1914
|
if (data.githubAuth?.token && data.githubAuth?.repo) {
|
|
1864
1915
|
cfg.auth = cfg.auth ?? {}
|
|
@@ -1868,7 +1919,7 @@ async function pushSnapshot(cfg, repo) {
|
|
|
1868
1919
|
for (const cmd of data.commands ?? []) {
|
|
1869
1920
|
await executeCommand(cfg, cmd, repo.path)
|
|
1870
1921
|
}
|
|
1871
|
-
return
|
|
1922
|
+
return true // sent
|
|
1872
1923
|
}
|
|
1873
1924
|
|
|
1874
1925
|
// ─── Low-latency command channel (gd-274) ───────────────────────────────────────
|
|
@@ -1921,7 +1972,7 @@ async function drainCommands(cfg) {
|
|
|
1921
1972
|
if (GIT_STATE_CMDS.has(cmd.type)) touched.set(repoPath, cmd.repoName || repoPath)
|
|
1922
1973
|
}
|
|
1923
1974
|
for (const [path, name] of touched) {
|
|
1924
|
-
try { await pushSnapshot(cfg, { path, name }) }
|
|
1975
|
+
try { await pushSnapshot(cfg, { path, name }, true) }
|
|
1925
1976
|
catch (err) { log(`✗ post-command snapshot failed @ ${path}: ${err.message}`) }
|
|
1926
1977
|
}
|
|
1927
1978
|
} while (drainAgain)
|
|
@@ -1981,11 +2032,12 @@ async function runLoop(cfg) {
|
|
|
1981
2032
|
const discovered = scanRepos(cfg.roots)
|
|
1982
2033
|
const tracked = await sync(cfg, discovered)
|
|
1983
2034
|
let pushed = 0
|
|
2035
|
+
let skipped = 0
|
|
1984
2036
|
for (const repo of tracked) {
|
|
1985
|
-
try { await pushSnapshot(cfg, repo)
|
|
2037
|
+
try { (await pushSnapshot(cfg, repo)) ? pushed++ : skipped++ }
|
|
1986
2038
|
catch (err) { log(`✗ snapshot failed @ ${repo.path}: ${err.message}`) }
|
|
1987
2039
|
}
|
|
1988
|
-
log(`✓ tick — discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}`)
|
|
2040
|
+
log(`✓ tick — discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}, unchanged: ${skipped}`)
|
|
1989
2041
|
} catch (err) {
|
|
1990
2042
|
log(`✗ sync error: ${err.message}`)
|
|
1991
2043
|
}
|