@posthog/agent 2.3.304 → 2.3.306
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/dist/agent.js +28 -1
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +28 -1
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +28 -1
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/codex/codex-agent.test.ts +128 -0
- package/src/adapters/codex/codex-agent.ts +32 -0
package/package.json
CHANGED
|
@@ -107,6 +107,48 @@ describe("CodexAcpAgent", () => {
|
|
|
107
107
|
).toBe("read-only");
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
it("propagates taskRunId and fires SDK_SESSION when loading a cloud session", async () => {
|
|
111
|
+
const { agent, client } = createAgent();
|
|
112
|
+
mockCodexConnection.loadSession.mockResolvedValue({
|
|
113
|
+
modes: { currentModeId: "auto", availableModes: [] },
|
|
114
|
+
configOptions: [],
|
|
115
|
+
} satisfies Partial<LoadSessionResponse>);
|
|
116
|
+
|
|
117
|
+
await agent.loadSession({
|
|
118
|
+
sessionId: "session-1",
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
_meta: { taskRunId: "run-1", taskId: "task-1" },
|
|
121
|
+
} as never);
|
|
122
|
+
|
|
123
|
+
expect(
|
|
124
|
+
(agent as unknown as { sessionState: { taskRunId?: string } })
|
|
125
|
+
.sessionState.taskRunId,
|
|
126
|
+
).toBe("run-1");
|
|
127
|
+
expect(client.extNotification).toHaveBeenCalledWith(
|
|
128
|
+
"_posthog/sdk_session",
|
|
129
|
+
{
|
|
130
|
+
taskRunId: "run-1",
|
|
131
|
+
sessionId: "session-1",
|
|
132
|
+
adapter: "codex",
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not emit SDK_SESSION on loadSession when taskRunId is absent", async () => {
|
|
138
|
+
const { agent, client } = createAgent();
|
|
139
|
+
mockCodexConnection.loadSession.mockResolvedValue({
|
|
140
|
+
modes: { currentModeId: "auto", availableModes: [] },
|
|
141
|
+
configOptions: [],
|
|
142
|
+
} satisfies Partial<LoadSessionResponse>);
|
|
143
|
+
|
|
144
|
+
await agent.loadSession({
|
|
145
|
+
sessionId: "session-1",
|
|
146
|
+
cwd: process.cwd(),
|
|
147
|
+
} as never);
|
|
148
|
+
|
|
149
|
+
expect(client.extNotification).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
110
152
|
it("preserves the live session mode when loading an existing session", async () => {
|
|
111
153
|
const { agent } = createAgent();
|
|
112
154
|
mockCodexConnection.loadSession.mockResolvedValue({
|
|
@@ -167,6 +209,92 @@ describe("CodexAcpAgent", () => {
|
|
|
167
209
|
});
|
|
168
210
|
});
|
|
169
211
|
|
|
212
|
+
it("serializes concurrent prompts so usage accumulators are not wiped mid-turn", async () => {
|
|
213
|
+
const { agent } = createAgent();
|
|
214
|
+
mockCodexConnection.newSession.mockResolvedValue({
|
|
215
|
+
sessionId: "session-1",
|
|
216
|
+
modes: { currentModeId: "auto", availableModes: [] },
|
|
217
|
+
configOptions: [],
|
|
218
|
+
} satisfies Partial<NewSessionResponse>);
|
|
219
|
+
await agent.newSession({
|
|
220
|
+
cwd: process.cwd(),
|
|
221
|
+
_meta: { taskRunId: "run-1" },
|
|
222
|
+
} as never);
|
|
223
|
+
|
|
224
|
+
const callOrder: string[] = [];
|
|
225
|
+
let releaseA: () => void;
|
|
226
|
+
const aStarted = new Promise<void>((resolve) => {
|
|
227
|
+
releaseA = resolve;
|
|
228
|
+
});
|
|
229
|
+
let allowAResolve: () => void;
|
|
230
|
+
const aHold = new Promise<void>((resolve) => {
|
|
231
|
+
allowAResolve = resolve;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
mockCodexConnection.prompt.mockImplementationOnce(async () => {
|
|
235
|
+
callOrder.push("A:start");
|
|
236
|
+
releaseA();
|
|
237
|
+
await aHold;
|
|
238
|
+
callOrder.push("A:end");
|
|
239
|
+
return { stopReason: "end_turn" };
|
|
240
|
+
});
|
|
241
|
+
mockCodexConnection.prompt.mockImplementationOnce(async () => {
|
|
242
|
+
callOrder.push("B:start");
|
|
243
|
+
return { stopReason: "end_turn" };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const promptA = agent.prompt({
|
|
247
|
+
sessionId: "session-1",
|
|
248
|
+
prompt: [{ type: "text", text: "A" }],
|
|
249
|
+
} as never);
|
|
250
|
+
|
|
251
|
+
await aStarted;
|
|
252
|
+
|
|
253
|
+
const promptB = agent.prompt({
|
|
254
|
+
sessionId: "session-1",
|
|
255
|
+
prompt: [{ type: "text", text: "B" }],
|
|
256
|
+
} as never);
|
|
257
|
+
|
|
258
|
+
// B must not have started while A is still in-flight.
|
|
259
|
+
expect(callOrder).toEqual(["A:start"]);
|
|
260
|
+
|
|
261
|
+
allowAResolve!();
|
|
262
|
+
await Promise.all([promptA, promptB]);
|
|
263
|
+
|
|
264
|
+
expect(callOrder).toEqual(["A:start", "A:end", "B:start"]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("does not let a failing prompt block subsequent prompts", async () => {
|
|
268
|
+
const { agent } = createAgent();
|
|
269
|
+
mockCodexConnection.newSession.mockResolvedValue({
|
|
270
|
+
sessionId: "session-1",
|
|
271
|
+
modes: { currentModeId: "auto", availableModes: [] },
|
|
272
|
+
configOptions: [],
|
|
273
|
+
} satisfies Partial<NewSessionResponse>);
|
|
274
|
+
await agent.newSession({
|
|
275
|
+
cwd: process.cwd(),
|
|
276
|
+
} as never);
|
|
277
|
+
|
|
278
|
+
mockCodexConnection.prompt.mockRejectedValueOnce(new Error("boom"));
|
|
279
|
+
mockCodexConnection.prompt.mockResolvedValueOnce({
|
|
280
|
+
stopReason: "end_turn",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await expect(
|
|
284
|
+
agent.prompt({
|
|
285
|
+
sessionId: "session-1",
|
|
286
|
+
prompt: [{ type: "text", text: "A" }],
|
|
287
|
+
} as never),
|
|
288
|
+
).rejects.toThrow("boom");
|
|
289
|
+
|
|
290
|
+
await expect(
|
|
291
|
+
agent.prompt({
|
|
292
|
+
sessionId: "session-1",
|
|
293
|
+
prompt: [{ type: "text", text: "B" }],
|
|
294
|
+
} as never),
|
|
295
|
+
).resolves.toEqual({ stopReason: "end_turn" });
|
|
296
|
+
});
|
|
297
|
+
|
|
170
298
|
it("broadcasts user prompt as user_message_chunk before delegating to codex-acp", async () => {
|
|
171
299
|
const { agent, client } = createAgent();
|
|
172
300
|
// Seed an active session so prompt() has the state it expects.
|
|
@@ -145,6 +145,17 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
145
145
|
private codexProcess: CodexProcess;
|
|
146
146
|
private codexConnection: ClientSideConnection;
|
|
147
147
|
private sessionState: CodexSessionState;
|
|
148
|
+
/**
|
|
149
|
+
* FIFO serializer for prompt() calls. codex-acp and codex-rs themselves
|
|
150
|
+
* serialize submissions at the conversation level, but our adapter
|
|
151
|
+
* accumulates per-turn usage into sessionState.accumulatedUsage via the
|
|
152
|
+
* codex-client sessionUpdate handler. If two prompts ran concurrently on
|
|
153
|
+
* the JS side, the second's resetUsage() would wipe out the first's
|
|
154
|
+
* in-flight counters and both TURN_COMPLETE notifications would report
|
|
155
|
+
* garbled totals. Serializing on the JS side keeps the accumulator
|
|
156
|
+
* single-owner.
|
|
157
|
+
*/
|
|
158
|
+
private promptMutex: Promise<unknown> = Promise.resolve();
|
|
148
159
|
|
|
149
160
|
constructor(client: AgentSideConnection, options: CodexAcpAgentOptions) {
|
|
150
161
|
super(client);
|
|
@@ -267,13 +278,27 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
267
278
|
meta?.permissionMode,
|
|
268
279
|
);
|
|
269
280
|
|
|
281
|
+
// Carry taskRunId/taskId across load so prompt() still emits cloud
|
|
282
|
+
// notifications (TURN_COMPLETE, USAGE_UPDATE) after a reload. newSession
|
|
283
|
+
// and unstable_resumeSession both do this; loadSession historically did
|
|
284
|
+
// not, which silently broke task-completion tracking on re-attach.
|
|
270
285
|
this.sessionState = createSessionState(params.sessionId, params.cwd, {
|
|
286
|
+
taskRunId: meta?.taskRunId,
|
|
287
|
+
taskId: meta?.taskId ?? meta?.persistence?.taskId,
|
|
271
288
|
modeId: response.modes?.currentModeId ?? "auto",
|
|
272
289
|
permissionMode: currentPermissionMode,
|
|
273
290
|
});
|
|
274
291
|
this.sessionId = params.sessionId;
|
|
275
292
|
this.sessionState.configOptions = response.configOptions ?? [];
|
|
276
293
|
|
|
294
|
+
if (meta?.taskRunId) {
|
|
295
|
+
await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
|
|
296
|
+
taskRunId: meta.taskRunId,
|
|
297
|
+
sessionId: params.sessionId,
|
|
298
|
+
adapter: "codex",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
277
302
|
return response;
|
|
278
303
|
}
|
|
279
304
|
|
|
@@ -383,6 +408,13 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
383
408
|
}
|
|
384
409
|
|
|
385
410
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
411
|
+
const previous = this.promptMutex;
|
|
412
|
+
const next = previous.catch(() => {}).then(() => this.runPrompt(params));
|
|
413
|
+
this.promptMutex = next;
|
|
414
|
+
return next;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private async runPrompt(params: PromptRequest): Promise<PromptResponse> {
|
|
386
418
|
this.session.cancelled = false;
|
|
387
419
|
this.session.interruptReason = undefined;
|
|
388
420
|
resetUsage(this.sessionState);
|