@bridge4dev/runner 0.48.0 → 0.50.0

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.
@@ -0,0 +1,86 @@
1
+ /** Is this cycle the one that owns `session`, and is it still live? */
2
+ export function ownsProcess(cycle, session) {
3
+ return cycle !== undefined && session !== null && cycle.session === session;
4
+ }
5
+ /**
6
+ * What to do with a `turn_end` that arrived inside a stop cycle.
7
+ *
8
+ * - `ordinary` — nothing special: a manual Stop keeps the contract it has had
9
+ * since QA-120, including the rule that the next real failure is not hidden.
10
+ * - `stopped` — the one ending this cycle publishes: `ok`, `aborted`, and the
11
+ * facts the turn actually carried.
12
+ * - `tail` — a second (or third) ending of a turn that has already been
13
+ * closed out. The incident's `error_during_execution`, 1.2 s late.
14
+ */
15
+ export function turnEndVerdict(cycle) {
16
+ if (cycle.reason === 'user')
17
+ return 'ordinary';
18
+ if (cycle.turnEndSent)
19
+ return 'tail';
20
+ return 'stopped';
21
+ }
22
+ /**
23
+ * What to do with an `error` that arrived inside a stop cycle.
24
+ *
25
+ * The one place the difference between «the process fell over» and «the process
26
+ * was closed» has to be paid attention to, and the only honest source for it is
27
+ * the adapter's own `processGone` — never the sentence in the message, which is
28
+ * written by the CLI and changes with it.
29
+ */
30
+ export function errorVerdict(cycle, processGone) {
31
+ // A manual Stop does not close anything, so nothing here is ours.
32
+ if (cycle.reason === 'user')
33
+ return 'failure';
34
+ // We have not closed it: a process dying on its own is a real failure, and
35
+ // burying it under the pause would be the original defect wearing the fix's
36
+ // clothes. Everything else from a process we asked to stop is the stopping.
37
+ if (!cycle.closed)
38
+ return processGone ? 'failure' : 'tail';
39
+ // We closed it. Whatever it says on the way out is the closing.
40
+ return 'tail';
41
+ }
42
+ /**
43
+ * Is this event work STARTING, rather than the tail of what is being stopped?
44
+ *
45
+ * Only a genuinely new turn deserves a second stop. A tool result belonging to
46
+ * a call that was already running when the stop began is the first stop still
47
+ * finishing — firing another interrupt at it is what gave the incident two
48
+ * interrupts, two `aborting` arms and one result too few to spend them.
49
+ */
50
+ export function isNewWork(event, cycle) {
51
+ if (event.type !== 'tool')
52
+ return true;
53
+ if (event.phase === 'use')
54
+ return true;
55
+ // A result is always the tail of something, and the only question is whose.
56
+ // The tail of a call that was ALREADY RUNNING when we stopped is the stop
57
+ // finishing — the incident's seq 832, which fired a second interrupt. A
58
+ // result for a call we did not see start, on a process we asked to stop, is
59
+ // the opposite: something began after the stop and has now finished.
60
+ //
61
+ // Without a cycle there is no ownership to read, and a result on its own is
62
+ // no evidence of a turn beginning — a turn begins with a call, a thought or a
63
+ // sentence. Excluding every result regardless of whose it is is what the plan
64
+ // forbids, and this is the distinction it asks for.
65
+ if (!cycle)
66
+ return false;
67
+ if (event.toolUseId === undefined)
68
+ return false;
69
+ return !cycle.toolsAtStop.has(event.toolUseId);
70
+ }
71
+ /**
72
+ * Is this the result of a tool call that was already running when we stopped?
73
+ *
74
+ * The incident's `tool_result` at seq 832 was exactly this: the withdrawn
75
+ * question's own refusal, arriving three seconds after the stop. Kept apart
76
+ * from «a result we never saw start» rather than lumping every result together
77
+ * — the two say different things about the process, and the diagnostic log has
78
+ * to be able to say which one happened.
79
+ */
80
+ export function isStoppedToolTail(cycle, event) {
81
+ return (event.type === 'tool' &&
82
+ event.phase === 'result' &&
83
+ event.toolUseId !== undefined &&
84
+ cycle.toolsAtStop.has(event.toolUseId));
85
+ }
86
+ //# sourceMappingURL=stop-cycle.js.map
@@ -116,6 +116,12 @@ export interface SupervisorOptions {
116
116
  * suite runs on real timers. Never set in production.
117
117
  */
118
118
  rateLimitsResendMs?: number;
119
+ /**
120
+ * How long a stop waits for the ending it asked for (#373) – a test seam over
121
+ * `STOP_SETTLE_MS`, for the same reason `emptyTurnSettleMs` exists. Never set
122
+ * in production.
123
+ */
124
+ stopSettleMs?: number;
119
125
  }
