@gaia-ai/addon-grok 0.12.0 → 0.13.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.
@@ -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 { parseGrokTranscript } from './footprint.js';
3
3
  export type GrokReasoningEffort = 'high' | 'medium' | 'low';
4
4
  export interface GrokAgentOptions {
@@ -12,8 +12,16 @@ export declare class GrokAgent implements GaiaAgent {
12
12
  private readonly home;
13
13
  readonly id = "grok";
14
14
  constructor(options?: GrokAgentOptions, home?: string);
15
- launchCommand(prompt: string): string;
16
- getRunLog(worktreePath: string): Promise<string>;
15
+ launchCommand(prompt: string, run?: AgentRunIdentity): string;
16
+ /**
17
+ * `grok --session-id <uuid>` documents "new conversation… must not already
18
+ * exist" (GAIA-473 review round 2), so the conductor checks this before
19
+ * pinning the run's own UUID — a re-dispatch of the same run would
20
+ * otherwise mint the id its first attempt already wrote and fail to
21
+ * launch.
22
+ */
23
+ sessionExists(worktreePath: string, sessionId: string): Promise<boolean>;
24
+ getRunLog(worktreePath: string, run?: AgentRunIdentity): Promise<string>;
17
25
  parseFootprint(log: string): AgentFootprint;
18
26
  }
19
27
  export declare function grokAgent(options?: GrokAgentOptions): AgentPlugin;
package/dist/src/index.js CHANGED
@@ -36,7 +36,7 @@ export class GrokAgent {
36
36
  this.options = options;
37
37
  this.home = home;
38
38
  }
39
- launchCommand(prompt) {
39
+ launchCommand(prompt, run) {
40
40
  const parts = ['grok'];
41
41
  if (prompt !== '') {
42
42
  // Positional — the interactive form Grok Build documents
@@ -49,10 +49,100 @@ export class GrokAgent {
49
49
  if (this.options.reasoningEffort) {
50
50
  parts.push('--reasoning-effort', this.options.reasoningEffort);
51
51
  }
52
+ // Pin the session dir name to the run's UUID (GAIA-473), so `getRunLog`
53
+ // can select it directly instead of scanning every session dir under the
54
+ // cwd for the newest match — the scan is what let a foreign transcript
55
+ // (resumed run, re-dispatch, reused worktree) get finalised.
56
+ if (run?.sessionId) {
57
+ parts.push('--session-id', run.sessionId);
58
+ }
52
59
  return parts.join(' ');
53
60
  }
54
- async getRunLog(worktreePath) {
61
+ /**
62
+ * `grok --session-id <uuid>` documents "new conversation… must not already
63
+ * exist" (GAIA-473 review round 2), so the conductor checks this before
64
+ * pinning the run's own UUID — a re-dispatch of the same run would
65
+ * otherwise mint the id its first attempt already wrote and fail to
66
+ * launch.
67
+ */
68
+ async sessionExists(worktreePath, sessionId) {
69
+ const dir = join(this.home, '.grok', 'sessions', encodeGrokSessionCwd(worktreePath), sessionId);
70
+ return (await readJson(join(dir, 'summary.json'))) !== null;
71
+ }
72
+ async getRunLog(worktreePath, run) {
55
73
  const root = join(this.home, '.grok', 'sessions', encodeGrokSessionCwd(worktreePath));
74
+ const startedAtMs = run?.startedAt !== undefined ? new Date(run.startedAt).getTime() : null;
75
+ // GAIA-473: when the run's own session id is known (pinned at launch via
76
+ // `--session-id`), read that session dir directly — no directory scan,
77
+ // so a foreign transcript can never be selected. AC-5: when the pinned
78
+ // session dir is missing (a run dispatched before this change, or the
79
+ // agent never wrote it), fall through to the scan below — bounded by
80
+ // `run.startedAt` where known — instead of returning '' and turning a
81
+ // wrong log into no log at all.
82
+ //
83
+ // Round 4 (review round 3, Critical 1): the pinned dir can also be
84
+ // *stale* — a re-dispatch omits `--session-id` (see `sessionExists`)
85
+ // because attempt 1's dir already owns this name, so a direct read here
86
+ // would silently keep returning attempt 1's transcript. `startedAt` is
87
+ // re-stamped on every dispatch, so a pinned dir older than this
88
+ // attempt's own start can only be a prior attempt's leftover — reject it
89
+ // and fall through to the scan, bounded by the same timestamp.
90
+ // GAIA-473 review round 4 (Critical 2): prefer the deterministic attempt
91
+ // family — the HIGHEST-numbered attempt whose session dir actually
92
+ // exists — over the `startedAt` window below, which a sibling session
93
+ // newer than the bound could still beat. Each family member names a
94
+ // distinct attempt, so no staleness check is needed.
95
+ if (run?.attemptFamily?.length) {
96
+ for (let i = run.attemptFamily.length - 1; i >= 0; i--) {
97
+ const candidate = run.attemptFamily[i];
98
+ if (!candidate)
99
+ continue;
100
+ const dir = join(root, candidate);
101
+ const summary = await readJson(join(dir, 'summary.json'));
102
+ if (!summary) {
103
+ continue; // try the next, lower-numbered attempt.
104
+ }
105
+ const [signals, chatHistory, updates] = await Promise.all([
106
+ readJson(join(dir, 'signals.json')),
107
+ readText(join(dir, 'chat_history.jsonl')),
108
+ readText(join(dir, 'updates.jsonl')),
109
+ ]);
110
+ return JSON.stringify({
111
+ summary,
112
+ signals: signals ?? {},
113
+ chatHistory,
114
+ updates,
115
+ });
116
+ }
117
+ // AC-5: none of the family's dirs exist yet — fall through to the scan.
118
+ }
119
+ else if (run?.sessionId) {
120
+ const dir = join(root, run.sessionId);
121
+ const summary = await readJson(join(dir, 'summary.json'));
122
+ let isStale = false;
123
+ if (summary && startedAtMs !== null && Number.isFinite(startedAtMs)) {
124
+ try {
125
+ isStale = (await stat(dir)).mtimeMs < startedAtMs;
126
+ }
127
+ catch {
128
+ isStale = true;
129
+ }
130
+ }
131
+ if (summary && !isStale) {
132
+ const [signals, chatHistory, updates] = await Promise.all([
133
+ readJson(join(dir, 'signals.json')),
134
+ readText(join(dir, 'chat_history.jsonl')),
135
+ readText(join(dir, 'updates.jsonl')),
136
+ ]);
137
+ return JSON.stringify({
138
+ summary,
139
+ signals: signals ?? {},
140
+ chatHistory,
141
+ updates,
142
+ });
143
+ }
144
+ // fall through to the scan.
145
+ }
56
146
  let names;
57
147
  try {
58
148
  names = await readdir(root);
@@ -84,6 +174,11 @@ export class GrokAgent {
84
174
  continue;
85
175
  }
86
176
  }
177
+ if (startedAtMs !== null &&
178
+ Number.isFinite(startedAtMs) &&
179
+ updated < startedAtMs) {
180
+ continue;
181
+ }
87
182
  if (!latest || updated > latest.updated) {
88
183
  latest = { dir, summary, updated };
89
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-grok",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "GAIA agent plugin for xAI Grok Build, with a real session footprint.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "directory": "gaia-cli/addons/grok"
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.1",
24
+ "@gaia-ai/core": "^0.13.1"
25
25
  }
26
26
  }