@basein/runner 0.3.13 → 0.3.14

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.
@@ -1073,6 +1073,31 @@ export declare class ControlServer {
1073
1073
  private openFragment;
1074
1074
  /** What the fragment's create call said: `created: false` is a hit, not a recording. */
1075
1075
  private applyFragmentCreation;
1076
+ /**
1077
+ * Whether this matched turn's task is now the model's own, so its work is
1078
+ * the repeat the service offered to record (the service's newestRun.md N1):
1079
+ * offered, nothing recording, no fragment, and no plan of the prompt match
1080
+ * that ran a step or still may. A plan that handed over is the fragment's to
1081
+ * record (fallbk.md D7).
1082
+ */
1083
+ private repeatDue;
1084
+ /**
1085
+ * Open the repeat run and record from this call on (newestRun.md N1).
1086
+ *
1087
+ * The same swap as {@link openFragment}: nothing allocated before it belongs
1088
+ * to it. Unlike a fragment it carries the turn's prompt, and its cost and
1089
+ * duration are the whole turn's, because no plan did any of the task. Nothing
1090
+ * waits for the service's answer: the steps queue behind the create, and a
1091
+ * create answered `id: null` — the service records no repeat of it — drops
1092
+ * them.
1093
+ */
1094
+ private openRepeat;
1095
+ /**
1096
+ * Why the matched turn's plan did not run, as the service shows it: what
1097
+ * the service withheld it for, else the code of the gate that declined it
1098
+ * here, a plan that was late included.
1099
+ */
1100
+ private repeatReason;
1076
1101
  /** Resolve with `p`, or null once `ms` passes or it rejects. */
1077
1102
  private withinBudget;
1078
1103
  /**
@@ -697,6 +697,9 @@ export class ControlServer {
697
697
  }
698
698
  : null,
699
699
  fragment: s.run?.fragment ? { state: s.run.fragment.state } : null,
700
+ // The service's offer to record the turn as a recording's newest run,
701
+ // and what became of it (newestRun.md).
702
+ repeat: s.run?.repeat ? { state: s.run.repeat.state, of: s.run.repeat.runId } : null,
700
703
  }));
701
704
  const sending = this.recorderBacklog();
702
705
  return {
@@ -1673,6 +1676,12 @@ export class ControlServer {
1673
1676
  if (!match || run.runId !== runId)
1674
1677
  return;
1675
1678
  run.recording = false;
1679
+ // The service's offer to record the turn after all, should the model end
1680
+ // up doing the task itself (newestRun.md N5). Taken up at the model's
1681
+ // first call of its own, once no plan of this match runs (`repeatDue`).
1682
+ if (match.repeat?.runId && !run.repeat) {
1683
+ run.repeat = { runId: match.repeat.runId, withheld: match.withheld ?? undefined, state: "offered" };
1684
+ }
1676
1685
  logLine("run.matched", {
1677
1686
  run: runId,
1678
1687
  matchedRun: match.runId,
@@ -2934,6 +2943,12 @@ export class ControlServer {
2934
2943
  this.noteHandover(run);
2935
2944
  return this.preToolAnswer(this.replay.deliverInstead(note, toolName));
2936
2945
  }
2946
+ // The model is doing the matched task itself, and the service asked for
2947
+ // that run to be recorded as the recording's newest (newestRun.md N1).
2948
+ // Opened before the probe below, so the turn is recorded from this call
2949
+ // on like any recording turn — a plan an intent match arms in it too.
2950
+ if (this.repeatDue(run))
2951
+ this.openRepeat(session, run);
2937
2952
  // 2. The model is driving: look for a scenario by this iteration's intent.
2938
2953
  // Never in plan mode, where nothing may run on the user's behalf.
2939
2954
  // If the deadline answers first, it answers `{}`: the call runs as
@@ -3266,6 +3281,9 @@ export class ControlServer {
3266
3281
  });
3267
3282
  }
3268
3283
  this.noteHandover(run);
3284
+ // The model doing the matched task itself, as in Claude Code (newestRun.md N1).
3285
+ if (this.repeatDue(run))
3286
+ this.openRepeat(session, run);
3269
3287
  const words = this.hostWords(payload);
3270
3288
  // Counted, never waited on, and never arming: see `tryIntentMatch`.
3271
3289
  await this.tryIntentMatch(session, run, toolName, words.intent, modelArgs);
@@ -3546,6 +3564,10 @@ export class ControlServer {
3546
3564
  out.add(prompt);
3547
3565
  if (run.fragment?.hitRunId)
3548
3566
  out.add(run.fragment.hitRunId);
3567
+ // The recording the match offered a repeat of: the same recording, and
3568
+ // known even when the match landed too late to be armed (newestRun.md).
3569
+ if (run.repeat)
3570
+ out.add(run.repeat.runId);
3549
3571
  return [...out];
3550
3572
  }
3551
3573
  /**
@@ -3985,8 +4007,16 @@ export class ControlServer {
3985
4007
  /** What the fragment's create call said: `created: false` is a hit, not a recording. */
3986
4008
  applyFragmentCreation(run, id, created) {
3987
4009
  const f = run.fragment;
3988
- if (created.created || !f || run.runId !== id)
4010
+ if (!f || run.runId !== id)
4011
+ return;
4012
+ if (created.created) {
4013
+ // Recorded as the newest run of the fragment it is a hit on (the
4014
+ // service's newestRun.md N2): still that fragment's repeat, so no step
4015
+ // hit may be counted against it (R-HIT-7).
4016
+ if (created.fragmentOf)
4017
+ f.hitRunId = created.fragmentOf.runId;
3989
4018
  return;
4019
+ }
3990
4020
  run.recording = false;
3991
4021
  f.state = "hit";
3992
4022
  // Which run it hit matters from here on: this turn must not count step
@@ -4000,6 +4030,83 @@ export class ControlServer {
4000
4030
  why: "a hand-over at this step was recorded before — counted as a hit, not recorded",
4001
4031
  });
4002
4032
  }
