agentel 0.2.6 → 0.3.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/src/sources.js CHANGED
@@ -15,7 +15,7 @@ const SOURCE_GROUPS = [
15
15
  sources: [
16
16
  { source: "claude", provider: "claude_code", label: "Claude Code CLI" },
17
17
  { source: "claude-code-desktop", provider: "claude_desktop", sourceType: "claude-code-desktop-metadata", label: "Claude Code Desktop" },
18
- { source: "claude-workspace", provider: "claude_desktop", sourceType: "claude-workspace-desktop", label: "Claude Workspace" },
18
+ { source: "claude-cowork", provider: "claude_desktop", sourceType: "claude-workspace-desktop", label: "Claude Cowork" },
19
19
  { source: "claude-web", provider: "claude_web", label: "Claude.ai" },
20
20
  { source: "claude-sdk", provider: "claude_sdk", label: "Claude SDK jobs" }
21
21
  ]
@@ -24,19 +24,42 @@ const SOURCE_GROUPS = [
24
24
  group: "Google",
25
25
  sources: [
26
26
  { source: "gemini-cli", provider: "gemini_cli", label: "Gemini CLI" },
27
- { source: "antigravity", provider: "antigravity", label: "Antigravity" }
27
+ { source: "antigravity-cli", provider: "antigravity_cli", sourceType: "antigravity-cli-transcript-log", label: "Antigravity CLI" },
28
+ { source: "antigravity", provider: "antigravity", label: "Antigravity 2.0" },
29
+ { source: "antigravity-ide", provider: "antigravity_ide", sourceType: "antigravity-ide-transcript-log", label: "Antigravity IDE" }
28
30
  ]
29
31
  },
30
32
  {
31
33
  group: "Cognition",
32
34
  sources: [
33
- { source: "devin-cli", provider: "devin", sourceType: "devin-cli-history", label: "Devin CLI" }
35
+ { source: "devin-cli", provider: "devin", sourceType: "devin-cli-history", label: "Devin CLI" },
36
+ { source: "devin-desktop", provider: "devin", sourceType: "devin-desktop-acp-events", label: "Devin Desktop" },
37
+ { source: "windsurf", provider: "windsurf", sourceType: "windsurf-cascade-brain", label: "Windsurf" }
38
+ ]
39
+ },
40
+ {
41
+ group: "GitHub",
42
+ sources: [
43
+ { source: "copilot-cli", provider: "copilot", sourceType: "copilot-cli-history", label: "GitHub Copilot CLI" }
44
+ ]
45
+ },
46
+ {
47
+ group: "Factory",
48
+ sources: [
49
+ { source: "factory", provider: "factory", sourceType: "factory-droid-history", label: "Factory Droid" }
50
+ ]
51
+ },
52
+ {
53
+ group: "xAI",
54
+ sources: [
55
+ { source: "grok-build", provider: "grok", sourceType: "grok-build-history", label: "Grok Build" }
34
56
  ]
35
57
  },
36
58
  {
37
59
  group: "Other",
38
60
  sources: [
39
61
  { source: "cursor", provider: "cursor", label: "Cursor" },
62
+ { source: "pi", provider: "pi", sourceType: "pi-cli-history", label: "pi" },
40
63
  { source: "cline", provider: "cline", sourceType: "cline-task-history", label: "Cline" },
41
64
  { source: "opencode-cli", provider: "opencode", sourceType: "opencode-cli-history", label: "OpenCode CLI" },
42
65
  { source: "opencode-desktop", provider: "opencode", sourceType: "opencode-desktop-history", label: "OpenCode Desktop" },
@@ -46,23 +69,28 @@ const SOURCE_GROUPS = [
46
69
  }
47
70
  ];
48
71
 
49
- const DISABLED_IMPORT_SOURCES = new Set(["windsurf"]);
72
+ const DISABLED_IMPORT_SOURCES = new Set();
50
73
 
51
- const DISABLED_IMPORT_SOURCE_MESSAGES = {
52
- windsurf:
53
- "Windsurf import is disabled because current Cascade transcripts are encrypted binary stores. agentlog can detect those sessions, but cannot archive readable conversation text yet."
54
- };
74
+ const DISABLED_IMPORT_SOURCE_MESSAGES = {};
55
75
 
56
76
  const IMPORT_SOURCE_ORDER = [
57
77
  "codex-cli",
58
78
  "codex-desktop",
59
79
  "claude",
60
80
  "claude-code-desktop",
61
- "claude-workspace",
81
+ "claude-cowork",
62
82
  "gemini-cli",
83
+ "antigravity-cli",
63
84
  "antigravity",
85
+ "antigravity-ide",
64
86
  "devin-cli",
87
+ "devin-desktop",
88
+ "windsurf",
89
+ "copilot-cli",
90
+ "factory",
91
+ "grok-build",
65
92
  "cursor",
93
+ "pi",
66
94
  "cline",
67
95
  "opencode-cli",
68
96
  "opencode-desktop",
@@ -70,17 +98,38 @@ const IMPORT_SOURCE_ORDER = [
70
98
  "aider"
71
99
  ];
72
100
 
101
+ const IMPORT_SOURCE_ALIASES = {
102
+ "claude-workspace": "claude-cowork",
103
+ claude_workspace: "claude-cowork",
104
+ claude_cowork: "claude-cowork"
105
+ };
106
+
73
107
  const HISTORY_PROVIDER_OPTIONS = SOURCE_GROUPS.flatMap((group) => group.sources);
74
108
 
109
+ function canonicalImportSource(source) {
110
+ const value = String(source || "").trim();
111
+ return IMPORT_SOURCE_ALIASES[value] || value;
112
+ }
113
+
75
114
  function enabledImportSources(sources) {
76
- return (sources || []).filter((source) => !DISABLED_IMPORT_SOURCES.has(source));
115
+ const seen = new Set();
116
+ const result = [];
117
+ for (const rawSource of sources || []) {
118
+ const source = canonicalImportSource(rawSource);
119
+ if (!source || seen.has(source) || DISABLED_IMPORT_SOURCES.has(source)) continue;
120
+ seen.add(source);
121
+ result.push(source);
122
+ }
123
+ return result;
77
124
  }
78
125
 
79
126
  function sourceLabel(source) {
80
- return HISTORY_PROVIDER_OPTIONS.find((item) => item.source === source)?.label || { opencode: "OpenCode" }[source] || source;
127
+ const canonical = canonicalImportSource(source);
128
+ return HISTORY_PROVIDER_OPTIONS.find((item) => item.source === canonical)?.label || { opencode: "OpenCode" }[canonical] || canonical;
81
129
  }
82
130
 
83
131
  module.exports = {
132
+ canonicalImportSource,
84
133
  DISABLED_IMPORT_SOURCE_MESSAGES,
85
134
  DISABLED_IMPORT_SOURCES,
86
135
  HISTORY_PROVIDER_OPTIONS,
package/src/supervisor.js CHANGED
@@ -6,12 +6,101 @@ const { spawn } = require("child_process");
6
6
  const { ensureBaseDirs, paths, readJson, writeJson } = require("./paths");
7
7
  const { effectiveImportSources, loadConfig } = require("./config");
8
8
  const { startCollector } = require("./collector");
9
+ const { startSourceWatchers } = require("./source-watch");
9
10
  const { hasRemoteTarget } = require("./sync");
10
11
 
11
12
  let tickRunning = false;
12
13
  const CHILD_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
13
14
  const CHILD_TIMEOUT_MS = 10 * 60 * 1000;
14
15
  const CHILD_KILL_GRACE_MS = 2000;
16
+ // Backoff so an idle machine doesn't spawn import/index children every 30s
17
+ // forever: after a few empty imports a source drops to a slower cadence and
18
+ // snaps back to every tick as soon as an import finds activity. Failing
19
+ // sources back off exponentially so a permanently broken source doesn't
20
+ // burn a child process (and a log line) per tick.
21
+ const SOURCE_IDLE_TICKS_BEFORE_BACKOFF = 4;
22
+ const SOURCE_IDLE_INTERVAL_MS = 5 * 60 * 1000;
23
+ const SOURCE_ERROR_BACKOFF_MIN_MS = 60 * 1000;
24
+ const SOURCE_ERROR_BACKOFF_MAX_MS = 30 * 60 * 1000;
25
+ // Sources covered by filesystem watchers don't need the fast poll at all:
26
+ // events mark them pending (imported on the next tick, triggered
27
+ // immediately), and this heartbeat poll only backstops dropped events —
28
+ // fs.watch loses events under load and emits nothing on network volumes.
29
+ const WATCHED_SOURCE_HEARTBEAT_MS = 15 * 60 * 1000;
30
+ const INDEX_MIN_INTERVAL_MS = 10 * 60 * 1000;
31
+ // Memory files change rarely and the backup is a stat+hash sweep, so a
32
+ // moderate cadence keeps provider memories continuously archived without
33
+ // meaningful cost.
34
+ const MEMORY_BACKUP_INTERVAL_MS = 15 * 60 * 1000;
35
+
36
+ // Per-run tick state. Owned by runSupervisorForeground so direct tick()
37
+ // callers (tests, one-shot runs) always start fresh.
38
+ function createTickState() {
39
+ return {
40
+ sources: new Map(),
41
+ lastIndexAt: 0,
42
+ indexDirty: true,
43
+ lastMemoryBackupAt: 0,
44
+ pendingSources: new Set(),
45
+ tickAgain: false,
46
+ watch: null
47
+ };
48
+ }
49
+
50
+ function sourceTickStateFor(state, source) {
51
+ let sourceState = state.sources.get(source);
52
+ if (!sourceState) {
53
+ sourceState = { nextEligibleAt: 0, idleTicks: 0, failures: 0, idleLogged: false };
54
+ state.sources.set(source, sourceState);
55
+ }
56
+ return sourceState;
57
+ }
58
+
59
+ // Returns true when the source just transitioned into idle backoff (so the
60
+ // caller can log the transition once instead of every tick).
61
+ function applySourceImportOutcome(sourceState, activity, now) {
62
+ sourceState.failures = 0;
63
+ if (activity > 0) {
64
+ sourceState.idleTicks = 0;
65
+ sourceState.nextEligibleAt = 0;
66
+ sourceState.idleLogged = false;
67
+ return false;
68
+ }
69
+ sourceState.idleTicks += 1;
70
+ if (sourceState.idleTicks < SOURCE_IDLE_TICKS_BEFORE_BACKOFF) return false;
71
+ sourceState.nextEligibleAt = now + SOURCE_IDLE_INTERVAL_MS;
72
+ const firstTime = !sourceState.idleLogged;
73
+ sourceState.idleLogged = true;
74
+ return firstTime;
75
+ }
76
+
77
+ // Watched sources skip idle backoff entirely: filesystem events snap them
78
+ // back via pendingSources, so after any import they rest until the heartbeat.
79
+ function applyWatchedSourceImportOutcome(sourceState, now) {
80
+ sourceState.failures = 0;
81
+ sourceState.idleTicks = 0;
82
+ sourceState.idleLogged = false;
83
+ sourceState.nextEligibleAt = now + WATCHED_SOURCE_HEARTBEAT_MS;
84
+ }
85
+
86
+ // Pending filesystem activity bypasses idle/heartbeat scheduling but not
87
+ // error backoff, so a chatty source that fails to import doesn't retry on
88
+ // every coalesce window.
89
+ function sourceEligibleForImport(sourceState, pending, now) {
90
+ if (now >= sourceState.nextEligibleAt) return true;
91
+ return pending && sourceState.failures === 0;
92
+ }
93
+
94
+ // Returns the backoff delay in ms applied after a failed import.
95
+ function applySourceImportFailure(sourceState, now) {
96
+ sourceState.failures += 1;
97
+ const backoffMs = Math.min(
98
+ SOURCE_ERROR_BACKOFF_MAX_MS,
99
+ SOURCE_ERROR_BACKOFF_MIN_MS * 2 ** (sourceState.failures - 1)
100
+ );
101
+ sourceState.nextEligibleAt = now + backoffMs;
102
+ return backoffMs;
103
+ }
15
104
 
16
105
  function startSupervisorDetached(env = process.env) {
17
106
  const p = paths(env);
@@ -37,57 +126,134 @@ async function runSupervisorForeground(env = process.env) {
37
126
  });
38
127
  if (collector) log(`collector listening on ${loadConfig(env).collector.otlpEndpoint}`, env);
39
128
 
129
+ const tickState = createTickState();
130
+
40
131
  let stopping = false;
41
132
  const stop = () => {
42
133
  if (stopping) return;
43
134
  stopping = true;
44
135
  log("supervisor stopping", env);
136
+ if (tickState.watch) tickState.watch.close();
45
137
  removeSupervisorPidIfOwned(process.pid, env);
46
138
  process.exit(0);
47
139
  };
48
140
  process.on("SIGTERM", stop);
49
141
  process.on("SIGINT", stop);
50
142
 
51
- await tick(env);
143
+ const runTick = () => {
144
+ if (stopping) return;
145
+ tick(env, tickState)
146
+ .catch((error) => log(`tick failed: ${error.message}`, env))
147
+ .finally(() => {
148
+ // Sources marked dirty while a tick ran get picked up right away
149
+ // instead of waiting for the interval.
150
+ if (tickState.tickAgain && !stopping) {
151
+ tickState.tickAgain = false;
152
+ runTick();
153
+ }
154
+ });
155
+ };
156
+
157
+ try {
158
+ const sources = effectiveImportSources(loadConfig(env));
159
+ tickState.watch = startSourceWatchers(
160
+ Array.isArray(sources) && sources.length ? sources : [],
161
+ env,
162
+ (source) => {
163
+ tickState.pendingSources.add(source);
164
+ if (tickRunning) {
165
+ tickState.tickAgain = true;
166
+ return;
167
+ }
168
+ runTick();
169
+ },
170
+ { onLog: (message) => log(message, env) }
171
+ );
172
+ const active = tickState.watch.activeWatchCount();
173
+ if (active) log(`watching ${active} source root(s) for changes`, env);
174
+ } catch (error) {
175
+ log(`source watch disabled: ${error.message}`, env);
176
+ }
177
+
178
+ await tick(env, tickState);
179
+ if (tickState.tickAgain) {
180
+ tickState.tickAgain = false;
181
+ runTick();
182
+ }
52
183
  const timer = setInterval(() => {
53
184
  if (tickRunning) {
54
185
  log("tick skipped: previous tick still running", env);
55
186
  return;
56
187
  }
57
- tick(env).catch((error) => log(`tick failed: ${error.message}`, env));
188
+ runTick();
58
189
  }, 30 * 1000);
59
190
 
60
191
  await new Promise(() => {});
61
192
  }
62
193
 
63
- async function tick(env) {
194
+ async function tick(env, state = createTickState()) {
64
195
  tickRunning = true;
65
196
  try {
197
+ const now = Date.now();
66
198
  try {
67
199
  const cfg = loadConfig(env);
68
200
  const sources = effectiveImportSources(cfg);
69
201
  const importSources = Array.isArray(sources) && sources.length ? sources : ["all"];
70
202
  for (const source of importSources) {
203
+ const sourceState = sourceTickStateFor(state, source);
204
+ const pending = state.pendingSources.has(source);
205
+ if (!sourceEligibleForImport(sourceState, pending, now)) continue;
206
+ state.pendingSources.delete(source);
71
207
  try {
72
208
  const imports = await importSourceInChild(source, cfg, env);
209
+ let activity = 0;
73
210
  for (const result of imports) {
211
+ activity += (Number(result.imported) || 0) + (Number(result.pruned) || 0);
74
212
  const summary = supervisorImportResultLog(result, source);
75
213
  if (summary) log(summary, env);
76
214
  if (result.errors?.length) log(`${result.provider} import errors from ${source}: ${result.errors.length}`, env);
77
215
  }
216
+ if (activity > 0) state.indexDirty = true;
217
+ if (state.watch && state.watch.isWatched(source)) {
218
+ applyWatchedSourceImportOutcome(sourceState, now);
219
+ } else if (applySourceImportOutcome(sourceState, activity, now)) {
220
+ log(`${source} import idle: slowing to ${formatDuration(SOURCE_IDLE_INTERVAL_MS)} cadence until new activity`, env);
221
+ }
78
222
  } catch (error) {
79
- log(`${source} history import skipped: ${error.message}`, env);
223
+ const backoffMs = applySourceImportFailure(sourceState, now);
224
+ log(`${source} history import skipped: ${error.message} (retrying in ${formatDuration(backoffMs)})`, env);
80
225
  }
81
226
  }
82
227
  } catch (error) {
83
228
  log(`history import skipped: ${error.message}`, env);
84
229
  }
85
230
 
86
- try {
87
- const result = await reindexInChild(env);
88
- if (result.index?.docCount != null) log(`index ready (${result.index.docCount} chunk(s))`, env);
89
- } catch (error) {
90
- log(`index skipped: ${error.message}`, env);
231
+ if (state.indexDirty || now - state.lastIndexAt >= INDEX_MIN_INTERVAL_MS) {
232
+ try {
233
+ const result = await reindexInChild(env);
234
+ if (result.index?.docCount != null) log(`index ready (${result.index.docCount} chunk(s))`, env);
235
+ } catch (error) {
236
+ log(`index skipped: ${error.message}`, env);
237
+ } finally {
238
+ // Advance the clock even on failure so a persistent indexing error
239
+ // backs off to INDEX_MIN_INTERVAL_MS instead of retrying every tick.
240
+ state.lastIndexAt = now;
241
+ state.indexDirty = false;
242
+ }
243
+ }
244
+
245
+ if (now - state.lastMemoryBackupAt >= MEMORY_BACKUP_INTERVAL_MS) {
246
+ try {
247
+ const summaries = await memoryBackupInChild(env);
248
+ const changed = summaries.reduce((sum, item) => sum + (Number(item.changed) || 0), 0);
249
+ if (changed > 0) log(`memory backup captured ${changed} change(s)`, env);
250
+ } catch (error) {
251
+ log(`memory backup skipped: ${error.message}`, env);
252
+ } finally {
253
+ // Advance even on failure so a persistent error backs off to the
254
+ // backup interval instead of retrying every tick.
255
+ state.lastMemoryBackupAt = now;
256
+ }
91
257
  }
92
258
 
93
259
  try {
@@ -124,6 +290,22 @@ try {
124
290
  return runJsonChild(script, env, `${source} import`, title);
125
291
  }
126
292
 
293
+ function memoryBackupInChild(env = process.env) {
294
+ const title = childProcessTitle("memory-backup");
295
+ const script = `
296
+ process.title = ${JSON.stringify(title)};
297
+ (async () => {
298
+ const { collectMemoryBackup } = require(${JSON.stringify(path.join(__dirname, "memory-sources"))});
299
+ const summaries = await collectMemoryBackup(process.env);
300
+ process.stdout.write(JSON.stringify(summaries));
301
+ })().catch((error) => {
302
+ console.error(error && error.message ? error.message : String(error));
303
+ process.exit(1);
304
+ });
305
+ `;
306
+ return runJsonChild(script, env, "memory backup", title);
307
+ }
308
+
127
309
  function reindexInChild(env = process.env) {
128
310
  const title = childProcessTitle("index");
129
311
  const script = `
@@ -350,6 +532,11 @@ function log(message, env = process.env) {
350
532
  }
351
533
 
352
534
  module.exports = {
535
+ applySourceImportFailure,
536
+ applySourceImportOutcome,
537
+ applyWatchedSourceImportOutcome,
538
+ createTickState,
539
+ sourceEligibleForImport,
353
540
  runSupervisorForeground,
354
541
  startSupervisorDetached,
355
542
  stopSupervisor,
@@ -359,6 +546,7 @@ module.exports = {
359
546
  removeSupervisorPidIfOwned,
360
547
  shouldRunScheduledSync,
361
548
  importSourceInChild,
549
+ memoryBackupInChild,
362
550
  reindexInChild,
363
551
  runJsonChild,
364
552
  syncInChild,
package/src/sync.js CHANGED
@@ -526,6 +526,7 @@ function listLocalArchiveObjects(env = process.env, prefix = "agentlog") {
526
526
  const stat = safeStat(file);
527
527
  if (!stat || !stat.isFile()) return;
528
528
  const relative = path.relative(root, file).split(path.sep).join("/");
529
+ if (!syncableArchiveObject(relative)) return;
529
530
  const key = [normalizePrefix(prefix), relative].filter(Boolean).join("/");
530
531
  objects.push({
531
532
  file,
@@ -538,6 +539,11 @@ function listLocalArchiveObjects(env = process.env, prefix = "agentlog") {
538
539
  return objects.sort((a, b) => a.key.localeCompare(b.key));
539
540
  }
540
541
 
542
+ function syncableArchiveObject(relative) {
543
+ const key = String(relative || "").replace(/\\/g, "/");
544
+ return !key.startsWith("indexes/");
545
+ }
546
+
541
547
  function listDirectoryObjects(root) {
542
548
  const objects = [];
543
549
  walk(root, (file) => {