@cursor/july 0.1.27 → 0.1.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/dist/bin/agent-serve.js +32 -5
- package/dist/channels/slack/auth.d.ts +13 -13
- package/dist/channels/slack/auth.d.ts.map +1 -1
- package/dist/channels/slack/auth.js +19 -10
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +3 -1
- package/dist/channels/slack/defaults.d.ts +7 -4
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +14 -4
- package/dist/channels/slack/dispatch.d.ts +6 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +59 -21
- package/dist/channels/slack/external-policy.d.ts +27 -0
- package/dist/channels/slack/external-policy.d.ts.map +1 -0
- package/dist/channels/slack/external-policy.js +53 -0
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/interactive.d.ts +6 -0
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +42 -15
- package/dist/channels/slack/slack-channel.d.ts +19 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +48 -6
- package/dist/channels/slack/types.d.ts +10 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BICLoJYF.js → app.QdunVQKg.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGe61XHA.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.wN4Ko9dT.js → VPLocalSearchBox.B6EpUXYf.js} +1 -1
- package/dist/docs/assets/chunks/{theme.dnnTE-Co.js → theme.BOTJVqh7.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
- package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
- package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
- package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
- package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +11 -7
- package/dist/docs/guides/webhooks.html +138 -5
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +5 -5
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-ax.d.ts +8 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +116 -57
- package/dist/internal/cloud-turn-cost.d.ts +34 -0
- package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
- package/dist/internal/cloud-turn-cost.js +106 -0
- package/dist/internal/cursor-slack-relay.d.ts +2 -0
- package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
- package/dist/internal/cursor-slack-relay.js +1 -0
- package/dist/internal/deploy-client.d.ts +2 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +1 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +9 -2
- package/dist/internal/event-log.d.ts +6 -0
- package/dist/internal/event-log.d.ts.map +1 -1
- package/dist/internal/event-log.js +16 -0
- package/dist/internal/host-platforms.d.ts +13 -0
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +25 -16
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +3 -1
- package/dist/internal/local-control-plane.d.ts +25 -0
- package/dist/internal/local-control-plane.d.ts.map +1 -0
- package/dist/internal/local-control-plane.js +172 -0
- package/dist/internal/platform-schedule-sync.d.ts +17 -0
- package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
- package/dist/internal/platform-schedule-sync.js +72 -0
- package/dist/internal/platform-timers.d.ts +28 -0
- package/dist/internal/platform-timers.d.ts.map +1 -0
- package/dist/internal/platform-timers.js +120 -0
- package/dist/internal/reminder-runner.d.ts +16 -12
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +112 -61
- package/dist/internal/reminder-store.d.ts +5 -4
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +5 -4
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +44 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +115 -16
- package/dist/internal/session-engine.d.ts +22 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +110 -4
- package/dist/internal/storage-coordinator.d.ts +4 -15
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +4 -30
- package/dist/playground/assets/index-CquRB-l0.js +85 -0
- package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +0 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +0 -3
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/guides/slack.md +32 -7
- package/docs/guides/webhooks.md +183 -0
- package/docs/reference/channels.md +2 -1
- package/package.json +1 -1
- package/skills/setup-slack/SKILL.md +3 -0
- package/src/bin/agent-serve.ts +33 -5
- package/src/channels/slack/auth.ts +29 -22
- package/src/channels/slack/cursor-account.ts +2 -0
- package/src/channels/slack/defaults.ts +24 -4
- package/src/channels/slack/dispatch.ts +42 -1
- package/src/channels/slack/external-policy.ts +62 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/interactive.ts +42 -10
- package/src/channels/slack/slack-channel.ts +66 -3
- package/src/channels/slack/types.ts +12 -0
- package/src/internal/cli-ax.ts +110 -34
- package/src/internal/cloud-turn-cost.ts +158 -0
- package/src/internal/cursor-slack-relay.ts +3 -0
- package/src/internal/deploy-client.ts +3 -0
- package/src/internal/discovery.ts +9 -1
- package/src/internal/event-log.ts +17 -0
- package/src/internal/host-platforms.ts +40 -18
- package/src/internal/http-channel.ts +3 -1
- package/src/internal/local-control-plane.ts +188 -0
- package/src/internal/platform-schedule-sync.ts +81 -0
- package/src/internal/platform-timers.ts +173 -0
- package/src/internal/reminder-runner.ts +131 -67
- package/src/internal/reminder-store.ts +5 -4
- package/src/internal/sdk-runner.ts +49 -1
- package/src/internal/server.ts +130 -11
- package/src/internal/session-engine.ts +138 -3
- package/src/internal/storage-coordinator.ts +4 -41
- package/src/storage.ts +0 -5
- package/src/types.ts +11 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +0 -1
- package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
- package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
- package/dist/playground/assets/index-BDCeW4uc.js +0 -86
- /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
buildDefaultEvents,
|
|
44
44
|
createDefaultOnChannelPost,
|
|
45
45
|
createDefaultOnInbound,
|
|
46
|
+
resolveSlackBlockExternals,
|
|
46
47
|
} from "./defaults.js";
|
|
47
48
|
import {
|
|
48
49
|
dispatchInboundFromEventBody,
|
|
@@ -66,9 +67,53 @@ import {
|
|
|
66
67
|
assertSlackCursorAgentName,
|
|
67
68
|
type SlackChannelOptions,
|
|
68
69
|
type SlackChannelState,
|
|
70
|
+
type SlackCredentials,
|
|
69
71
|
type SlackCursorAgentName,
|
|
70
72
|
} from "./types.js";
|
|
71
73
|
|
|
74
|
+
/** Options used to construct a slack channel — for host-level credential binding. */
|
|
75
|
+
const slackChannelOptionsByDefinition = new WeakMap<
|
|
76
|
+
object,
|
|
77
|
+
SlackChannelOptions
|
|
78
|
+
>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recover the {@link SlackChannelOptions} used to build a channel definition,
|
|
82
|
+
* if it was created via {@link slackChannel}.
|
|
83
|
+
*/
|
|
84
|
+
export function getSlackChannelOptions(
|
|
85
|
+
definition: object
|
|
86
|
+
): SlackChannelOptions | undefined {
|
|
87
|
+
return slackChannelOptionsByDefinition.get(definition);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Dedicated Socket Mode credentials for `ctx.host.slack.getClient()`. */
|
|
91
|
+
export interface SlackChannelHostBinding {
|
|
92
|
+
credentials: SlackCredentials;
|
|
93
|
+
envPrefix?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* First dedicated (Socket Mode) Slack channel on a project. Skips
|
|
98
|
+
* `cursorAccount` channels — those have no local bot token for
|
|
99
|
+
* {@link HostContext.slack.getClient}.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveDedicatedSlackHostBinding(
|
|
102
|
+
channels: ReadonlyArray<{ definition: object }>
|
|
103
|
+
): SlackChannelHostBinding | undefined {
|
|
104
|
+
for (const channel of channels) {
|
|
105
|
+
const options = slackChannelOptionsByDefinition.get(channel.definition);
|
|
106
|
+
if (options === undefined || options.cursorAccount === true) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
credentials: options.credentials ?? credentialsFromEnv(options.envPrefix),
|
|
111
|
+
envPrefix: options.envPrefix,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
72
117
|
/**
|
|
73
118
|
* Create a Slack channel. Author as `agent/channels/slack.ts` default export.
|
|
74
119
|
*
|
|
@@ -152,11 +197,14 @@ export function slackChannel<const Name extends string = string>(
|
|
|
152
197
|
: (options.credentials ?? credentialsFromEnv(options.envPrefix));
|
|
153
198
|
const envKeys = slackEnvKeys(options.envPrefix);
|
|
154
199
|
const loadingMessages = options.loadingMessages;
|
|
155
|
-
const
|
|
200
|
+
const policy = options.policy;
|
|
201
|
+
const blockExternals = resolveSlackBlockExternals(policy);
|
|
202
|
+
const defaultInbound = createDefaultOnInbound(loadingMessages, policy);
|
|
156
203
|
const onAppMention = options.onAppMention ?? defaultInbound;
|
|
157
204
|
const onDirectMessage = options.onDirectMessage ?? defaultInbound;
|
|
158
205
|
const onChannelPost =
|
|
159
|
-
options.onChannelPost ??
|
|
206
|
+
options.onChannelPost ??
|
|
207
|
+
createDefaultOnChannelPost(loadingMessages, policy);
|
|
160
208
|
const engagement = options.engagement ?? {};
|
|
161
209
|
const mentionsEnabled = engagement.mentions !== false;
|
|
162
210
|
const directMessagesEnabled = engagement.directMessages !== false;
|
|
@@ -237,6 +285,8 @@ export function slackChannel<const Name extends string = string>(
|
|
|
237
285
|
mentionsEnabled,
|
|
238
286
|
directMessagesEnabled,
|
|
239
287
|
evalCommandsEnabled,
|
|
288
|
+
// cursorAccount: backend router already refuses externals.
|
|
289
|
+
blockExternals: cursorAccount ? false : blockExternals,
|
|
240
290
|
...(args.hasContinuationSession === undefined
|
|
241
291
|
? {}
|
|
242
292
|
: { hasContinuationSession: args.hasContinuationSession }),
|
|
@@ -329,10 +379,15 @@ export function slackChannel<const Name extends string = string>(
|
|
|
329
379
|
|
|
330
380
|
let botUserId: string | undefined;
|
|
331
381
|
let botId: string | undefined;
|
|
382
|
+
let installTeamId: string | undefined;
|
|
332
383
|
let webClient: InstanceType<
|
|
333
384
|
typeof import("@slack/web-api").WebClient
|
|
334
385
|
> | null = null;
|
|
335
|
-
if (
|
|
386
|
+
if (
|
|
387
|
+
channelPosts !== undefined ||
|
|
388
|
+
botMentions !== undefined ||
|
|
389
|
+
routeInteractive
|
|
390
|
+
) {
|
|
336
391
|
try {
|
|
337
392
|
const { WebClient } = await import("@slack/web-api");
|
|
338
393
|
webClient = new WebClient(botToken);
|
|
@@ -341,6 +396,8 @@ export function slackChannel<const Name extends string = string>(
|
|
|
341
396
|
typeof identity.user_id === "string" ? identity.user_id : undefined;
|
|
342
397
|
botId =
|
|
343
398
|
typeof identity.bot_id === "string" ? identity.bot_id : undefined;
|
|
399
|
+
installTeamId =
|
|
400
|
+
typeof identity.team_id === "string" ? identity.team_id : undefined;
|
|
344
401
|
} catch (error) {
|
|
345
402
|
getLog().error("bot identity lookup degraded", {
|
|
346
403
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -416,6 +473,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
416
473
|
artifacts: args.artifacts,
|
|
417
474
|
log: getLog(),
|
|
418
475
|
evalCommandsEnabled,
|
|
476
|
+
blockExternals,
|
|
419
477
|
...(args.hasContinuationSession === undefined
|
|
420
478
|
? {}
|
|
421
479
|
: {
|
|
@@ -491,6 +549,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
491
549
|
resolveApproval: args.resolveApproval,
|
|
492
550
|
waitUntil: args.waitUntil,
|
|
493
551
|
log: getLog(),
|
|
552
|
+
blockExternals,
|
|
494
553
|
}
|
|
495
554
|
);
|
|
496
555
|
if (handledAsApproval || onBlockAction === undefined) {
|
|
@@ -503,6 +562,8 @@ export function slackChannel<const Name extends string = string>(
|
|
|
503
562
|
host: args.host,
|
|
504
563
|
artifacts: args.artifacts,
|
|
505
564
|
log: getLog(),
|
|
565
|
+
blockExternals,
|
|
566
|
+
installTeamId,
|
|
506
567
|
hasContinuationSession: args.hasContinuationSession,
|
|
507
568
|
isContinuationBusy: args.isContinuationBusy,
|
|
508
569
|
getContinuationLastBotMessageTs:
|
|
@@ -542,6 +603,8 @@ export function slackChannel<const Name extends string = string>(
|
|
|
542
603
|
},
|
|
543
604
|
});
|
|
544
605
|
|
|
606
|
+
slackChannelOptionsByDefinition.set(definition, options);
|
|
607
|
+
|
|
545
608
|
if (accountTransport !== undefined) {
|
|
546
609
|
// Serve-time seam: the host discovers cursor-account channels through
|
|
547
610
|
// this registry, binds the outbound transport, and dispatches relayed
|
|
@@ -432,8 +432,20 @@ export function assertSlackCursorAgentName(name: string): void {
|
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/** `"thread"` = anyone in the thread (default); `"author"` = session starter only. */
|
|
436
|
+
export type SlackRespondTo = "thread" | "author";
|
|
437
|
+
|
|
438
|
+
export interface SlackAccessPolicy {
|
|
439
|
+
/** Defaults to `"thread"`. Applied by the default inbound handlers. */
|
|
440
|
+
respondTo?: SlackRespondTo;
|
|
441
|
+
/** Refuse Slack Connect / guest / other-workspace senders. Defaults to `true`. */
|
|
442
|
+
blockExternals?: boolean;
|
|
443
|
+
}
|
|
444
|
+
|
|
435
445
|
export interface SlackChannelOptions {
|
|
436
446
|
credentials?: SlackCredentials;
|
|
447
|
+
/** Defaults to `{ respondTo: "thread", blockExternals: true }`. */
|
|
448
|
+
policy?: SlackAccessPolicy;
|
|
437
449
|
/**
|
|
438
450
|
* Per-agent env prefix for tokens. `envPrefix: "MY_AGENT"` reads
|
|
439
451
|
* `MY_AGENT_SLACK_BOT_TOKEN` + `MY_AGENT_SLACK_APP_TOKEN`. Prefer this over
|
package/src/internal/cli-ax.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { Artifacts, JUnit } from "../evals/reporters.js";
|
|
14
14
|
import type {
|
|
15
15
|
EvalAssertionResult,
|
|
@@ -122,6 +122,12 @@ export interface AxCliOptions {
|
|
|
122
122
|
skipReport?: boolean;
|
|
123
123
|
/** `eval`: override `evals.config.ts` `maxConcurrency`. */
|
|
124
124
|
maxConcurrency?: number;
|
|
125
|
+
/**
|
|
126
|
+
* `eval` / `eval status <runId>`: also write the full results JSON to this
|
|
127
|
+
* path. `session`: write the raw NDJSON trace (same format as `run
|
|
128
|
+
* --events` and the playground download) to this path instead of printing.
|
|
129
|
+
*/
|
|
130
|
+
out?: string;
|
|
125
131
|
/** Scaffold target directory for `init`. */
|
|
126
132
|
initTarget?: string;
|
|
127
133
|
/** Tool name for `call`. */
|
|
@@ -794,6 +800,18 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
|
|
|
794
800
|
return 2;
|
|
795
801
|
}
|
|
796
802
|
}
|
|
803
|
+
if (trimNonEmpty(options.out) !== undefined) {
|
|
804
|
+
if (options.list === true) {
|
|
805
|
+
process.stderr.write("--out and --list are mutually exclusive\n");
|
|
806
|
+
return 2;
|
|
807
|
+
}
|
|
808
|
+
if (options.noWait === true) {
|
|
809
|
+
process.stderr.write(
|
|
810
|
+
`--out needs the finished results; drop --no-wait, or poll later with ${CLI_COMMAND_NAME} eval status <evalId> --out <path>\n`
|
|
811
|
+
);
|
|
812
|
+
return 2;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
797
815
|
|
|
798
816
|
const filterIds =
|
|
799
817
|
options.evalIds === undefined || options.evalIds.length === 0
|
|
@@ -888,7 +906,12 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
|
|
|
888
906
|
process.stderr.write("No matching evals found.\n");
|
|
889
907
|
return 2;
|
|
890
908
|
}
|
|
891
|
-
return printEvalResults(
|
|
909
|
+
return await printEvalResults(
|
|
910
|
+
results,
|
|
911
|
+
options,
|
|
912
|
+
undefined,
|
|
913
|
+
artifactsDir
|
|
914
|
+
);
|
|
892
915
|
} finally {
|
|
893
916
|
progress?.close();
|
|
894
917
|
}
|
|
@@ -944,7 +967,7 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
|
|
|
944
967
|
process.stderr.write("No matching evals found.\n");
|
|
945
968
|
return 2;
|
|
946
969
|
}
|
|
947
|
-
return printEvalResults(results, options, {
|
|
970
|
+
return await printEvalResults(results, options, {
|
|
948
971
|
runId: snapshot.runId,
|
|
949
972
|
status: snapshot.status,
|
|
950
973
|
error: snapshot.error,
|
|
@@ -1049,8 +1072,8 @@ async function printEvalAccepted(
|
|
|
1049
1072
|
agentUrl
|
|
1050
1073
|
);
|
|
1051
1074
|
if (options.json) {
|
|
1052
|
-
|
|
1053
|
-
JSON.stringify(
|
|
1075
|
+
process.stdout.write(
|
|
1076
|
+
`${JSON.stringify(
|
|
1054
1077
|
{
|
|
1055
1078
|
ok: true,
|
|
1056
1079
|
accepted: true,
|
|
@@ -1064,7 +1087,7 @@ async function printEvalAccepted(
|
|
|
1064
1087
|
},
|
|
1065
1088
|
null,
|
|
1066
1089
|
2
|
|
1067
|
-
)
|
|
1090
|
+
)}\n`
|
|
1068
1091
|
);
|
|
1069
1092
|
} else {
|
|
1070
1093
|
const durable = started.config.durableRuns === true ? " (persisted)" : "";
|
|
@@ -1168,7 +1191,7 @@ export async function cmdEvalCancel(
|
|
|
1168
1191
|
fetchImpl: options.fetchImpl,
|
|
1169
1192
|
});
|
|
1170
1193
|
if (options.json) {
|
|
1171
|
-
|
|
1194
|
+
process.stdout.write(`${JSON.stringify(snap, null, 2)}\n`);
|
|
1172
1195
|
} else {
|
|
1173
1196
|
process.stdout.write(
|
|
1174
1197
|
`Cancelled Eval ID ${snap.runId} (${snap.summary.done}/${snap.summary.total} cases finalized)\n`
|
|
@@ -1214,6 +1237,13 @@ export async function cmdEvalStatus(
|
|
|
1214
1237
|
process.stderr.write("eval status does not accept --list or --no-wait\n");
|
|
1215
1238
|
return 2;
|
|
1216
1239
|
}
|
|
1240
|
+
if (
|
|
1241
|
+
trimNonEmpty(options.out) !== undefined &&
|
|
1242
|
+
(runId === undefined || runId === "")
|
|
1243
|
+
) {
|
|
1244
|
+
process.stderr.write("eval status --out requires an eval id\n");
|
|
1245
|
+
return 2;
|
|
1246
|
+
}
|
|
1217
1247
|
|
|
1218
1248
|
const target = await resolveEvalTarget(options);
|
|
1219
1249
|
if (typeof target === "number") {
|
|
@@ -1232,7 +1262,9 @@ export async function cmdEvalStatus(
|
|
|
1232
1262
|
fetchImpl: options.fetchImpl,
|
|
1233
1263
|
});
|
|
1234
1264
|
if (options.json) {
|
|
1235
|
-
|
|
1265
|
+
process.stdout.write(
|
|
1266
|
+
`${JSON.stringify({ runs, activeRunId }, null, 2)}\n`
|
|
1267
|
+
);
|
|
1236
1268
|
} else if (runs.length === 0) {
|
|
1237
1269
|
process.stdout.write("No eval runs on this agent.\n");
|
|
1238
1270
|
} else {
|
|
@@ -1252,8 +1284,12 @@ export async function cmdEvalStatus(
|
|
|
1252
1284
|
auth,
|
|
1253
1285
|
fetchImpl: options.fetchImpl,
|
|
1254
1286
|
});
|
|
1287
|
+
const outPath = await writeJsonOutFile(options.out, snap);
|
|
1255
1288
|
if (options.json) {
|
|
1256
|
-
|
|
1289
|
+
process.stdout.write(`${JSON.stringify(snap, null, 2)}\n`);
|
|
1290
|
+
if (outPath !== undefined) {
|
|
1291
|
+
process.stderr.write(`results: ${outPath}\n`);
|
|
1292
|
+
}
|
|
1257
1293
|
} else {
|
|
1258
1294
|
process.stdout.write(`${formatEvalRunSummaryLine(snap)}\n`);
|
|
1259
1295
|
if (snap.error !== undefined && snap.error !== "") {
|
|
@@ -1271,6 +1307,9 @@ export async function cmdEvalStatus(
|
|
|
1271
1307
|
process.stdout.write(` error: ${c.error}\n`);
|
|
1272
1308
|
}
|
|
1273
1309
|
}
|
|
1310
|
+
if (outPath !== undefined) {
|
|
1311
|
+
process.stdout.write(`results: ${outPath}\n`);
|
|
1312
|
+
}
|
|
1274
1313
|
}
|
|
1275
1314
|
if (snap.status === "running") {
|
|
1276
1315
|
return 3;
|
|
@@ -1333,7 +1372,7 @@ function printListedEvals(
|
|
|
1333
1372
|
emptyMessage: string
|
|
1334
1373
|
): number {
|
|
1335
1374
|
if (options.json) {
|
|
1336
|
-
|
|
1375
|
+
process.stdout.write(`${JSON.stringify(listed, null, 2)}\n`);
|
|
1337
1376
|
} else if (listed.length === 0) {
|
|
1338
1377
|
process.stdout.write(`${emptyMessage}\n`);
|
|
1339
1378
|
} else {
|
|
@@ -1438,7 +1477,7 @@ const EVAL_VERDICT_LABEL: Record<EvalVerdict, string> = {
|
|
|
1438
1477
|
skipped: "SKIP",
|
|
1439
1478
|
};
|
|
1440
1479
|
|
|
1441
|
-
function printEvalResults(
|
|
1480
|
+
async function printEvalResults(
|
|
1442
1481
|
results: EvalRunResult[],
|
|
1443
1482
|
options: AxCliOptions,
|
|
1444
1483
|
batch?: {
|
|
@@ -1447,7 +1486,7 @@ function printEvalResults(
|
|
|
1447
1486
|
error?: string;
|
|
1448
1487
|
},
|
|
1449
1488
|
artifactsDir?: string
|
|
1450
|
-
): number {
|
|
1489
|
+
): Promise<number> {
|
|
1451
1490
|
const strict = options.strict === true;
|
|
1452
1491
|
const counts = { passed: 0, failed: 0, scored: 0, skipped: 0 };
|
|
1453
1492
|
for (const result of results) {
|
|
@@ -1462,28 +1501,29 @@ function printEvalResults(
|
|
|
1462
1501
|
process.stderr.write(`eval run error: ${batch.error}\n`);
|
|
1463
1502
|
}
|
|
1464
1503
|
|
|
1504
|
+
const payload = {
|
|
1505
|
+
ok,
|
|
1506
|
+
passed: counts.passed,
|
|
1507
|
+
failed: counts.failed,
|
|
1508
|
+
scored: counts.scored,
|
|
1509
|
+
skipped: counts.skipped,
|
|
1510
|
+
strict,
|
|
1511
|
+
results,
|
|
1512
|
+
...(artifactsDir === undefined ? {} : { artifactsDir }),
|
|
1513
|
+
...(batch?.runId === undefined ? {} : { runId: batch.runId }),
|
|
1514
|
+
...(batch?.status === undefined ? {} : { status: batch.status }),
|
|
1515
|
+
...(batch?.error === undefined || batch.error === ""
|
|
1516
|
+
? {}
|
|
1517
|
+
: { error: batch.error }),
|
|
1518
|
+
};
|
|
1519
|
+
const outPath = await writeJsonOutFile(options.out, payload);
|
|
1520
|
+
|
|
1465
1521
|
if (options.json) {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
failed: counts.failed,
|
|
1472
|
-
scored: counts.scored,
|
|
1473
|
-
skipped: counts.skipped,
|
|
1474
|
-
strict,
|
|
1475
|
-
results,
|
|
1476
|
-
...(artifactsDir === undefined ? {} : { artifactsDir }),
|
|
1477
|
-
...(batch?.runId === undefined ? {} : { runId: batch.runId }),
|
|
1478
|
-
...(batch?.status === undefined ? {} : { status: batch.status }),
|
|
1479
|
-
...(batch?.error === undefined || batch.error === ""
|
|
1480
|
-
? {}
|
|
1481
|
-
: { error: batch.error }),
|
|
1482
|
-
},
|
|
1483
|
-
null,
|
|
1484
|
-
2
|
|
1485
|
-
)
|
|
1486
|
-
);
|
|
1522
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1523
|
+
if (outPath !== undefined) {
|
|
1524
|
+
// stdout stays pure JSON; report the file on stderr.
|
|
1525
|
+
process.stderr.write(`results: ${outPath}\n`);
|
|
1526
|
+
}
|
|
1487
1527
|
} else {
|
|
1488
1528
|
for (const r of results) {
|
|
1489
1529
|
const verdict = evalVerdictOf(r);
|
|
@@ -1532,10 +1572,31 @@ function printEvalResults(
|
|
|
1532
1572
|
if (artifactsDir !== undefined) {
|
|
1533
1573
|
process.stdout.write(`artifacts: ${artifactsDir}\n`);
|
|
1534
1574
|
}
|
|
1575
|
+
if (outPath !== undefined) {
|
|
1576
|
+
process.stdout.write(`results: ${outPath}\n`);
|
|
1577
|
+
}
|
|
1535
1578
|
}
|
|
1536
1579
|
return ok ? 0 : 1;
|
|
1537
1580
|
}
|
|
1538
1581
|
|
|
1582
|
+
/**
|
|
1583
|
+
* `--out <path>`: write a JSON payload (results / run snapshot), creating
|
|
1584
|
+
* parent directories. Returns the resolved path, or undefined when unset.
|
|
1585
|
+
*/
|
|
1586
|
+
async function writeJsonOutFile(
|
|
1587
|
+
out: string | undefined,
|
|
1588
|
+
payload: unknown
|
|
1589
|
+
): Promise<string | undefined> {
|
|
1590
|
+
const trimmed = trimNonEmpty(out);
|
|
1591
|
+
if (trimmed === undefined) {
|
|
1592
|
+
return undefined;
|
|
1593
|
+
}
|
|
1594
|
+
const target = resolve(trimmed);
|
|
1595
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1596
|
+
await writeFile(target, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1597
|
+
return target;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1539
1600
|
/**
|
|
1540
1601
|
* One assertion line. Soft assertions read `~` rather than `✗` when they miss,
|
|
1541
1602
|
* so a tracked score is visibly different from a failed gate.
|
|
@@ -1721,6 +1782,8 @@ export async function cmdSessions(options: AxCliOptions): Promise<number> {
|
|
|
1721
1782
|
/**
|
|
1722
1783
|
* `session <id>` — view a full session (trajectory text by default).
|
|
1723
1784
|
* `--json` → trajectory JSON; `--events` (`rawEvents`) → raw event list.
|
|
1785
|
+
* `--out <path>` → write the raw NDJSON trace to a file instead of printing
|
|
1786
|
+
* (same format as `run --events` and the playground download).
|
|
1724
1787
|
* Interactive reattach: `resume [id]` / `chat --resume` (or `chat --session`).
|
|
1725
1788
|
*/
|
|
1726
1789
|
export async function cmdSession(
|
|
@@ -1729,7 +1792,7 @@ export async function cmdSession(
|
|
|
1729
1792
|
): Promise<number> {
|
|
1730
1793
|
if (sessionId === undefined || sessionId.trim() === "") {
|
|
1731
1794
|
process.stderr.write(
|
|
1732
|
-
`Usage: ${CLI_COMMAND_NAME} session <sessionId> [--url | --prod] [--json | --text | --events]\n`
|
|
1795
|
+
`Usage: ${CLI_COMMAND_NAME} session <sessionId> [--url | --prod] [--json | --text | --events] [--out <file.ndjson>]\n`
|
|
1733
1796
|
);
|
|
1734
1797
|
return 2;
|
|
1735
1798
|
}
|
|
@@ -1754,6 +1817,19 @@ export async function cmdSession(
|
|
|
1754
1817
|
fetchImpl: options.fetchImpl,
|
|
1755
1818
|
});
|
|
1756
1819
|
|
|
1820
|
+
const outPath = trimNonEmpty(options.out);
|
|
1821
|
+
if (outPath !== undefined) {
|
|
1822
|
+
const targetPath = resolve(outPath);
|
|
1823
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
1824
|
+
await writeFile(
|
|
1825
|
+
targetPath,
|
|
1826
|
+
`${events.map((e) => JSON.stringify(e)).join("\n")}\n`,
|
|
1827
|
+
"utf8"
|
|
1828
|
+
);
|
|
1829
|
+
process.stdout.write(`trace: ${targetPath} (${events.length} events)\n`);
|
|
1830
|
+
return 0;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1757
1833
|
if (options.rawEvents === true) {
|
|
1758
1834
|
process.stdout.write(
|
|
1759
1835
|
`${JSON.stringify({ sessionId, events }, null, 2)}\n`
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recover token usage / estimated cost for cloud turns that finished without
|
|
3
|
+
* either field on `turn.completed`.
|
|
4
|
+
*
|
|
5
|
+
* Cloud `run.wait()` often drops streamed usage after SSE reconnect, and
|
|
6
|
+
* older engines never persisted cost. The playground Trace tab only renders
|
|
7
|
+
* what is on the event, so historical sessions stay blank unless we backfill
|
|
8
|
+
* from `Agent.getUsage` when the stream is read.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ModelSetting,
|
|
13
|
+
SessionEvent,
|
|
14
|
+
TurnCost,
|
|
15
|
+
TurnUsage,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { estimateTurnCost } from "./model-pricing.js";
|
|
18
|
+
|
|
19
|
+
export interface CloudRunUsage {
|
|
20
|
+
runId: string;
|
|
21
|
+
usage: TurnUsage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CloudAgentUsage {
|
|
25
|
+
usage: TurnUsage;
|
|
26
|
+
runs: CloudRunUsage[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function usageTokenSum(usage: TurnUsage | undefined): number {
|
|
30
|
+
if (usage === undefined) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
usage.inputTokens +
|
|
35
|
+
usage.outputTokens +
|
|
36
|
+
usage.cacheReadTokens +
|
|
37
|
+
usage.cacheWriteTokens
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isUnpricedCompletedTurn(event: SessionEvent): boolean {
|
|
42
|
+
return (
|
|
43
|
+
event.type === "turn.completed" &&
|
|
44
|
+
usageTokenSum(event.data.usage) <= 0 &&
|
|
45
|
+
event.data.cost === undefined
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function cloudSdkAgentIdFromEvents(
|
|
50
|
+
events: ReadonlyArray<SessionEvent>
|
|
51
|
+
): string | undefined {
|
|
52
|
+
let sdkAgentId: string | undefined;
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.type !== "agent.bound") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const id = event.data.sdkAgentId.trim();
|
|
58
|
+
if (id.startsWith("bc-")) {
|
|
59
|
+
sdkAgentId = id;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return sdkAgentId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Copy usage/cost onto unpriced `turn.completed` events. Returns undefined
|
|
67
|
+
* when the snapshot cannot be attributed safely (multiple unmatched turns).
|
|
68
|
+
*/
|
|
69
|
+
export function applyCloudUsageToEvents(
|
|
70
|
+
events: ReadonlyArray<SessionEvent>,
|
|
71
|
+
billed: CloudAgentUsage,
|
|
72
|
+
model: ModelSetting
|
|
73
|
+
): SessionEvent[] | undefined {
|
|
74
|
+
const completedIndexes: number[] = [];
|
|
75
|
+
const unpricedIndexes: number[] = [];
|
|
76
|
+
for (let i = 0; i < events.length; i++) {
|
|
77
|
+
const event = events[i];
|
|
78
|
+
if (event === undefined || event.type !== "turn.completed") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
completedIndexes.push(i);
|
|
82
|
+
if (isUnpricedCompletedTurn(event)) {
|
|
83
|
+
unpricedIndexes.push(i);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (unpricedIndexes.length === 0) {
|
|
87
|
+
return [...events];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const patches = new Map<
|
|
91
|
+
number,
|
|
92
|
+
{ usage: TurnUsage; cost: TurnCost | undefined }
|
|
93
|
+
>();
|
|
94
|
+
if (
|
|
95
|
+
unpricedIndexes.length === 1 &&
|
|
96
|
+
completedIndexes.length === 1 &&
|
|
97
|
+
usageTokenSum(billed.usage) > 0
|
|
98
|
+
) {
|
|
99
|
+
const index = unpricedIndexes[0];
|
|
100
|
+
if (index !== undefined) {
|
|
101
|
+
patches.set(index, {
|
|
102
|
+
usage: billed.usage,
|
|
103
|
+
cost: estimateTurnCost(model, billed.usage),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} else if (
|
|
107
|
+
billed.runs.length === unpricedIndexes.length &&
|
|
108
|
+
billed.runs.every((run) => usageTokenSum(run.usage) > 0)
|
|
109
|
+
) {
|
|
110
|
+
for (let i = 0; i < unpricedIndexes.length; i++) {
|
|
111
|
+
const index = unpricedIndexes[i];
|
|
112
|
+
const run = billed.runs[i];
|
|
113
|
+
if (index === undefined || run === undefined) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
patches.set(index, {
|
|
117
|
+
usage: run.usage,
|
|
118
|
+
cost: estimateTurnCost(model, run.usage),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return events.map((event, index) => {
|
|
126
|
+
const patch = patches.get(index);
|
|
127
|
+
if (patch === undefined || event.type !== "turn.completed") {
|
|
128
|
+
return event;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...event,
|
|
132
|
+
data: {
|
|
133
|
+
...event.data,
|
|
134
|
+
usage: patch.usage,
|
|
135
|
+
...(patch.cost === undefined ? {} : { cost: patch.cost }),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function mapSdkTokenUsage(usage: {
|
|
142
|
+
inputTokens: number;
|
|
143
|
+
outputTokens: number;
|
|
144
|
+
cacheReadTokens: number;
|
|
145
|
+
cacheWriteTokens: number;
|
|
146
|
+
reasoningTokens?: number;
|
|
147
|
+
}): TurnUsage {
|
|
148
|
+
const mapped: TurnUsage = {
|
|
149
|
+
inputTokens: usage.inputTokens,
|
|
150
|
+
outputTokens: usage.outputTokens,
|
|
151
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
152
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
153
|
+
};
|
|
154
|
+
if (usage.reasoningTokens !== undefined) {
|
|
155
|
+
mapped.reasoningTokens = usage.reasoningTokens;
|
|
156
|
+
}
|
|
157
|
+
return mapped;
|
|
158
|
+
}
|
|
@@ -23,6 +23,8 @@ export interface CursorSlackAgentRegistration {
|
|
|
23
23
|
name: string;
|
|
24
24
|
iconUrl?: string;
|
|
25
25
|
iconEmoji?: string;
|
|
26
|
+
/** Opt out of the default external-sender block. */
|
|
27
|
+
blockExternals?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface RegistrationResponse {
|
|
@@ -160,6 +162,7 @@ export class CursorSlackRelay {
|
|
|
160
162
|
name: agent.name,
|
|
161
163
|
iconUrl: agent.iconUrl,
|
|
162
164
|
iconEmoji: agent.iconEmoji,
|
|
165
|
+
blockExternals: agent.blockExternals === false ? false : undefined,
|
|
163
166
|
})),
|
|
164
167
|
};
|
|
165
168
|
},
|
|
@@ -148,6 +148,8 @@ export interface Deployment {
|
|
|
148
148
|
engineGeneration?: number | null;
|
|
149
149
|
publicId?: string;
|
|
150
150
|
engineUrl?: string | null;
|
|
151
|
+
/** Anyrun pod id for the active engine, when coordinates are known. */
|
|
152
|
+
enginePodId?: string | null;
|
|
151
153
|
lastError?: string | null;
|
|
152
154
|
engineAlias: string;
|
|
153
155
|
desiredUpdatedAt?: string;
|
|
@@ -259,6 +261,7 @@ const deploymentSchema = z.object({
|
|
|
259
261
|
engineGeneration: z.number().nullable().optional(),
|
|
260
262
|
publicId: z.string().optional(),
|
|
261
263
|
engineUrl: z.string().nullable().optional(),
|
|
264
|
+
enginePodId: z.string().nullable().optional(),
|
|
262
265
|
lastError: z.string().nullable().optional(),
|
|
263
266
|
engineAlias: z.string(),
|
|
264
267
|
desiredUpdatedAt: z.string().optional(),
|
|
@@ -1753,6 +1753,7 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
|
|
|
1753
1753
|
];
|
|
1754
1754
|
return {
|
|
1755
1755
|
name: project.name,
|
|
1756
|
+
description: agent.description,
|
|
1756
1757
|
model: agent.model,
|
|
1757
1758
|
runtime: agent.runtime,
|
|
1758
1759
|
cloud:
|
|
@@ -2000,7 +2001,14 @@ function isModuleFile(fileName: string): boolean {
|
|
|
2000
2001
|
if (fileName.endsWith(".d.ts") || fileName.startsWith(".")) {
|
|
2001
2002
|
return false;
|
|
2002
2003
|
}
|
|
2003
|
-
|
|
2004
|
+
if (!MODULE_EXTENSIONS.has(extname(fileName))) {
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
// Colocated unit tests (Vitest/Jest convention) are not definitions.
|
|
2008
|
+
// Without this, `channels/slack.test.ts` is scanned as a channel named
|
|
2009
|
+
// "slack.test", fails NAME_PATTERN, and the whole agent refuses to serve.
|
|
2010
|
+
const stem = moduleStem(fileName);
|
|
2011
|
+
return !stem.endsWith(".test") && !stem.endsWith(".spec");
|
|
2004
2012
|
}
|
|
2005
2013
|
|
|
2006
2014
|
function moduleStem(fileName: string): string {
|
|
@@ -97,6 +97,23 @@ export class SessionEventLog {
|
|
|
97
97
|
return startIndex <= 0 ? [...this.events] : this.events.slice(startIndex);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Replace one already-recorded event in memory. Used to backfill usage/cost
|
|
102
|
+
* onto historical `turn.completed` rows without rewriting `events.ndjson`.
|
|
103
|
+
* Live subscribers are not notified — patch before opening a replay stream.
|
|
104
|
+
*/
|
|
105
|
+
replace(index: number, event: SessionEvent): boolean {
|
|
106
|
+
if (index < 0 || index >= this.events.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const current = this.events[index];
|
|
110
|
+
if (current === undefined || current.index !== index) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
this.events[index] = event;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
subscribe(listener: (event: SessionEvent) => void): () => void {
|
|
101
118
|
this.listeners.add(listener);
|
|
102
119
|
return () => {
|