4033
+ /**
4034
+ * Whether this matched turn's task is now the model's own, so its work is
4035
+ * the repeat the service offered to record (the service's newestRun.md N1):
4036
+ * offered, nothing recording, no fragment, and no plan of the prompt match
4037
+ * that ran a step or still may. A plan that handed over is the fragment's to
4038
+ * record (fallbk.md D7).
4039
+ */
4040
+ repeatDue(run) {
4041
+ const r = run.repeat;
4042
+ if (!r || r.state !== "offered" || run.recording || run.finished || run.fragment)
4043
+ return false;
4044
+ const prompt = run.replays.find((s) => s.armedBy === "prompt");
4045
+ if (!prompt?.plan)
4046
+ return true;
4047
+ return prompt.retired && prompt.stepsPinned === 0 && !prompt.handover;
4048
+ }
4049
+ /**
4050
+ * Open the repeat run and record from this call on (newestRun.md N1).
4051
+ *
4052
+ * The same swap as {@link openFragment}: nothing allocated before it belongs
4053
+ * to it. Unlike a fragment it carries the turn's prompt, and its cost and
4054
+ * duration are the whole turn's, because no plan did any of the task. Nothing
4055
+ * waits for the service's answer: the steps queue behind the create, and a
4056
+ * create answered `id: null` — the service records no repeat of it — drops
4057
+ * them.
4058
+ */
4059
+ openRepeat(session, run) {
4060
+ const r = run.repeat;
4061
+ if (!r || r.state !== "offered")
4062
+ return;
4063
+ const reason = this.repeatReason(run);
4064
+ const repeatOf = reason ? { runId: r.runId, reason } : { runId: r.runId };
4065
+ // The repeat is a run of the same host as the turn it records.
4066
+ const framework = frameworkOf(session);
4067
+ const id = this.recorder.startRun(run.input, { ...this.runMetadata(session), repeatOf }, framework ? { repeatOf, framework } : { repeatOf });
4068
+ for (const c of run.correlations.values()) {
4069
+ c.settled = true;
4070
+ if (c.fallback)
4071
+ clearTimeout(c.fallback);
4072
+ }
4073
+ run.builtIns.clear();
4074
+ run.runId = id;
4075
+ run.ordering = new StepIndexAllocator();
4076
+ run.recording = true;
4077
+ r.state = "open";
4078
+ logLine("run.repeat", {
4079
+ run: id,
4080
+ of: r.runId,
4081
+ reason,
4082
+ why: "the agent is doing a recorded task itself — recorded as that recording's newest run",
4083
+ });
4084
+ if (!isRunCreationAware(this.recorder))
4085
+ return;
4086
+ void this.recorder
4087
+ .runCreated(id)
4088
+ .catch(() => ({ created: true }))
4089
+ .then((created) => {
4090
+ if (created.created || run.runId !== id)
4091
+ return;
4092
+ run.recording = false;
4093
+ r.state = "refused";
4094
+ logLine("run.repeat_refused", {
4095
+ run: id,
4096
+ of: r.runId,
4097
+ why: "the service records no repeat of this recording — nothing recorded",
4098
+ });
4099
+ });
4100
+ }
4101
+ /**
4102
+ * Why the matched turn's plan did not run, as the service shows it: what
4103
+ * the service withheld it for, else the code of the gate that declined it
4104
+ * here, a plan that was late included.
4105
+ */
4106
+ repeatReason(run) {
4107
+ const prompt = run.replays.find((s) => s.armedBy === "prompt") ?? run.lateMatch;
4108
+ return run.repeat?.withheld ?? prompt?.declined ?? undefined;
4109
+ }
4003
4110
  /** Resolve with `p`, or null once `ms` passes or it rejects. */
