@gaia-ai/addon-claude 0.12.0 → 0.13.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.
@@ -1,4 +1,4 @@
1
- import type { AgentFootprint, AgentPlugin, GaiaAgent } from '@gaia-ai/conductor/contract';
1
+ import type { AgentFootprint, AgentPlugin, AgentRunIdentity, GaiaAgent } from '@gaia-ai/conductor/contract';
2
2
  export { parseClaudeTranscript } from './footprint.js';
3
3
  /**
4
4
  * Encode an absolute cwd the way Claude Code names its per-project transcript
@@ -13,8 +13,16 @@ export declare class ClaudeAgent implements GaiaAgent {
13
13
  private readonly home;
14
14
  readonly id = "claude";
15
15
  constructor(options: ClaudeAgentOptions, home?: string);
16
- launchCommand(prompt: string): string;
17
- getRunLog(worktreePath: string): Promise<string>;
16
+ launchCommand(prompt: string, run?: AgentRunIdentity): string;
17
+ /**
18
+ * `claude --session-id <uuid>` refuses to start when that session already
19
+ * exists (GAIA-473 review round 2), so the conductor checks this before
20
+ * pinning the run's own UUID — a re-dispatch of the same run would
21
+ * otherwise mint the very id its first attempt already wrote and fail to
22
+ * launch at all, silently (`claude` exits 0 on that error).
23
+ */
24
+ sessionExists(worktreePath: string, sessionId: string): Promise<boolean>;
25
+ getRunLog(worktreePath: string, run?: AgentRunIdentity): Promise<string>;
18
26
  parseFootprint(log: string): AgentFootprint;
19
27
  }
20
28
  /** Config-facing factory: `claudeAgent({ model: 'claude-opus-4-8' })`. */
package/dist/src/index.js CHANGED
@@ -19,7 +19,7 @@ export class ClaudeAgent {
19
19
  this.options = options;
20
20
  this.home = home;
21
21
  }
22
- launchCommand(prompt) {
22
+ launchCommand(prompt, run) {
23
23
  if (prompt === '') {
24
24
  return 'claude';
25
25
  }
@@ -28,10 +28,85 @@ export class ClaudeAgent {
28
28
  parts.push('--model', this.options.model);
29
29
  }
30
30
  parts.push('--dangerously-skip-permissions');
31
+ // Pin the transcript's own session id to the run's UUID (GAIA-473), so
32
+ // `getRunLog` can read that ONE file directly instead of scanning the
33
+ // worktree-keyed directory by mtime — the scan is what let a
34
+ // resumed/re-dispatched/reused worktree finalise a foreign transcript.
35
+ if (run?.sessionId) {
36
+ parts.push('--session-id', run.sessionId);
37
+ }
31
38
  return parts.join(' ');
32
39
  }
33
- async getRunLog(worktreePath) {
40
+ /**
41
+ * `claude --session-id <uuid>` refuses to start when that session already
42
+ * exists (GAIA-473 review round 2), so the conductor checks this before
43
+ * pinning the run's own UUID — a re-dispatch of the same run would
44
+ * otherwise mint the very id its first attempt already wrote and fail to
45
+ * launch at all, silently (`claude` exits 0 on that error).
46
+ */
47
+ async sessionExists(worktreePath, sessionId) {
34
48
  const dir = join(this.home, '.claude', 'projects', encodeClaudeProjectDir(worktreePath));
49
+ try {
50
+ await stat(join(dir, `${sessionId}.jsonl`));
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ async getRunLog(worktreePath, run) {
58
+ const dir = join(this.home, '.claude', 'projects', encodeClaudeProjectDir(worktreePath));
59
+ const startedAtMs = run?.startedAt !== undefined ? new Date(run.startedAt).getTime() : null;
60
+ // GAIA-473: when the run's own session id is known (pinned at launch via
61
+ // `--session-id`), read that file directly — no directory scan, so a
62
+ // foreign transcript (resumed run, re-dispatch, reused worktree) can
63
+ // never be selected. AC-5: when the pinned file is missing (a run
64
+ // dispatched before this change, or the agent never wrote it), fall
65
+ // through to the scan below — bounded by `run.startedAt` where known —
66
+ // instead of returning '' and turning a wrong log into no log at all.
67
+ //
68
+ // Round 4 (review round 3, Critical 1): the pinned file can also be
69
+ // *stale* rather than missing — a re-dispatch omits `--session-id` (see
70
+ // `sessionExists`) precisely because attempt 1's file already owns this
71
+ // name, so a direct read here would silently keep returning attempt 1's
72
+ // transcript forever. `startedAt` is re-stamped by the conductor on
73
+ // every dispatch (`markRunning`), so a pinned file older than this
74
+ // attempt's own start can only be a leftover from a prior attempt —
75
+ // reject it and fall through to the scan, which is bounded by the same
76
+ // timestamp and will find the fresh, unpinned file this attempt wrote.
77
+ // GAIA-473 review round 4 (Critical 2): when the run's deterministic
78
+ // attempt family is known, select the HIGHEST-numbered attempt whose
79
+ // file actually exists — an IDENTITY, not the `startedAt` window below,
80
+ // which a sibling session newer than the bound could still beat. Each
81
+ // family member names a distinct attempt, so no staleness check is
82
+ // needed: whichever member exists IS that attempt's own transcript.
83
+ if (run?.attemptFamily?.length) {
84
+ for (let i = run.attemptFamily.length - 1; i >= 0; i--) {
85
+ try {
86
+ return await readFile(join(dir, `${run.attemptFamily[i]}.jsonl`), 'utf8');
87
+ }
88
+ catch {
89
+ // try the next, lower-numbered attempt.
90
+ }
91
+ }
92
+ // AC-5: none of the family's files exist yet — a run dispatched before
93
+ // this change, or the agent never wrote its pinned file — so fall
94
+ // through to the scan below instead of returning ''.
95
+ }
96
+ else if (run?.sessionId) {
97
+ try {
98
+ const path = join(dir, `${run.sessionId}.jsonl`);
99
+ const isStale = startedAtMs !== null &&
100
+ Number.isFinite(startedAtMs) &&
101
+ (await stat(path)).mtimeMs < startedAtMs;
102
+ if (!isStale) {
103
+ return await readFile(path, 'utf8');
104
+ }
105
+ }
106
+ catch {
107
+ // fall through to the scan.
108
+ }
109
+ }
35
110
  let entries;
36
111
  try {
37
112
  entries = await readdir(dir);
@@ -47,6 +122,11 @@ export class ClaudeAgent {
47
122
  const full = join(dir, name);
48
123
  try {
49
124
  const s = await stat(full);
125
+ if (startedAtMs !== null &&
126
+ Number.isFinite(startedAtMs) &&
127
+ s.mtimeMs < startedAtMs) {
128
+ continue;
129
+ }
50
130
  if (!latest || s.mtimeMs > latest.mtimeMs) {
51
131
  latest = { path: full, mtimeMs: s.mtimeMs };
52
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-claude",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "GAIA agent plugin for the Claude CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "directory": "gaia-cli/addons/claude"
21
21
  },
22
22
  "peerDependencies": {
23
- "@gaia-ai/conductor": "^0.12.0",
24
- "@gaia-ai/core": "^0.12.0"
23
+ "@gaia-ai/conductor": "^0.13.0",
24
+ "@gaia-ai/core": "^0.13.0"
25
25
  }
26
26
  }