120
126
  export declare class Supervisor {
121
127
  private readonly ws;
@@ -144,6 +150,8 @@ export declare class Supervisor {
144
150
  /** The window actually used — the constant, or a test's own shorter one. */
145
151
  private readonly emptyTurnSettleMs;
146
152
  private readonly rateLimitsResendMs;
153
+ /** The stop-settle window actually used — the constant, or a test's own. */
154
+ private readonly stopSettleMs;
147
155
  /** A finished session's journal is kept this long for a late reconnect. */
148
156
  private static readonly JOURNAL_TTL_MS;
149
157
  /** Backstop: events the API will never accept must not pile up forever. */
@@ -760,6 +768,90 @@ export declare class Supervisor {
760
768
  * turn it stops would bury the feed under a fact nobody asked about.
761
769
  */
762
770
  private interruptSession;
771
+ /**
772
+ * How long a stop waits for the ending that should follow it (#373).
773
+ *
774
+ * The transition out of `interrupting` is the first result, not the
775
+ * acknowledgement — an ACK only says the request was heard, and the incident
776
+ * shows an ACK and a result are different events that arrive in either order.
777
+ * This is the backstop for the case where no result comes at all: an idle
778
+ * session had no turn to end, and a CLI can always simply not answer.
779
+ */
780
+ private static readonly STOP_SETTLE_MS;
781
+ /**
782
+ * How long after one stop the next output is still read as its tail (#196).
783
+ *
784
+ * The floor under the stop cycle rather than a substitute for it: while a
785
+ * cycle is open every event joins it, and this is what keeps a burst from
786
+ * opening a NEW cycle per line in the window after one closed without closing
787
+ * its process. Far shorter than the gap before a genuinely new turn — a
788
+ * background subagent finishing — which still gets its own stop.
789
+ */
790
+ private static readonly PAUSE_BURST_MS;
791
+ /**
792
+ * Stop one agent process once, and own what stopping it produces (#373).
793
+ *
794
+ * The shape is deliberate: the interrupt is a round trip that can take half a
795
+ * minute (a CLI sitting out a provider retry answers only when it comes back
796
+ * — 28.5 seconds, measured in #357), so nothing here may be on the path the
797
+ * event pump takes. The cycle is stored before the request goes out, which is
798
+ * what makes every later request join this one instead of starting a second.
799
+ */
800
+ private beginStopCycle;
801
+ /**
802
+ * Wait a bounded time for the ending an accepted interrupt should produce.
803
+ *
804
+ * «Bounded» rather than «for ever» because two real cases produce no ending
805
+ * at all: a pause landing between turns (there was nothing to stop), and a
806
+ * CLI that takes the request and then says nothing. Leaving the cycle open
807
+ * there would hold the seat and the clock indefinitely.
808
+ */
809
+ private armStopSettle;
810
+ private clearStopSettleTimer;
811
+ /**
812
+ * The turn is over — close the process, keeping the session resumable.
813
+ *
814
+ * This is the change the whole plan is about. A pause used to leave a live
815
+ * CLI standing for up to five hours, which is where the second result came
816
+ * from; now the runner takes the ending it asked for and parks, and the next
817
+ * message (or the clock's own «continue») starts a process that resumes the
818
+ * same conversation.
819
+ */
820
+ private settleStopCycle;
821
+ /**
822
+ * The agent never said yes — say so, and say only what actually happened.
823
+ *
824
+ * Invariant 5: a stop the CLI refused must not read as a clean cancellation.
825
+ * Which sentence is true depends on what came next, so it is written here and
826
+ * not where the refusal was noticed — a line promising «its process was
827
+ * closed» over a manual Stop, which closes nothing, is the same class of lie
828
+ * as the one this ticket is about (gotcha 324).
829
+ */
830
+ private sayIfTheStopWasRefused;
831
+ /**
832
+ * Why this paused process must NOT be closed, or null when it may be.
833
+ *
834
+ * Invariant 8 lives here. `park()` calls the adapter's `stop()`, and `stop()`
835
+ * answers every pending permission «denied» in the RUNNER's name — a decision
836
+ * nobody made, in a place that keeps decisions for ninety days. Worse, the API
837
+ * sends nothing when the clock is lifted over an open card
838
+ * (`pauseInterruptedTurn` is false while `openAsks > 0`), so the session would
839
+ * simply stand there, silent, with the card gone.
840
+ *
841
+ * A conversation the CLI has not named yet is the other refusal: parking
842
+ * promises the context is saved, and there is nothing to resume from.
843
+ */
844
+ private parkRefusalUnderPause;
845
+ /**
846
+ * The one ending a stop cycle publishes (#373).
847
+ *
848
+ * `ok` unconditionally: a turn this runner stopped is not a turn that failed,
849
+ * and `turn_end{ok:false}` is what put a paused session into the terminal
850
+ * status it could never be woken out of. The facts the turn actually carried
851
+ * ride along — `limitBlocked` because the API counts refusals with it, and
852
+ * `produced` because it is what keeps the refusal counter honest (#300).
853
+ */
854
+ private publishStoppedTurn;
763
855
  /** Is this session held under a clock right now (ticket #196)? */
764
856
  private static isPaused;
765
857
  /**