4004
4111
  async withinBudget(p, ms) {
4005
4112
  let timer;
@@ -11,6 +11,16 @@ export interface StartRunOptions {
11
11
  scenarioId: string;
12
12
  stepIndex: number;
13
13
  };
14
+ /**
15
+ * Marks a *repeat* (the service's newestRun.md): a matched turn whose plan
16
+ * did not run, so the model is doing the recording's task itself. Sent only
17
+ * when the match offered it ({@link RunMatch.repeat}). `reason` is why the
18
+ * plan did not run. The service never prompt-matches a repeat.
19
+ */
20
+ repeatOf?: {
21
+ runId: string;
22
+ reason?: string;
23
+ };
14
24
  /**
15
25
  * The run's `framework`, when the session named a host that is not Claude
16
26
  * Code: its `app`, or `app:scope` (`langchain`, `langchain:billing`). The
@@ -176,6 +186,15 @@ export interface RunMatch {
176
186
  * ordinary hand-out, and from an older service.
177
187
  */
178
188
  withheld?: string | null;
189
+ /**
190
+ * The service's offer (its newestRun.md N5): should the model end up doing
191
+ * this task itself — the plan declined, withheld, late or off — record the
192
+ * turn as a repeat of `runId`, the recording's newest run. Absent or null
193
+ * from a service that does not record repeats: then nothing is recorded.
194
+ */
195
+ repeat?: {
196
+ runId: string;
197
+ } | null;
179
198
  }
180
199
  /** Optional capability: `null` means "fresh run, keep recording". */
181
200
  export interface MatchAware {
@@ -202,10 +221,18 @@ export declare function isMatchAware(r: Recorder): r is Recorder & MatchAware;
202
221
  /** What the create call said about a run: fresh, or a hit on a fragment. */
203
222
  export interface RunCreation {
204
223
  created: boolean;
205
- /** Set when this was a hit on an existing fragment (R-FALL-7, R-HIT-7). */
224
+ /**
225
+ * Set when this was a hit on an existing fragment (R-FALL-7, R-HIT-7) —
226
+ * with `created` too when the service recorded it all the same, as that
227
+ * fragment's newest run (its newestRun.md N2).
228
+ */
206
229
  fragmentOf?: {
207
230
  runId: string;
208
231
  };
232
+ /** Set when the service recorded the run as a repeat: the run it repeats. */
233
+ repeatOf?: {
234
+ runId: string;
235
+ };
209
236
  }
210
237
  export interface RunCreationAware {
211
238
  runCreated(runId: string): Promise<RunCreation>;
@@ -475,6 +475,9 @@ export class RemoteRecorder {
475
475
  // A fragment (fallbk.md D7). Omitted, not null, so an older service
476
476
  // that does not know the field sees the body it always saw.
477
477
  ...(options?.fallbackOf ? { fallbackOf: options.fallbackOf } : {}),
478
+ // A repeat (the service's newestRun.md). Sent only after a match
479
+ // that offered it, so a service that would not know it never sees it.
480
+ ...(options?.repeatOf ? { repeatOf: options.repeatOf } : {}),
478
481
  // A chain with calls is inlined for this runner rather than stopped in
479
482
  // front of its first call (segmented.md R-CALL-28).
480
483
  supportsCalls: true,
@@ -496,7 +499,11 @@ export class RemoteRecorder {
496
499
  why: "the create's first attempt failed, so the turn went ahead as a fresh one; the service kept its own run, and this one's steps are not sent",
497
500
  });
498
501
  }
499
- entry.settle(b.matched ?? null, { created: b.id !== null, fragmentOf: b.fragmentOf });
502
+ entry.settle(b.matched ?? null, {
503
+ created: b.id !== null,
504
+ fragmentOf: b.fragmentOf,
505
+ ...(b.repeatOf ? { repeatOf: b.repeatOf } : {}),
506
+ });
500
507
  }
501
508
  catch (err) {
502
509
  entry.settle(null, { created: true }); // fail-safe: on a create error, record normally
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basein/runner",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
5
5
  "type": "module",
6
6
  "license": "MIT",