@agentex/agent 0.0.27 → 0.0.29
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 +138 -0
- package/README.md +98 -8
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +99 -2
- package/dist/derived.js.map +1 -1
- package/dist/goals/controller.d.ts.map +1 -1
- package/dist/goals/controller.js +21 -0
- package/dist/goals/controller.js.map +1 -1
- package/dist/goals/normalize.d.ts +1 -0
- package/dist/goals/normalize.d.ts.map +1 -1
- package/dist/goals/normalize.js +5 -0
- package/dist/goals/normalize.js.map +1 -1
- package/dist/history/fs.d.ts +36 -0
- package/dist/history/fs.d.ts.map +1 -0
- package/dist/history/fs.js +266 -0
- package/dist/history/fs.js.map +1 -0
- package/dist/history/index.d.ts +3 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/types.d.ts +75 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +9 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/attach.d.ts.map +1 -1
- package/dist/providers/claude/attach.js +50 -9
- package/dist/providers/claude/attach.js.map +1 -1
- package/dist/providers/claude/history.d.ts +3 -0
- package/dist/providers/claude/history.d.ts.map +1 -0
- package/dist/providers/claude/history.js +248 -0
- package/dist/providers/claude/history.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +26 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +2 -0
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +17 -10
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +2 -2
- package/dist/providers/codex/attach.d.ts.map +1 -1
- package/dist/providers/codex/attach.js +44 -7
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/history.d.ts +3 -0
- package/dist/providers/codex/history.d.ts.map +1 -0
- package/dist/providers/codex/history.js +350 -0
- package/dist/providers/codex/history.js.map +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +26 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +3 -2
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +4 -3
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/providers/codex/transcript.d.ts +3 -1
- package/dist/providers/codex/transcript.d.ts.map +1 -1
- package/dist/providers/codex/transcript.js +3 -1
- package/dist/providers/codex/transcript.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts +6 -0
- package/dist/providers/cursor/discovery.d.ts.map +1 -0
- package/dist/providers/cursor/discovery.js +118 -0
- package/dist/providers/cursor/discovery.js.map +1 -0
- package/dist/providers/cursor/execute.d.ts.map +1 -1
- package/dist/providers/cursor/execute.js +83 -45
- package/dist/providers/cursor/execute.js.map +1 -1
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +17 -4
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.js +1 -1
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/cursor/probe.d.ts +3 -0
- package/dist/providers/cursor/probe.d.ts.map +1 -0
- package/dist/providers/cursor/probe.js +89 -0
- package/dist/providers/cursor/probe.js.map +1 -0
- package/dist/providers/cursor/runtime.d.ts +4 -0
- package/dist/providers/cursor/runtime.d.ts.map +1 -0
- package/dist/providers/cursor/runtime.js +12 -0
- package/dist/providers/cursor/runtime.js.map +1 -0
- package/dist/providers/cursor/session.d.ts +3 -0
- package/dist/providers/cursor/session.d.ts.map +1 -0
- package/dist/providers/cursor/session.js +14 -0
- package/dist/providers/cursor/session.js.map +1 -0
- package/dist/providers/opencode/client.d.ts +14 -0
- package/dist/providers/opencode/client.d.ts.map +1 -0
- package/dist/providers/opencode/client.js +40 -0
- package/dist/providers/opencode/client.js.map +1 -0
- package/dist/providers/opencode/discovery.d.ts +4 -0
- package/dist/providers/opencode/discovery.d.ts.map +1 -0
- package/dist/providers/opencode/discovery.js +68 -0
- package/dist/providers/opencode/discovery.js.map +1 -0
- package/dist/providers/opencode/execute.d.ts.map +1 -1
- package/dist/providers/opencode/execute.js +219 -164
- package/dist/providers/opencode/execute.js.map +1 -1
- package/dist/providers/opencode/history.d.ts +24 -0
- package/dist/providers/opencode/history.d.ts.map +1 -0
- package/dist/providers/opencode/history.js +242 -0
- package/dist/providers/opencode/history.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +333 -49
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +26 -3
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/manager.d.ts +11 -0
- package/dist/providers/opencode/manager.d.ts.map +1 -0
- package/dist/providers/opencode/manager.js +240 -0
- package/dist/providers/opencode/manager.js.map +1 -0
- package/dist/providers/opencode/modes.d.ts +3 -0
- package/dist/providers/opencode/modes.d.ts.map +1 -0
- package/dist/providers/opencode/modes.js +30 -0
- package/dist/providers/opencode/modes.js.map +1 -0
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +2 -22
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/probe.d.ts +3 -0
- package/dist/providers/opencode/probe.d.ts.map +1 -0
- package/dist/providers/opencode/probe.js +107 -0
- package/dist/providers/opencode/probe.js.map +1 -0
- package/dist/providers/opencode/runtime.d.ts +8 -0
- package/dist/providers/opencode/runtime.d.ts.map +1 -0
- package/dist/providers/opencode/runtime.js +12 -0
- package/dist/providers/opencode/runtime.js.map +1 -0
- package/dist/providers/opencode/server.d.ts +18 -3
- package/dist/providers/opencode/server.d.ts.map +1 -1
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -1
- package/dist/providers/opencode/skill-config.d.ts +11 -0
- package/dist/providers/opencode/skill-config.d.ts.map +1 -0
- package/dist/providers/opencode/skill-config.js +57 -0
- package/dist/providers/opencode/skill-config.js.map +1 -0
- package/dist/sessions/exec-backed.d.ts +10 -0
- package/dist/sessions/exec-backed.d.ts.map +1 -0
- package/dist/sessions/exec-backed.js +141 -0
- package/dist/sessions/exec-backed.js.map +1 -0
- package/dist/sessions/history.d.ts +4 -0
- package/dist/sessions/history.d.ts.map +1 -0
- package/dist/sessions/history.js +59 -0
- package/dist/sessions/history.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/types.d.ts +152 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +36 -6
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +3 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/endpoint.js +3 -3
- package/dist/utils/endpoint.js.map +1 -1
- package/package.json +10 -2
- package/src/derived.ts +126 -3
- package/src/goals/controller.ts +17 -0
- package/src/goals/normalize.ts +5 -0
- package/src/history/fs.ts +307 -0
- package/src/history/index.ts +18 -0
- package/src/history/types.ts +104 -0
- package/src/index.ts +31 -0
- package/src/providers/claude/attach.ts +48 -6
- package/src/providers/claude/history.ts +290 -0
- package/src/providers/claude/index.ts +26 -0
- package/src/providers/claude/session.ts +15 -10
- package/src/providers/codex/attach.ts +52 -7
- package/src/providers/codex/history.ts +399 -0
- package/src/providers/codex/index.ts +26 -0
- package/src/providers/codex/transcript-normalize.ts +4 -3
- package/src/providers/codex/transcript.ts +5 -1
- package/src/providers/cursor/discovery.ts +114 -0
- package/src/providers/cursor/execute.ts +67 -30
- package/src/providers/cursor/index.ts +19 -5
- package/src/providers/cursor/parse.ts +1 -1
- package/src/providers/cursor/probe.ts +90 -0
- package/src/providers/cursor/runtime.ts +11 -0
- package/src/providers/cursor/session.ts +15 -0
- package/src/providers/opencode/client.ts +44 -0
- package/src/providers/opencode/discovery.ts +69 -0
- package/src/providers/opencode/execute.ts +64 -12
- package/src/providers/opencode/history.ts +270 -0
- package/src/providers/opencode/http-session.ts +319 -50
- package/src/providers/opencode/index.ts +26 -3
- package/src/providers/opencode/manager.ts +269 -0
- package/src/providers/opencode/modes.ts +31 -0
- package/src/providers/opencode/parse.ts +2 -22
- package/src/providers/opencode/probe.ts +113 -0
- package/src/providers/opencode/runtime.ts +13 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/opencode/skill-config.ts +64 -0
- package/src/sessions/exec-backed.ts +169 -0
- package/src/sessions/history.ts +76 -0
- package/src/sessions/index.ts +3 -0
- package/src/types.ts +163 -4
- package/src/utils/auth.ts +35 -6
- package/src/utils/binary.ts +3 -0
- package/src/utils/endpoint.ts +3 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { BaseStreamEventFields, StreamEvent } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface LocalHistoryDiscoverOptions {
|
|
4
|
+
includeArchived?: boolean;
|
|
5
|
+
mainSessionsOnly?: boolean;
|
|
6
|
+
requireUserMessage?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
limit?: number;
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LocalHistoryProbeOptions {
|
|
13
|
+
limit?: number;
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LocalHistoryProbeResult {
|
|
18
|
+
providerType: string;
|
|
19
|
+
homeAvailable: boolean;
|
|
20
|
+
historyAvailable: boolean;
|
|
21
|
+
/** A file count only. It is not an eligible-session count. */
|
|
22
|
+
approximateCount?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type LocalHistoryArchiveState = "active" | "archived" | "unknown";
|
|
26
|
+
|
|
27
|
+
export interface LocalHistorySourceFingerprint {
|
|
28
|
+
size: number;
|
|
29
|
+
/** String because nanosecond timestamps can exceed Number.MAX_SAFE_INTEGER. */
|
|
30
|
+
modifiedAtNs: string;
|
|
31
|
+
sha256?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LocalHistorySession {
|
|
35
|
+
version: 1;
|
|
36
|
+
providerType: string;
|
|
37
|
+
externalSessionId: string;
|
|
38
|
+
/** Local-runtime detail. Network hosts must not serialize this to clients. */
|
|
39
|
+
transcriptPath: string;
|
|
40
|
+
cwd: string | null;
|
|
41
|
+
title: string | null;
|
|
42
|
+
startedAt: string | null;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
branch: string | null;
|
|
45
|
+
gitOriginUrl: string | null;
|
|
46
|
+
archiveState: LocalHistoryArchiveState;
|
|
47
|
+
hasUserMessage: boolean;
|
|
48
|
+
source: LocalHistorySourceFingerprint;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type LocalHistoryUserEvent = {
|
|
52
|
+
type: "user";
|
|
53
|
+
text: string;
|
|
54
|
+
} & BaseStreamEventFields;
|
|
55
|
+
|
|
56
|
+
export type LocalHistoryEvent = StreamEvent | LocalHistoryUserEvent;
|
|
57
|
+
|
|
58
|
+
export interface LocalHistoryYield {
|
|
59
|
+
event: LocalHistoryEvent & { eventId: string };
|
|
60
|
+
lineStartOffset: number;
|
|
61
|
+
nextOffset: number;
|
|
62
|
+
/** Disambiguates several normalized events produced from one source record. */
|
|
63
|
+
partIndex: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LocalHistoryReadOptions {
|
|
67
|
+
fromOffset?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface LocalHistoryFingerprintOptions {
|
|
71
|
+
sha256?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type LocalHistoryErrorCode =
|
|
75
|
+
| "home_missing"
|
|
76
|
+
| "permission_denied"
|
|
77
|
+
| "source_missing"
|
|
78
|
+
| "unsupported_format"
|
|
79
|
+
| "source_changed_during_read"
|
|
80
|
+
| "invalid_session"
|
|
81
|
+
| "io_error";
|
|
82
|
+
|
|
83
|
+
export class LocalHistoryError extends Error {
|
|
84
|
+
readonly code: LocalHistoryErrorCode;
|
|
85
|
+
|
|
86
|
+
constructor(code: LocalHistoryErrorCode, message: string, options?: ErrorOptions) {
|
|
87
|
+
super(message, options);
|
|
88
|
+
this.name = "LocalHistoryError";
|
|
89
|
+
this.code = code;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface LocalHistoryOps {
|
|
94
|
+
probe(options?: LocalHistoryProbeOptions): Promise<LocalHistoryProbeResult>;
|
|
95
|
+
discover(options?: LocalHistoryDiscoverOptions): AsyncIterable<LocalHistorySession>;
|
|
96
|
+
read(
|
|
97
|
+
session: LocalHistorySession,
|
|
98
|
+
options?: LocalHistoryReadOptions,
|
|
99
|
+
): AsyncIterable<LocalHistoryYield>;
|
|
100
|
+
fingerprint(
|
|
101
|
+
session: LocalHistorySession,
|
|
102
|
+
options?: LocalHistoryFingerprintOptions,
|
|
103
|
+
): Promise<LocalHistorySourceFingerprint>;
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -141,6 +141,9 @@ export {
|
|
|
141
141
|
export type {
|
|
142
142
|
ProviderModule,
|
|
143
143
|
ProviderCapabilities,
|
|
144
|
+
ProviderRuntimeContext,
|
|
145
|
+
ProviderRuntimeReport,
|
|
146
|
+
CapabilityStatus,
|
|
144
147
|
AgentMode,
|
|
145
148
|
ListModesOptions,
|
|
146
149
|
ExecutionContext,
|
|
@@ -168,6 +171,11 @@ export type {
|
|
|
168
171
|
AuthRequiredReason,
|
|
169
172
|
BinaryStatus,
|
|
170
173
|
ProviderModel,
|
|
174
|
+
ListModelsOptions,
|
|
175
|
+
ProviderAuthMethod,
|
|
176
|
+
ProviderAuthFlow,
|
|
177
|
+
UpstreamProvider,
|
|
178
|
+
UpstreamProviderManager,
|
|
171
179
|
SessionContext,
|
|
172
180
|
AgentSession,
|
|
173
181
|
SendHandle,
|
|
@@ -198,6 +206,24 @@ export type {
|
|
|
198
206
|
GoalSentinelContext,
|
|
199
207
|
} from "./types.js";
|
|
200
208
|
|
|
209
|
+
// Local persisted session discovery. Runtime errors are exported from the
|
|
210
|
+
// `@agentex/agent/history` subpath to keep the root barrel lazy.
|
|
211
|
+
export type {
|
|
212
|
+
LocalHistoryArchiveState,
|
|
213
|
+
LocalHistoryDiscoverOptions,
|
|
214
|
+
LocalHistoryErrorCode,
|
|
215
|
+
LocalHistoryEvent,
|
|
216
|
+
LocalHistoryFingerprintOptions,
|
|
217
|
+
LocalHistoryOps,
|
|
218
|
+
LocalHistoryProbeOptions,
|
|
219
|
+
LocalHistoryProbeResult,
|
|
220
|
+
LocalHistoryReadOptions,
|
|
221
|
+
LocalHistorySession,
|
|
222
|
+
LocalHistorySourceFingerprint,
|
|
223
|
+
LocalHistoryUserEvent,
|
|
224
|
+
LocalHistoryYield,
|
|
225
|
+
} from "./history/index.js";
|
|
226
|
+
|
|
201
227
|
// Goals
|
|
202
228
|
export {
|
|
203
229
|
GoalController,
|
|
@@ -235,6 +261,11 @@ export type {
|
|
|
235
261
|
CatchUpOptions,
|
|
236
262
|
AttachOptions,
|
|
237
263
|
SessionAttachment,
|
|
264
|
+
HistoryCheckpoint,
|
|
265
|
+
HistorySource,
|
|
266
|
+
HistoryCatchUpYield,
|
|
267
|
+
HistoryCatchUpOptions,
|
|
268
|
+
HistoryAttachment,
|
|
238
269
|
} from "./types.js";
|
|
239
270
|
|
|
240
271
|
export type {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import { stat } from "node:fs/promises";
|
|
3
|
+
import * as readline from "node:readline";
|
|
2
4
|
import type {
|
|
3
5
|
AttachOptions,
|
|
4
6
|
CatchUpOptions,
|
|
@@ -19,7 +21,6 @@ import { claudeSessionCodec } from "./codec.js";
|
|
|
19
21
|
import {
|
|
20
22
|
findClaudeTranscriptBySessionId,
|
|
21
23
|
getClaudeTranscriptPath,
|
|
22
|
-
peekClaudeTranscript,
|
|
23
24
|
readClaudeTranscript,
|
|
24
25
|
} from "./transcript.js";
|
|
25
26
|
// Heavy-on-heavy is fine: this whole module loads lazily via
|
|
@@ -77,6 +78,44 @@ const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
|
77
78
|
},
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Classify lifecycle boundaries from the raw transcript rather than replay's
|
|
83
|
+
* normalized events. Ordinary Claude `user` prompt records intentionally
|
|
84
|
+
* normalize to no StreamEvent, but still start a new turn. Looking only at
|
|
85
|
+
* normalized output can therefore mistake an interrupted new turn for the
|
|
86
|
+
* preceding completed one.
|
|
87
|
+
*/
|
|
88
|
+
async function classifyLastTurn(filePath: string): Promise<LastTurnStatus> {
|
|
89
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
90
|
+
stream.on("error", () => {});
|
|
91
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
92
|
+
let lastTurn: LastTurnStatus = "unknown";
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
for await (const line of lines) {
|
|
96
|
+
let raw: unknown;
|
|
97
|
+
try {
|
|
98
|
+
raw = JSON.parse(line);
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
103
|
+
const type = (raw as Record<string, unknown>)["type"];
|
|
104
|
+
if (type === "result") lastTurn = "completed";
|
|
105
|
+
else if (type === "user" || type === "assistant") lastTurn = "interrupted";
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
109
|
+
if (fsError?.code !== "ENOENT") throw error;
|
|
110
|
+
return "unknown";
|
|
111
|
+
} finally {
|
|
112
|
+
lines.close();
|
|
113
|
+
stream.destroy();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lastTurn;
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* Read-only reattachment to a durable Claude session. Composition of existing
|
|
82
121
|
* primitives (codec + transcript ops + `createClaudeSession` resume) — spawns
|
|
@@ -87,6 +126,12 @@ export async function attachClaudeSession(
|
|
|
87
126
|
opts?: AttachOptions,
|
|
88
127
|
): Promise<SessionAttachment> {
|
|
89
128
|
assertSessionRecord(record);
|
|
129
|
+
if (record.providerType !== "claude") {
|
|
130
|
+
throw new MalformedSessionRecordError(
|
|
131
|
+
`claude attach requires providerType "claude"; got ${JSON.stringify(record.providerType)}`,
|
|
132
|
+
"providerType",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
90
135
|
|
|
91
136
|
// 1. Normalize params through the codec.
|
|
92
137
|
const params = claudeSessionCodec.deserialize(record.params);
|
|
@@ -113,10 +158,7 @@ export async function attachClaudeSession(
|
|
|
113
158
|
// 3. Classify how the last persisted turn ended.
|
|
114
159
|
let lastTurn: LastTurnStatus = "unknown";
|
|
115
160
|
if (transcript) {
|
|
116
|
-
|
|
117
|
-
if (lastEvent === null) lastTurn = "unknown";
|
|
118
|
-
else if (lastEvent.type === "result") lastTurn = "completed";
|
|
119
|
-
else lastTurn = "interrupted";
|
|
161
|
+
lastTurn = await classifyLastTurn(transcript.filePath);
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
return {
|
|
@@ -141,7 +183,7 @@ export async function attachClaudeSession(
|
|
|
141
183
|
},
|
|
142
184
|
// 5. Continue live — exactly `createSession` with the record's params.
|
|
143
185
|
resume(ctx?: SessionContext) {
|
|
144
|
-
return createClaudeSession({ ...ctx, sessionParams: params });
|
|
186
|
+
return createClaudeSession({ ...ctx, cwd: ctx?.cwd ?? cwd ?? undefined, sessionParams: params });
|
|
145
187
|
},
|
|
146
188
|
};
|
|
147
189
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { BaseStreamEventFields, StreamEvent } from "../../types.js";
|
|
5
|
+
import type {
|
|
6
|
+
LocalHistoryDiscoverOptions,
|
|
7
|
+
LocalHistoryOps,
|
|
8
|
+
LocalHistoryProbeOptions,
|
|
9
|
+
LocalHistorySession,
|
|
10
|
+
LocalHistoryYield,
|
|
11
|
+
} from "../../history/types.js";
|
|
12
|
+
import {
|
|
13
|
+
UUID_JSONL_RE,
|
|
14
|
+
asRecord,
|
|
15
|
+
asString,
|
|
16
|
+
cleanTitle,
|
|
17
|
+
isDirectory,
|
|
18
|
+
isoDate,
|
|
19
|
+
meaningfulHumanText,
|
|
20
|
+
readJsonlRecords,
|
|
21
|
+
recentEligible,
|
|
22
|
+
sourceFingerprint,
|
|
23
|
+
textFromContent,
|
|
24
|
+
} from "../../history/fs.js";
|
|
25
|
+
import { parseStreamLine } from "./parse.js";
|
|
26
|
+
import { resolveClaudeHome } from "./transcript.js";
|
|
27
|
+
|
|
28
|
+
const TITLE_SCAN_BYTES = 64 * 1024;
|
|
29
|
+
const MAX_DISCOVERY_SCAN_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
const DEFAULT_PROBE_LIMIT = 100;
|
|
31
|
+
const DISCOVERY_CONCURRENCY = 16;
|
|
32
|
+
const DROP_EVENT_TYPES = new Set([
|
|
33
|
+
"assistant_delta",
|
|
34
|
+
"thinking_delta",
|
|
35
|
+
"permission_mode",
|
|
36
|
+
"rate_limit",
|
|
37
|
+
"unknown",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function claudeHome(options?: { env?: Record<string, string> }): string {
|
|
41
|
+
return resolveClaudeHome(options?.env?.["CLAUDE_CONFIG_DIR"]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function listClaudeSessionFiles(
|
|
45
|
+
home: string,
|
|
46
|
+
options: { limit?: number; includeSubagents?: boolean } = {},
|
|
47
|
+
): Promise<string[]> {
|
|
48
|
+
const projectsRoot = path.join(home, "projects");
|
|
49
|
+
let projects;
|
|
50
|
+
try {
|
|
51
|
+
projects = await readdir(projectsRoot, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
56
|
+
const files: string[] = [];
|
|
57
|
+
const cap = Math.max(1, options.limit ?? Number.MAX_SAFE_INTEGER);
|
|
58
|
+
for (const project of projects) {
|
|
59
|
+
if (files.length >= cap) break;
|
|
60
|
+
if (!project.isDirectory()) continue;
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(path.join(projectsRoot, project.name), { withFileTypes: true });
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (files.length >= cap) break;
|
|
70
|
+
// Top-level UUID files are main sessions. Nested subagent directories and
|
|
71
|
+
// symlinks are deliberately ignored.
|
|
72
|
+
if (entry.isFile() && UUID_JSONL_RE.test(entry.name)) {
|
|
73
|
+
files.push(path.join(projectsRoot, project.name, entry.name));
|
|
74
|
+
}
|
|
75
|
+
if (options.includeSubagents && entry.isDirectory() && UUID_JSONL_RE.test(`${entry.name}.jsonl`)) {
|
|
76
|
+
const subagentsDir = path.join(projectsRoot, project.name, entry.name, "subagents");
|
|
77
|
+
let subagents;
|
|
78
|
+
try {
|
|
79
|
+
subagents = await readdir(subagentsDir, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
subagents.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
for (const subagent of subagents) {
|
|
85
|
+
if (files.length >= cap) break;
|
|
86
|
+
if (subagent.isFile() && /^agent-[a-z0-9]+\.jsonl$/i.test(subagent.name)) {
|
|
87
|
+
files.push(path.join(subagentsDir, subagent.name));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function subagentParentId(transcriptPath: string): string | null {
|
|
97
|
+
if (path.basename(path.dirname(transcriptPath)) !== "subagents") return null;
|
|
98
|
+
return path.basename(path.dirname(path.dirname(transcriptPath))) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function parentSessionMetadata(
|
|
102
|
+
transcriptPath: string,
|
|
103
|
+
): Promise<{ cwd: string | null; branch: string | null }> {
|
|
104
|
+
const parentId = subagentParentId(transcriptPath);
|
|
105
|
+
if (!parentId) return { cwd: null, branch: null };
|
|
106
|
+
const projectDir = path.dirname(path.dirname(path.dirname(transcriptPath)));
|
|
107
|
+
const parentTranscript = path.join(projectDir, `${parentId}.jsonl`);
|
|
108
|
+
for await (const line of readJsonlRecords(parentTranscript)) {
|
|
109
|
+
const cwd = absoluteCwd(asString(line.raw["cwd"]));
|
|
110
|
+
const branch = asString(line.raw["gitBranch"]);
|
|
111
|
+
if (cwd) return { cwd, branch };
|
|
112
|
+
if (line.nextOffset >= TITLE_SCAN_BYTES) break;
|
|
113
|
+
}
|
|
114
|
+
return { cwd: null, branch: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function claudeUserText(record: Record<string, unknown>): string | null {
|
|
118
|
+
if (record["type"] !== "user") return null;
|
|
119
|
+
const message = asRecord(record["message"]);
|
|
120
|
+
if (!message || message["role"] !== "user") return null;
|
|
121
|
+
return meaningfulHumanText(textFromContent(
|
|
122
|
+
message["content"],
|
|
123
|
+
new Set(["text", "input_text"]),
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function absoluteCwd(value: string | null): string | null {
|
|
128
|
+
return value && path.isAbsolute(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function inspectClaudeSession(
|
|
132
|
+
transcriptPath: string,
|
|
133
|
+
options: LocalHistoryDiscoverOptions,
|
|
134
|
+
): Promise<LocalHistorySession | null> {
|
|
135
|
+
const fileIdentity = path.basename(transcriptPath, ".jsonl");
|
|
136
|
+
const parentId = subagentParentId(transcriptPath);
|
|
137
|
+
const externalSessionId = parentId
|
|
138
|
+
? `subagent:${parentId}:${fileIdentity}`
|
|
139
|
+
: fileIdentity;
|
|
140
|
+
let cwd: string | null = null;
|
|
141
|
+
let title: string | null = null;
|
|
142
|
+
let firstPrompt: string | null = null;
|
|
143
|
+
let startedAt: string | null = null;
|
|
144
|
+
let branch: string | null = null;
|
|
145
|
+
let sidechain = false;
|
|
146
|
+
|
|
147
|
+
for await (const line of readJsonlRecords(transcriptPath)) {
|
|
148
|
+
const record = line.raw;
|
|
149
|
+
if (record["isSidechain"] === true) sidechain = true;
|
|
150
|
+
cwd ??= absoluteCwd(asString(record["cwd"]));
|
|
151
|
+
startedAt ??= isoDate(record["timestamp"]);
|
|
152
|
+
branch ??= asString(record["gitBranch"]);
|
|
153
|
+
if (record["type"] === "ai-title") title ??= cleanTitle(asString(record["aiTitle"]));
|
|
154
|
+
firstPrompt ??= claudeUserText(record);
|
|
155
|
+
if (cwd && firstPrompt && (title || line.nextOffset >= TITLE_SCAN_BYTES)) break;
|
|
156
|
+
if (line.nextOffset >= MAX_DISCOVERY_SCAN_BYTES) break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!cwd && parentId) {
|
|
160
|
+
const parent = await parentSessionMetadata(transcriptPath);
|
|
161
|
+
cwd = parent.cwd;
|
|
162
|
+
branch ??= parent.branch;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ((options.mainSessionsOnly ?? true) && sidechain) return null;
|
|
166
|
+
const hasUserMessage = firstPrompt !== null;
|
|
167
|
+
if ((options.requireUserMessage ?? true) && !hasUserMessage) return null;
|
|
168
|
+
if ((options.mainSessionsOnly ?? true) && !cwd) return null;
|
|
169
|
+
if (options.cwd && (!cwd || path.normalize(cwd) !== path.normalize(options.cwd))) return null;
|
|
170
|
+
|
|
171
|
+
const source = await sourceFingerprint(transcriptPath);
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
providerType: "claude",
|
|
175
|
+
externalSessionId,
|
|
176
|
+
transcriptPath,
|
|
177
|
+
cwd,
|
|
178
|
+
title: title ?? cleanTitle(firstPrompt),
|
|
179
|
+
startedAt,
|
|
180
|
+
updatedAt: new Date(Number(BigInt(source.modifiedAtNs) / 1_000_000n)).toISOString(),
|
|
181
|
+
branch,
|
|
182
|
+
gitOriginUrl: null,
|
|
183
|
+
archiveState: "unknown",
|
|
184
|
+
hasUserMessage,
|
|
185
|
+
source,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function baseFields(
|
|
190
|
+
session: LocalHistorySession,
|
|
191
|
+
raw: Record<string, unknown>,
|
|
192
|
+
eventId: string,
|
|
193
|
+
): BaseStreamEventFields & { eventId: string } {
|
|
194
|
+
const message = asRecord(raw["message"]);
|
|
195
|
+
return {
|
|
196
|
+
timestamp: isoDate(raw["timestamp"], session.startedAt ?? new Date(0).toISOString())!,
|
|
197
|
+
providerType: "claude",
|
|
198
|
+
sessionId: session.externalSessionId,
|
|
199
|
+
messageId: asString(message?.["id"]),
|
|
200
|
+
eventId,
|
|
201
|
+
turnId: null,
|
|
202
|
+
parentToolCallId: asString(raw["parent_tool_use_id"]),
|
|
203
|
+
raw,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizedClaudeEvents(
|
|
208
|
+
session: LocalHistorySession,
|
|
209
|
+
raw: Record<string, unknown>,
|
|
210
|
+
text: string,
|
|
211
|
+
eventId: string,
|
|
212
|
+
): Array<StreamEvent & { eventId: string }> {
|
|
213
|
+
return parseStreamLine(text)
|
|
214
|
+
.filter((event) => !DROP_EVENT_TYPES.has(event.type))
|
|
215
|
+
.map((event) => ({
|
|
216
|
+
...event,
|
|
217
|
+
timestamp: isoDate(raw["timestamp"], event.timestamp)!,
|
|
218
|
+
providerType: "claude",
|
|
219
|
+
sessionId: session.externalSessionId,
|
|
220
|
+
eventId,
|
|
221
|
+
raw,
|
|
222
|
+
} as StreamEvent & { eventId: string }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const claudeLocalHistory: LocalHistoryOps = {
|
|
226
|
+
async probe(options?: LocalHistoryProbeOptions) {
|
|
227
|
+
const home = claudeHome(options);
|
|
228
|
+
const homeAvailable = await isDirectory(home);
|
|
229
|
+
const files = homeAvailable
|
|
230
|
+
? await listClaudeSessionFiles(home, { limit: options?.limit ?? DEFAULT_PROBE_LIMIT })
|
|
231
|
+
: [];
|
|
232
|
+
return {
|
|
233
|
+
providerType: "claude",
|
|
234
|
+
homeAvailable,
|
|
235
|
+
historyAvailable: files.length > 0,
|
|
236
|
+
approximateCount: files.length,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async *discover(options: LocalHistoryDiscoverOptions = {}) {
|
|
241
|
+
const files = await listClaudeSessionFiles(claudeHome(options), {
|
|
242
|
+
includeSubagents: options.mainSessionsOnly === false,
|
|
243
|
+
});
|
|
244
|
+
const sessions = await recentEligible(
|
|
245
|
+
files,
|
|
246
|
+
{ limit: options.limit, concurrency: DISCOVERY_CONCURRENCY },
|
|
247
|
+
async (filePath) => {
|
|
248
|
+
try {
|
|
249
|
+
return await inspectClaudeSession(filePath, options);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
for (const session of sessions) yield session;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async *read(session, options = {}): AsyncIterable<LocalHistoryYield> {
|
|
259
|
+
if (session.providerType !== "claude" || !path.isAbsolute(session.transcriptPath)) {
|
|
260
|
+
throw new Error("Invalid Claude local history session");
|
|
261
|
+
}
|
|
262
|
+
for await (const line of readJsonlRecords(
|
|
263
|
+
session.transcriptPath,
|
|
264
|
+
options.fromOffset ?? 0,
|
|
265
|
+
{ rejectChanges: true },
|
|
266
|
+
)) {
|
|
267
|
+
const eventId = asString(line.raw["uuid"])
|
|
268
|
+
?? `claude:${session.externalSessionId}:${line.lineStartOffset}`;
|
|
269
|
+
const events: LocalHistoryYield["event"][] = [];
|
|
270
|
+
const userText = claudeUserText(line.raw);
|
|
271
|
+
if (userText) {
|
|
272
|
+
events.push({ type: "user", text: userText, ...baseFields(session, line.raw, eventId) });
|
|
273
|
+
}
|
|
274
|
+
events.push(...normalizedClaudeEvents(session, line.raw, line.text, eventId));
|
|
275
|
+
for (let partIndex = 0; partIndex < events.length; partIndex++) {
|
|
276
|
+
yield {
|
|
277
|
+
event: events[partIndex]!,
|
|
278
|
+
lineStartOffset: line.lineStartOffset,
|
|
279
|
+
nextOffset: line.nextOffset,
|
|
280
|
+
partIndex,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
fingerprint(session, options) {
|
|
287
|
+
if (session.providerType !== "claude") throw new Error("Invalid Claude local history session");
|
|
288
|
+
return sourceFingerprint(session.transcriptPath, options?.sha256 ?? false);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
@@ -35,6 +35,8 @@ export const claudeProvider: ProviderModule = {
|
|
|
35
35
|
modes: false,
|
|
36
36
|
goals: claudeGoalCapability,
|
|
37
37
|
durableSessions: true,
|
|
38
|
+
durableHistory: true,
|
|
39
|
+
localHistory: true,
|
|
38
40
|
},
|
|
39
41
|
// Heavy machinery (execute.ts, session.ts, attach.ts) loads lazily on first
|
|
40
42
|
// use — every ProviderModule method is already async, so this is invisible to
|
|
@@ -48,6 +50,30 @@ export const claudeProvider: ProviderModule = {
|
|
|
48
50
|
transcript: claudeTranscriptOps,
|
|
49
51
|
attachSession: async (record, opts) =>
|
|
50
52
|
(await import("./attach.js")).attachClaudeSession(record, opts),
|
|
53
|
+
attachHistory: async (record, opts) => {
|
|
54
|
+
const [{ attachClaudeSession }, { historyFromSessionAttachment }] = await Promise.all([
|
|
55
|
+
import("./attach.js"),
|
|
56
|
+
import("../../sessions/history.js"),
|
|
57
|
+
]);
|
|
58
|
+
return historyFromSessionAttachment("claude", await attachClaudeSession(record, opts));
|
|
59
|
+
},
|
|
60
|
+
localHistory: {
|
|
61
|
+
probe: (options) => import("./history.js").then((module) => module.claudeLocalHistory.probe(options)),
|
|
62
|
+
discover: (options) => ({
|
|
63
|
+
async *[Symbol.asyncIterator]() {
|
|
64
|
+
const module = await import("./history.js");
|
|
65
|
+
yield* module.claudeLocalHistory.discover(options);
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
read: (session, options) => ({
|
|
69
|
+
async *[Symbol.asyncIterator]() {
|
|
70
|
+
const module = await import("./history.js");
|
|
71
|
+
yield* module.claudeLocalHistory.read(session, options);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
fingerprint: (session, options) => import("./history.js")
|
|
75
|
+
.then((module) => module.claudeLocalHistory.fingerprint(session, options)),
|
|
76
|
+
},
|
|
51
77
|
};
|
|
52
78
|
|
|
53
79
|
export {
|
|
@@ -216,11 +216,7 @@ export async function createClaudeSession(ctx: SessionContext): Promise<AgentSes
|
|
|
216
216
|
throw new Error("Failed to open stdio on Claude process");
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const session = new ClaudeSessionImpl(proc, ctx, skillsDir, mcpConfigPath);
|
|
220
|
-
|
|
221
|
-
// On resume, restore an unmet native goal from the transcript (best-effort,
|
|
222
|
-
// non-blocking so session creation isn't delayed by a transcript read).
|
|
223
|
-
if (resumeId) void session.hydrateGoalFromTranscript(resumeId);
|
|
219
|
+
const session = new ClaudeSessionImpl(proc, { ...ctx, cwd }, skillsDir, mcpConfigPath);
|
|
224
220
|
|
|
225
221
|
// Wire up AbortSignal to close the session
|
|
226
222
|
if (ctx.signal) {
|
|
@@ -231,6 +227,10 @@ export async function createClaudeSession(ctx: SessionContext): Promise<AgentSes
|
|
|
231
227
|
}
|
|
232
228
|
}
|
|
233
229
|
|
|
230
|
+
// Finish historical hydration before exposing the session. This prevents a
|
|
231
|
+
// late transcript read from overwriting a goal the caller sets immediately.
|
|
232
|
+
if (resumeId && session.state !== "closed") await session.hydrateGoalFromTranscript(resumeId);
|
|
233
|
+
|
|
234
234
|
return session;
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -307,6 +307,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
307
307
|
/** Tail state for observing native goal_status (transcript-only, not on stdout). */
|
|
308
308
|
private _goalScanOffset = 0;
|
|
309
309
|
private _goalPoll: ReturnType<typeof setInterval> | null = null;
|
|
310
|
+
private readonly cwd: string;
|
|
310
311
|
|
|
311
312
|
constructor(
|
|
312
313
|
private readonly proc: ChildProcess,
|
|
@@ -314,6 +315,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
314
315
|
private readonly skillsDir: string | null,
|
|
315
316
|
private readonly mcpConfigPath: string | null = null,
|
|
316
317
|
) {
|
|
318
|
+
this.cwd = ctx.cwd ?? process.cwd();
|
|
317
319
|
this._goals = new GoalController({
|
|
318
320
|
providerType: "claude",
|
|
319
321
|
capability: claudeGoalCapability,
|
|
@@ -384,6 +386,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
384
386
|
|
|
385
387
|
get sessionId(): string | null { return this._sessionId; }
|
|
386
388
|
get state(): SessionState { return this._state; }
|
|
389
|
+
private isClosed(): boolean { return this._state === "closed"; }
|
|
387
390
|
|
|
388
391
|
/**
|
|
389
392
|
* Durable identity for persistence + later `attachSession`. Null until Claude
|
|
@@ -392,7 +395,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
392
395
|
*/
|
|
393
396
|
describe(): SessionRecord | null {
|
|
394
397
|
if (!this._sessionId) return null;
|
|
395
|
-
const cwd = this.
|
|
398
|
+
const cwd = this.cwd;
|
|
396
399
|
const params = claudeSessionCodec.serialize({
|
|
397
400
|
sessionId: this._sessionId,
|
|
398
401
|
...(cwd ? { cwd } : {}),
|
|
@@ -1144,7 +1147,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
1144
1147
|
try {
|
|
1145
1148
|
const found = await claudeTranscriptOps.find({
|
|
1146
1149
|
sessionId,
|
|
1147
|
-
cwd: this.
|
|
1150
|
+
cwd: this.cwd,
|
|
1148
1151
|
});
|
|
1149
1152
|
if (found) this._transcriptPath = found.filePath;
|
|
1150
1153
|
} catch { /* best effort */ } finally {
|
|
@@ -1166,20 +1169,22 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
1166
1169
|
*/
|
|
1167
1170
|
async hydrateGoalFromTranscript(sessionId: string): Promise<void> {
|
|
1168
1171
|
try {
|
|
1172
|
+
if (this.isClosed()) return;
|
|
1169
1173
|
const found = await claudeTranscriptOps.find({
|
|
1170
1174
|
sessionId,
|
|
1171
|
-
cwd: this.
|
|
1175
|
+
cwd: this.cwd,
|
|
1172
1176
|
});
|
|
1173
|
-
if (!found) return;
|
|
1177
|
+
if (!found || this.isClosed()) return;
|
|
1174
1178
|
this._transcriptPath = found.filePath;
|
|
1175
1179
|
const events: StreamEvent[] = [];
|
|
1176
1180
|
let endOffset = 0;
|
|
1177
1181
|
for await (const { event, offset } of claudeTranscriptOps.read({ filePath: found.filePath })) {
|
|
1182
|
+
if (this.isClosed()) return;
|
|
1178
1183
|
endOffset = offset;
|
|
1179
1184
|
if (event.type === "goal_status") events.push(event);
|
|
1180
1185
|
}
|
|
1181
1186
|
const last = latestGoalFromEvents(events);
|
|
1182
|
-
if (last && !isTerminalGoalStatus(last.status)) {
|
|
1187
|
+
if (!this.isClosed() && last && !isTerminalGoalStatus(last.status)) {
|
|
1183
1188
|
// Start observing from the END of the historical transcript so the poller
|
|
1184
1189
|
// surfaces only post-resume transitions (we already hydrated the state).
|
|
1185
1190
|
this._goalScanOffset = endOffset;
|