@brettinternet/pi-loop 0.1.2 → 0.1.3
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/README.md +4 -2
- package/index.ts +168 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,19 +12,21 @@ pi install npm:@brettinternet/pi-loop
|
|
|
12
12
|
/loop <count>
|
|
13
13
|
/loop +<count>
|
|
14
14
|
/loop -<count>
|
|
15
|
+
/loop time <duration>
|
|
15
16
|
/loop delay <duration>
|
|
16
17
|
/loop prompt <text>
|
|
17
18
|
/loop append <text>
|
|
18
19
|
/loop status
|
|
19
20
|
/loop
|
|
21
|
+
/loop pause
|
|
20
22
|
/loop end
|
|
21
23
|
/loop resume
|
|
22
24
|
/loop next
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
Durations use `ms`, `s`, `m`, `h`, or `d`. Delays range from 1 second to 24 hours. Timed loops run for at most 30 days.
|
|
27
|
+
Durations use `ms`, `s`, `m`, `h`, or `d`. Delays range from 1 second to 24 hours. Timed loops run for at most 30 days. During a loop, `/loop time <duration>` switches to a deadline from now or resets the current deadline; timed mode requires a non-zero delay. `/loop <count>` switches back to a count of future iterations.
|
|
26
28
|
|
|
27
|
-
Errors retry after 30 seconds, 1 minute, and 2 minutes, then pause. Aborting pauses the loop.
|
|
29
|
+
Errors retry after 30 seconds, 1 minute, and 2 minutes, then pause. Aborting pauses the loop. `/loop pause` pauses after the active iteration settles; if the loop is between iterations, it pauses immediately. Resuming then starts the next iteration. An agent `loop_pause` request pauses mid-iteration for human blockers, and resuming continues that iteration. Recovery preserves the loop so you can resume or advance it.
|
|
28
30
|
|
|
29
31
|
Chain commands in the prompt:
|
|
30
32
|
|
package/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Type } from "typebox";
|
|
|
12
12
|
export const LOOP_STATE_ENTRY = "pi-loop-state-v1";
|
|
13
13
|
export const LOOP_WIDGET_KEY = "pi-loop";
|
|
14
14
|
export const LOOP_USAGE =
|
|
15
|
-
"usage: /loop <positive-count> [--delay <duration>] <prompt> | /loop for <duration> --delay <duration> <prompt> | /loop <positive-count> | /loop <+|-><count> | /loop delay <duration> | /loop prompt <text> | /loop append <text> | /loop status | /loop resume | /loop next | /loop end";
|
|
15
|
+
"usage: /loop <positive-count> [--delay <duration>] <prompt> | /loop for <duration> --delay <duration> <prompt> | /loop <positive-count> | /loop <+|-><count> | /loop time <duration> | /loop delay <duration> | /loop prompt <text> | /loop append <text> | /loop status | /loop pause | /loop resume | /loop next | /loop end";
|
|
16
16
|
|
|
17
17
|
export const MIN_LOOP_DELAY_MS = 1_000;
|
|
18
18
|
export const MAX_LOOP_DELAY_MS = 24 * 60 * 60 * 1_000;
|
|
@@ -26,7 +26,7 @@ const LOOP_AGENT_GUIDANCE = `## Active Loop
|
|
|
26
26
|
|
|
27
27
|
This session is part of an active unattended loop. If no useful work can continue without human input, credentials, permissions, or another non-transient external dependency, call loop_pause with the blocker. Do not pause for a temporary condition expected to resolve in a later iteration.`;
|
|
28
28
|
|
|
29
|
-
export type LoopStatus = "active" | "stopping" | "paused" | "completed" | "stopped" | "inactive";
|
|
29
|
+
export type LoopStatus = "active" | "pausing" | "stopping" | "paused" | "completed" | "stopped" | "inactive";
|
|
30
30
|
export type LoopPhase = "running" | "waiting" | "retrying";
|
|
31
31
|
|
|
32
32
|
export interface LoopState {
|
|
@@ -54,18 +54,20 @@ export type ParsedLoopCommand =
|
|
|
54
54
|
| { kind: "startTimed"; duration: number; delay: number; prompt: string }
|
|
55
55
|
| { kind: "retune"; count: number }
|
|
56
56
|
| { kind: "adjust"; delta: number }
|
|
57
|
+
| { kind: "time"; duration: number }
|
|
57
58
|
| { kind: "delay"; delay: number }
|
|
58
59
|
| { kind: "replacePrompt"; prompt: string }
|
|
59
60
|
| { kind: "appendPrompt"; prompt: string }
|
|
60
61
|
| { kind: "status" }
|
|
62
|
+
| { kind: "pauseAtBoundary" }
|
|
61
63
|
| { kind: "resume" }
|
|
62
64
|
| { kind: "next" }
|
|
63
65
|
| { kind: "end" }
|
|
64
66
|
| { kind: "continue"; runId: string; iteration: number }
|
|
65
67
|
| { kind: "pause"; runId: string; iteration: number };
|
|
66
68
|
|
|
67
|
-
const ACTIVE_STATUSES = new Set<LoopStatus>(["active", "stopping"]);
|
|
68
|
-
const VISIBLE_STATUSES = new Set<LoopStatus>(["active", "stopping", "paused"]);
|
|
69
|
+
const ACTIVE_STATUSES = new Set<LoopStatus>(["active", "pausing", "stopping"]);
|
|
70
|
+
const VISIBLE_STATUSES = new Set<LoopStatus>(["active", "pausing", "stopping", "paused"]);
|
|
69
71
|
const TERMINAL_STATUSES = new Set<LoopStatus>(["completed", "stopped", "inactive"]);
|
|
70
72
|
|
|
71
73
|
type ArgumentCompletion = { value: string; label: string; description?: string };
|
|
@@ -100,6 +102,15 @@ function completeLoopArguments(prefix: string): ArgumentCompletion[] | null {
|
|
|
100
102
|
const delayCommand = /^(delay|--delay)(?:\s+(.*))?$/.exec(input);
|
|
101
103
|
if (delayCommand?.[2] !== undefined) return delayCompletions(prefix, delayCommand[1]);
|
|
102
104
|
|
|
105
|
+
const timeCommand = /^time(?:\s+(.*))?$/.exec(input);
|
|
106
|
+
if (timeCommand?.[1] !== undefined) {
|
|
107
|
+
return completeArguments(timeCommand[1], COMMON_LOOP_TIMEFRAMES.map((value) => ({
|
|
108
|
+
value: `time ${value}`,
|
|
109
|
+
label: `time ${value}`,
|
|
110
|
+
description: "Switch to timed mode or reset the remaining time",
|
|
111
|
+
})));
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
const timedDelay = /^for\s+(\S+)\s+--delay(=|\s+)?(.*)$/.exec(input);
|
|
104
115
|
if (timedDelay) {
|
|
105
116
|
if (timedDelay[2] !== undefined) {
|
|
@@ -164,9 +175,11 @@ function completeLoopArguments(prefix: string): ArgumentCompletion[] | null {
|
|
|
164
175
|
|
|
165
176
|
return completeArguments(prefix, [
|
|
166
177
|
{ value: "status", label: "status", description: "Show the current loop state" },
|
|
167
|
-
{ value: "
|
|
178
|
+
{ value: "pause", label: "pause", description: "Pause after the active iteration settles" },
|
|
179
|
+
{ value: "resume", label: "resume", description: "Resume a paused loop" },
|
|
168
180
|
{ value: "next", label: "next", description: "Skip a paused iteration and start the next one" },
|
|
169
181
|
{ value: "end", label: "end", description: "End the loop gracefully" },
|
|
182
|
+
{ value: "time ", label: "time <duration>", description: "Switch to timed mode or reset the remaining time" },
|
|
170
183
|
{ value: "delay ", label: "delay <duration>", description: "Set the delay between settled iterations" },
|
|
171
184
|
{ value: "prompt ", label: "prompt <text>", description: "Replace the future loop prompt" },
|
|
172
185
|
{ value: "append ", label: "append <text>", description: "Append to the future loop prompt" },
|
|
@@ -255,6 +268,10 @@ export function parseLoopCommand(args: string): ParsedLoopCommand {
|
|
|
255
268
|
if (rest) throw new Error(`end does not accept arguments; ${LOOP_USAGE}`);
|
|
256
269
|
return { kind: "end" };
|
|
257
270
|
}
|
|
271
|
+
if (first === "pause") {
|
|
272
|
+
if (rest) throw new Error(`pause does not accept arguments; ${LOOP_USAGE}`);
|
|
273
|
+
return { kind: "pauseAtBoundary" };
|
|
274
|
+
}
|
|
258
275
|
if (first === "resume") {
|
|
259
276
|
if (rest) throw new Error(`resume does not accept arguments; ${LOOP_USAGE}`);
|
|
260
277
|
return { kind: "resume" };
|
|
@@ -263,6 +280,11 @@ export function parseLoopCommand(args: string): ParsedLoopCommand {
|
|
|
263
280
|
if (rest) throw new Error(`next does not accept arguments; ${LOOP_USAGE}`);
|
|
264
281
|
return { kind: "next" };
|
|
265
282
|
}
|
|
283
|
+
if (first === "time") {
|
|
284
|
+
const fields = rest.split(/\s+/).filter(Boolean);
|
|
285
|
+
if (fields.length !== 1) throw new Error(`time requires one duration; ${LOOP_USAGE}`);
|
|
286
|
+
return { kind: "time", duration: parseLoopTimeframe(fields[0]) };
|
|
287
|
+
}
|
|
266
288
|
if (first === "delay") {
|
|
267
289
|
const fields = rest.split(/\s+/).filter(Boolean);
|
|
268
290
|
if (fields.length !== 1) throw new Error(`delay requires one duration; ${LOOP_USAGE}`);
|
|
@@ -352,6 +374,7 @@ export function parseLoopState(value: unknown): LoopState | undefined {
|
|
|
352
374
|
const status = value.status;
|
|
353
375
|
if (
|
|
354
376
|
status !== "active" &&
|
|
377
|
+
status !== "pausing" &&
|
|
355
378
|
status !== "stopping" &&
|
|
356
379
|
status !== "paused" &&
|
|
357
380
|
status !== "completed" &&
|
|
@@ -523,12 +546,13 @@ export function formatLoopWidget(state: LoopState, width: number, now = Date.now
|
|
|
523
546
|
const retries = (state.retryCount ?? 0) > 0
|
|
524
547
|
? ` · retry ${state.retryCount}/${DEFAULT_LOOP_RETRIES}`
|
|
525
548
|
: "";
|
|
526
|
-
|
|
527
|
-
|
|
549
|
+
const iteration = state.endsAt === undefined ? "" : ` · #${state.currentIteration}`;
|
|
550
|
+
if (state.status === "pausing" || state.status === "stopping") {
|
|
551
|
+
return truncateToWidth(`loop ${state.status}${iteration}${timeframe}${delay}${retries} · ${prompt}`, width, "…");
|
|
528
552
|
}
|
|
529
553
|
if (state.endsAt !== undefined) {
|
|
530
554
|
return truncateToWidth(
|
|
531
|
-
`loop ${state.status}${timeframe}${delay}${retries} · ${prompt}`,
|
|
555
|
+
`loop ${state.status}${iteration}${timeframe}${delay}${retries} · ${prompt}`,
|
|
532
556
|
width,
|
|
533
557
|
"…",
|
|
534
558
|
);
|
|
@@ -572,6 +596,16 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
572
596
|
let commandInterruptedKey: string | undefined;
|
|
573
597
|
let pendingFailure: PendingFailure | undefined;
|
|
574
598
|
let currentSessionManagerRef: unknown;
|
|
599
|
+
let herdrBlocked = false;
|
|
600
|
+
|
|
601
|
+
function reportHerdrBlocked(state: LoopState | undefined): void {
|
|
602
|
+
const blocked = state?.status === "paused";
|
|
603
|
+
if (blocked === herdrBlocked) return;
|
|
604
|
+
herdrBlocked = blocked;
|
|
605
|
+
pi.events.emit("herdr:blocked", blocked
|
|
606
|
+
? { active: true, label: `Loop paused: ${state.pauseReason ?? "human input required"}`, scope: "root" }
|
|
607
|
+
: { active: false, scope: "root" });
|
|
608
|
+
}
|
|
575
609
|
|
|
576
610
|
function stateFrom(ctx: ContextWithSession): LoopState | undefined {
|
|
577
611
|
runState = latestStateFromContext(ctx);
|
|
@@ -581,6 +615,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
581
615
|
function persist(ctx: Pick<ExtensionAPI, "appendEntry">, state: LoopState): void {
|
|
582
616
|
ctx.appendEntry(LOOP_STATE_ENTRY, state);
|
|
583
617
|
runState = state;
|
|
618
|
+
reportHerdrBlocked(state);
|
|
584
619
|
}
|
|
585
620
|
|
|
586
621
|
function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
@@ -602,6 +637,8 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
602
637
|
ctx.ui.setWidget(LOOP_WIDGET_KEY, [formatLoopWidget(state, Number.MAX_SAFE_INTEGER)], { placement: "belowEditor" });
|
|
603
638
|
return;
|
|
604
639
|
}
|
|
640
|
+
// Replacing the widget invalidates Pi's parent layout caches. Requesting a
|
|
641
|
+
// render alone can leave the previous line visible until another UI event.
|
|
605
642
|
ctx.ui.setWidget(LOOP_WIDGET_KEY, (_tui, _theme) => ({
|
|
606
643
|
render: (width) => [formatLoopWidget(state, width)],
|
|
607
644
|
invalidate: () => {},
|
|
@@ -625,8 +662,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
625
662
|
}
|
|
626
663
|
if (currentSessionManagerRef !== undefined && sessionManager !== currentSessionManagerRef) return undefined;
|
|
627
664
|
const loaded = stateFrom(ctx);
|
|
628
|
-
if (!loaded)
|
|
629
|
-
|
|
665
|
+
if (!loaded || !stateBelongsToContext(loaded, ctx)) {
|
|
666
|
+
reportHerdrBlocked(undefined);
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
reportHerdrBlocked(loaded);
|
|
630
670
|
return loaded;
|
|
631
671
|
}
|
|
632
672
|
|
|
@@ -679,7 +719,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
679
719
|
): void {
|
|
680
720
|
if (!statusIsActive(state) || transitionInFlight) return;
|
|
681
721
|
const nextBudget = state.pendingRetune ?? state.remainingBudget;
|
|
682
|
-
if (state.status === "stopping" || (state.endsAt === undefined && nextBudget <= 0) || state.delay === 0) {
|
|
722
|
+
if (state.status === "pausing" || state.status === "stopping" || (state.endsAt === undefined && nextBudget <= 0) || state.delay === 0) {
|
|
683
723
|
clearContinuationWait();
|
|
684
724
|
dispatchContinuation(ctx, state);
|
|
685
725
|
return;
|
|
@@ -971,6 +1011,27 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
971
1011
|
clearContinuationWait();
|
|
972
1012
|
clearRetryWait();
|
|
973
1013
|
|
|
1014
|
+
if (state.status === "pausing") {
|
|
1015
|
+
const {
|
|
1016
|
+
nextActionAt: _nextActionAt,
|
|
1017
|
+
pauseReason: _pauseReason,
|
|
1018
|
+
...withoutSchedule
|
|
1019
|
+
} = state;
|
|
1020
|
+
const paused: LoopState = {
|
|
1021
|
+
...withoutSchedule,
|
|
1022
|
+
status: "paused",
|
|
1023
|
+
phase: "waiting",
|
|
1024
|
+
settledAt: Date.now(),
|
|
1025
|
+
pauseReason: "paused by user",
|
|
1026
|
+
pausedAt: Date.now(),
|
|
1027
|
+
};
|
|
1028
|
+
persist(pi, paused);
|
|
1029
|
+
renderWidget(ctx, paused);
|
|
1030
|
+
runState = paused;
|
|
1031
|
+
notify(ctx, "loop paused", "info");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
974
1035
|
if (state.status === "stopping") {
|
|
975
1036
|
const stopped = { ...state, status: "stopped" as const };
|
|
976
1037
|
persist(pi, stopped);
|
|
@@ -1075,6 +1136,41 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1075
1136
|
return;
|
|
1076
1137
|
}
|
|
1077
1138
|
|
|
1139
|
+
if (parsed.kind === "time") {
|
|
1140
|
+
if (!state || isTerminal(state)) {
|
|
1141
|
+
notify(ctx, "a loop must be active, stopping, or paused to update its remaining time", "error");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (state.delay === 0) {
|
|
1145
|
+
notify(ctx, "timed loops require a non-zero delay; set /loop delay <duration> first", "error");
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const switchingModes = state.endsAt === undefined;
|
|
1149
|
+
const endsAt = Date.now() + parsed.duration;
|
|
1150
|
+
const nextActionAt = state.phase === "waiting" && state.nextActionAt !== undefined
|
|
1151
|
+
? Math.min((state.settledAt ?? state.nextActionAt - state.delay) + state.delay, endsAt)
|
|
1152
|
+
: state.nextActionAt;
|
|
1153
|
+
const updated = {
|
|
1154
|
+
...state,
|
|
1155
|
+
remainingBudget: 0,
|
|
1156
|
+
pendingRetune: null,
|
|
1157
|
+
endsAt,
|
|
1158
|
+
...(nextActionAt !== undefined ? { nextActionAt } : {}),
|
|
1159
|
+
};
|
|
1160
|
+
persist(pi, updated);
|
|
1161
|
+
renderWidget(ctx, updated);
|
|
1162
|
+
if (state.status === "active") rescheduleContinuation(ctx, updated);
|
|
1163
|
+
const duration = formatLoopDelay(parsed.duration);
|
|
1164
|
+
if (state.status === "paused") {
|
|
1165
|
+
notify(ctx, `loop ${switchingModes ? `changed to timed mode with ${duration} left` : `time left set to ${duration}`}; resume will use it`, "info");
|
|
1166
|
+
} else if (state.status === "stopping") {
|
|
1167
|
+
notify(ctx, `loop ${switchingModes ? `changed to timed mode with ${duration} left` : `time left set to ${duration}`}; loop is still stopping`, "info");
|
|
1168
|
+
} else {
|
|
1169
|
+
notify(ctx, switchingModes ? `loop changed to timed mode with ${duration} left` : `loop time left set to ${duration}`, "info");
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1078
1174
|
if (parsed.kind === "delay") {
|
|
1079
1175
|
if (!state || isTerminal(state)) {
|
|
1080
1176
|
notify(ctx, "a loop must be active, stopping, or paused to update its delay", "error");
|
|
@@ -1108,6 +1204,35 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1108
1204
|
return;
|
|
1109
1205
|
}
|
|
1110
1206
|
|
|
1207
|
+
if (parsed.kind === "pauseAtBoundary") {
|
|
1208
|
+
if (!state || isTerminal(state)) {
|
|
1209
|
+
notify(ctx, "a loop must be active to pause", "error");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (state.status === "paused") {
|
|
1213
|
+
notify(ctx, "loop is already paused", "info");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (state.status === "pausing") {
|
|
1217
|
+
notify(ctx, "loop will pause after the active iteration", "info");
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (state.phase === "retrying") {
|
|
1221
|
+
pauseLoop(ctx, state, "paused by user");
|
|
1222
|
+
notify(ctx, "loop paused", "info");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const pausing: LoopState = { ...state, status: "pausing" };
|
|
1226
|
+
persist(pi, pausing);
|
|
1227
|
+
renderWidget(ctx, pausing);
|
|
1228
|
+
if (state.phase === "waiting") {
|
|
1229
|
+
await advanceAtBoundary(ctx, pausing.runId, pausing.currentIteration);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
notify(ctx, "loop will pause after the active iteration", "info");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1111
1236
|
if (parsed.kind === "end") {
|
|
1112
1237
|
if (!state || state.status === "inactive" || state.status === "completed" || state.status === "stopped") {
|
|
1113
1238
|
notify(ctx, "loop: no active run", "info");
|
|
@@ -1145,7 +1270,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1145
1270
|
}
|
|
1146
1271
|
|
|
1147
1272
|
if (parsed.kind === "resume") {
|
|
1148
|
-
if (state?.status === "stopping") {
|
|
1273
|
+
if (state?.status === "pausing" || state?.status === "stopping") {
|
|
1149
1274
|
const resumed = { ...state, status: "active" as const };
|
|
1150
1275
|
persist(pi, resumed);
|
|
1151
1276
|
renderWidget(ctx, resumed);
|
|
@@ -1160,6 +1285,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1160
1285
|
await advanceAtBoundary(ctx, state.runId, state.currentIteration, true);
|
|
1161
1286
|
return;
|
|
1162
1287
|
}
|
|
1288
|
+
const pausedAtBoundary = state.phase === "waiting";
|
|
1163
1289
|
const {
|
|
1164
1290
|
pauseReason: _pauseReason,
|
|
1165
1291
|
pausedAt: _pausedAt,
|
|
@@ -1176,7 +1302,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1176
1302
|
persist(pi, resumed);
|
|
1177
1303
|
renderWidget(ctx, resumed);
|
|
1178
1304
|
handledSettlementKey = undefined;
|
|
1179
|
-
|
|
1305
|
+
if (pausedAtBoundary) {
|
|
1306
|
+
await advanceAtBoundary(ctx, resumed.runId, resumed.currentIteration);
|
|
1307
|
+
} else {
|
|
1308
|
+
continueCurrentIteration(ctx, resumed);
|
|
1309
|
+
}
|
|
1180
1310
|
return;
|
|
1181
1311
|
}
|
|
1182
1312
|
|
|
@@ -1214,12 +1344,18 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1214
1344
|
}
|
|
1215
1345
|
|
|
1216
1346
|
if (parsed.kind === "retune" || parsed.kind === "adjust") {
|
|
1217
|
-
|
|
1218
|
-
|
|
1347
|
+
const canRetune = state && (
|
|
1348
|
+
state.status === "active" ||
|
|
1349
|
+
state.status === "stopping" ||
|
|
1350
|
+
(parsed.kind === "retune" && state.status === "paused")
|
|
1351
|
+
);
|
|
1352
|
+
if (!canRetune) {
|
|
1353
|
+
notify(ctx, "a loop must be active, stopping, or paused to retune its remaining budget", "error");
|
|
1219
1354
|
return;
|
|
1220
1355
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1356
|
+
const switchingModes = state.endsAt !== undefined;
|
|
1357
|
+
if (switchingModes && parsed.kind === "adjust") {
|
|
1358
|
+
notify(ctx, "a timed loop has no iteration budget to adjust; use /loop <positive-count> to switch modes", "error");
|
|
1223
1359
|
return;
|
|
1224
1360
|
}
|
|
1225
1361
|
const currentBudget = state.pendingRetune ?? state.remainingBudget;
|
|
@@ -1228,11 +1364,20 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1228
1364
|
notify(ctx, `cannot subtract more than the ${currentBudget} future iteration${currentBudget === 1 ? "" : "s"}`, "error");
|
|
1229
1365
|
return;
|
|
1230
1366
|
}
|
|
1231
|
-
const
|
|
1367
|
+
const { endsAt: _endsAt, ...withoutDeadline } = state;
|
|
1368
|
+
const retuned = {
|
|
1369
|
+
...withoutDeadline,
|
|
1370
|
+
pendingRetune: nextBudget,
|
|
1371
|
+
status: state.status === "paused" ? "paused" as const : "active" as const,
|
|
1372
|
+
};
|
|
1232
1373
|
persist(pi, retuned);
|
|
1233
1374
|
renderWidget(ctx, retuned);
|
|
1234
|
-
if (nextBudget <= 0) rescheduleContinuation(ctx, retuned);
|
|
1235
|
-
|
|
1375
|
+
if (switchingModes || nextBudget <= 0) rescheduleContinuation(ctx, retuned);
|
|
1376
|
+
if (state.status === "paused") {
|
|
1377
|
+
notify(ctx, `loop ${switchingModes ? "changed to" : "set to"} ${nextBudget} future iteration${nextBudget === 1 ? "" : "s"}; resume will use it`, "info");
|
|
1378
|
+
} else {
|
|
1379
|
+
notify(ctx, `loop ${switchingModes ? "changed to" : "will run"} ${nextBudget} future iteration${nextBudget === 1 ? "" : "s"}`, "info");
|
|
1380
|
+
}
|
|
1236
1381
|
return;
|
|
1237
1382
|
}
|
|
1238
1383
|
|
|
@@ -1309,6 +1454,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1309
1454
|
const loaded = latestStateFromContext(ctx);
|
|
1310
1455
|
const owned = loaded && stateBelongsToContext(loaded, ctx) ? loaded : undefined;
|
|
1311
1456
|
runState = owned;
|
|
1457
|
+
reportHerdrBlocked(owned);
|
|
1312
1458
|
if (!owned || owned.status === "inactive") clearWidget(ctx);
|
|
1313
1459
|
else renderWidget(ctx, owned);
|
|
1314
1460
|
// New-session setup writes transferred state after this event and starts
|
|
@@ -1387,6 +1533,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1387
1533
|
const loaded = latestStateFromContext(ctx);
|
|
1388
1534
|
const owned = loaded && stateBelongsToContext(loaded, ctx) ? loaded : undefined;
|
|
1389
1535
|
runState = owned;
|
|
1536
|
+
reportHerdrBlocked(owned);
|
|
1390
1537
|
if (!owned || owned.status === "inactive") clearWidget(ctx);
|
|
1391
1538
|
else renderWidget(ctx, owned);
|
|
1392
1539
|
if (owned && statusIsActive(owned)) scheduleStartupRecovery(ctx, owned);
|
|
@@ -1406,6 +1553,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1406
1553
|
// Shutdown may already have detached the runtime's append action.
|
|
1407
1554
|
}
|
|
1408
1555
|
}
|
|
1556
|
+
reportHerdrBlocked(undefined);
|
|
1409
1557
|
clearWidget(ctx);
|
|
1410
1558
|
currentSessionManagerRef = undefined;
|
|
1411
1559
|
});
|