@frockbot/plugin-shell 0.3.6 → 0.3.8
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/package.json +31 -31
- package/src/backend-recovery-integration.test.ts +12 -18
- package/src/backend.ts +84 -11
- package/src/client/FrockBotApp.vue +66 -1
- package/src/client/index.test.ts +132 -0
- package/src/client/index.ts +151 -21
- package/src/client/styles.css +34 -0
- package/src/client/turn-limits.test.ts +41 -0
- package/src/client/turn-limits.ts +40 -0
- package/src/client/uncertain-admission.test.ts +69 -0
- package/src/client/uncertain-admission.ts +78 -0
- package/src/connection-return.test.ts +55 -0
- package/src/debug-protocol.test.ts +38 -2
- package/src/debug-protocol.ts +39 -6
- package/src/history.test.ts +52 -0
- package/src/history.ts +79 -1
- package/src/run-protocol.test.ts +49 -3
- package/src/run-protocol.ts +134 -17
- package/src/shared.ts +72 -0
- package/src/unread.test.ts +68 -0
- package/src/unread.ts +53 -0
package/src/unread.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
optionalUnreadStateV1,
|
|
14
14
|
projectBotUnreadViewV1,
|
|
15
15
|
sidebarMessagePreviewForTurnV1,
|
|
16
|
+
sidebarMessagePreviewFromRunsV1,
|
|
16
17
|
UNREAD_COUNT_CAP,
|
|
17
18
|
type UnreadStateV1,
|
|
18
19
|
} from "./unread.js";
|
|
@@ -263,6 +264,73 @@ describe("the unread projection", () => {
|
|
|
263
264
|
});
|
|
264
265
|
});
|
|
265
266
|
|
|
267
|
+
// The record is written at settlement, so a Bot whose Turns settled before
|
|
268
|
+
// that projection existed has a full transcript and no preview — and its
|
|
269
|
+
// sidebar row said "No messages yet" over six messages. A read derives it.
|
|
270
|
+
describe("the sidebar preview derived from stored runs", () => {
|
|
271
|
+
const run = (over: Record<string, unknown> = {}) => ({
|
|
272
|
+
acceptedAt: "2026-08-31T00:02:00.000Z",
|
|
273
|
+
input: "What is the plan?",
|
|
274
|
+
responseText: "Here is the plan.",
|
|
275
|
+
status: "completed",
|
|
276
|
+
events: [{ timestamp: "2026-08-31T00:02:05.000Z" }],
|
|
277
|
+
...over,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("takes the newest settled chat Turn's reply, stamped when it settled", () => {
|
|
281
|
+
expect(sidebarMessagePreviewFromRunsV1([run()])).toEqual({
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
text: "Here is the plan.",
|
|
284
|
+
at: "2026-08-31T00:02:05.000Z",
|
|
285
|
+
role: "assistant",
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("walks past a running Turn and an automation to the newest chat reply", () => {
|
|
290
|
+
expect(
|
|
291
|
+
sidebarMessagePreviewFromRunsV1([
|
|
292
|
+
run({ status: "running", responseText: undefined }),
|
|
293
|
+
run({
|
|
294
|
+
admission: { turnType: "automation" },
|
|
295
|
+
responseText: "Routine ran.",
|
|
296
|
+
}),
|
|
297
|
+
run({ responseText: "The older answer." }),
|
|
298
|
+
]),
|
|
299
|
+
).toMatchObject({ text: "The older answer.", role: "assistant" });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("falls back to the User's own words when the reply was empty", () => {
|
|
303
|
+
expect(
|
|
304
|
+
sidebarMessagePreviewFromRunsV1([run({ responseText: "" })]),
|
|
305
|
+
).toEqual({
|
|
306
|
+
schemaVersion: 1,
|
|
307
|
+
text: "What is the plan?",
|
|
308
|
+
at: "2026-08-31T00:02:00.000Z",
|
|
309
|
+
role: "user",
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a Bot with no settled chat Turn still has no preview", () => {
|
|
314
|
+
expect(sidebarMessagePreviewFromRunsV1([])).toBeUndefined();
|
|
315
|
+
expect(
|
|
316
|
+
sidebarMessagePreviewFromRunsV1([
|
|
317
|
+
run({ status: "running", responseText: undefined, input: "" }),
|
|
318
|
+
]),
|
|
319
|
+
).toBeUndefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// A read of durable data never throws: a run whose stamps are unreadable
|
|
323
|
+
// costs the row, not the whole sidebar.
|
|
324
|
+
test("skips a run whose stored timestamps cannot be read", () => {
|
|
325
|
+
expect(
|
|
326
|
+
sidebarMessagePreviewFromRunsV1([
|
|
327
|
+
run({ acceptedAt: "not a time", events: [] }),
|
|
328
|
+
run({ responseText: "The readable one." }),
|
|
329
|
+
]),
|
|
330
|
+
).toMatchObject({ text: "The readable one." });
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
266
334
|
describe("the unread command", () => {
|
|
267
335
|
test("decodes each type and refuses a mismatched cursor", () => {
|
|
268
336
|
expect(
|
package/src/unread.ts
CHANGED
|
@@ -260,6 +260,59 @@ export function sidebarMessagePreviewForTurnV1(
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** The little a settled run has to expose for a preview to be derived from it. */
|
|
264
|
+
export interface SidebarPreviewRunV1 {
|
|
265
|
+
acceptedAt: string;
|
|
266
|
+
input: string;
|
|
267
|
+
responseText?: string;
|
|
268
|
+
status: string;
|
|
269
|
+
/** Only the timestamps are read: the newest one is when the Turn settled. */
|
|
270
|
+
events?: readonly { timestamp?: string }[];
|
|
271
|
+
/** Absent means the Turn was admitted as `chat`, as everywhere else. */
|
|
272
|
+
admission?: { turnType?: string };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The preview for a Bot that has a transcript but no preview record.
|
|
277
|
+
*
|
|
278
|
+
* The record is written at settlement, so every Turn that settled before that
|
|
279
|
+
* projection existed left one behind — and the row then claimed "No messages
|
|
280
|
+
* yet" over a full conversation. A read cannot write the record it is missing,
|
|
281
|
+
* so it derives the same line from the runs that are already durable. Runs
|
|
282
|
+
* arrive newest-first and the walk stops at the first settled chat Turn with
|
|
283
|
+
* text, which is exactly the line the settlement would have stored.
|
|
284
|
+
*/
|
|
285
|
+
export function sidebarMessagePreviewFromRunsV1(
|
|
286
|
+
runs: readonly SidebarPreviewRunV1[],
|
|
287
|
+
): SidebarMessagePreviewV1 | undefined {
|
|
288
|
+
for (const run of runs) {
|
|
289
|
+
if (run.status === "running") continue;
|
|
290
|
+
// An automation Turn reaches the User through its own inbox entry and
|
|
291
|
+
// never became this row at settlement either.
|
|
292
|
+
if ((run.admission?.turnType ?? "chat") !== "chat") continue;
|
|
293
|
+
const settledAt = settlementTimestampV1(run);
|
|
294
|
+
try {
|
|
295
|
+
const preview = sidebarMessagePreviewForTurnV1(run, settledAt);
|
|
296
|
+
if (preview) return preview;
|
|
297
|
+
} catch {
|
|
298
|
+
// A run whose stored timestamps cannot be read is not worth the row —
|
|
299
|
+
// and a read of durable data never throws over one bad record.
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** When a stored run settled: its newest event's stamp, else its admission. */
|
|
307
|
+
function settlementTimestampV1(run: SidebarPreviewRunV1): string {
|
|
308
|
+
for (let index = (run.events?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
309
|
+
const timestamp = run.events?.[index]?.timestamp;
|
|
310
|
+
if (typeof timestamp === "string" && Number.isFinite(Date.parse(timestamp)))
|
|
311
|
+
return timestamp;
|
|
312
|
+
}
|
|
313
|
+
return run.acceptedAt;
|
|
314
|
+
}
|
|
315
|
+
|
|
263
316
|
/**
|
|
264
317
|
* Records a settled chat Turn. Monotonic: a cursor that is not newer than the
|
|
265
318
|
* one already recorded leaves the record byte-for-byte unchanged, so a
|