@frockbot/plugin-shell 0.3.11 → 0.3.13
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 +35 -33
- package/src/agent.test.ts +78 -0
- package/src/agent.ts +130 -2
- package/src/backend-configuration.test.ts +26 -26
- package/src/backend-recovery-integration.test.ts +10 -10
- package/src/backend-runner.ts +19 -2
- package/src/backend.ts +85 -18
- package/src/client/AppletCanvas.vue +19 -6
- package/src/client/FrockBotApp.vue +405 -75
- package/src/client/activity-trail.test.ts +205 -0
- package/src/client/activity-trail.ts +227 -0
- package/src/client/applets-client.test.ts +62 -0
- package/src/client/applets-client.ts +19 -0
- package/src/client/index.test.ts +128 -21
- package/src/client/index.ts +359 -114
- package/src/client/model-presentation.test.ts +3 -3
- package/src/client/no-bot-model-label.test.ts +7 -7
- package/src/client/skill-invocation.test.ts +34 -0
- package/src/client/skill-invocation.ts +22 -0
- package/src/client/styles.css +69 -19
- package/src/client/transcript-cache.test.ts +125 -0
- package/src/client/transcript-cache.ts +190 -0
- package/src/compaction-scheduler.test.ts +96 -0
- package/src/compaction-scheduler.ts +108 -0
- package/src/compaction-transcript.test.ts +174 -0
- package/src/compaction.test.ts +596 -0
- package/src/compaction.ts +539 -0
- package/src/focus.test.ts +222 -0
- package/src/focus.ts +93 -0
- package/src/history.ts +86 -8
- package/src/legacy-frock-model-id.test.ts +148 -0
- package/src/notification-id.ts +0 -0
- package/src/run-failure-copy.test.ts +150 -0
- package/src/run-failure-copy.ts +110 -0
- package/src/run-protocol.test.ts +50 -7
- package/src/run-protocol.ts +152 -43
- package/src/settings-links.test.ts +8 -2
- package/src/settings-links.ts +11 -2
- package/src/shared.ts +36 -0
- package/tsconfig.json +1 -2
- package/src/client/activity-ring.test.ts +0 -89
- package/src/client/activity-ring.ts +0 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
"./client/FrockBotApp.vue": "./src/client/FrockBotApp.vue",
|
|
28
28
|
"./client/notify": "./src/client/notify.ts",
|
|
29
29
|
"./client/styles.css": "./src/client/styles.css",
|
|
30
|
+
"./focus": "./src/focus.ts",
|
|
30
31
|
"./frockbot.json": "./frockbot.json",
|
|
31
32
|
"./host": "./src/host.ts",
|
|
32
33
|
"./manifest": "./src/manifest.ts",
|
|
33
34
|
"./notification-id": "./src/notification-id.ts",
|
|
34
35
|
"./package.json": "./package.json",
|
|
35
36
|
"./run-cursor": "./src/run-cursor.ts",
|
|
37
|
+
"./run-failure-copy": "./src/run-failure-copy.ts",
|
|
36
38
|
"./run-protocol": "./src/run-protocol.ts",
|
|
37
39
|
"./settings-links": "./src/settings-links.ts",
|
|
38
40
|
"./shared": "./src/shared.ts",
|
|
@@ -48,46 +50,46 @@
|
|
|
48
50
|
},
|
|
49
51
|
"dependencies": {
|
|
50
52
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
51
|
-
"@frockbot/agent-runtime": "0.3.
|
|
52
|
-
"@frockbot/application-foundation": "0.3.
|
|
53
|
-
"@frockbot/catalog-core": "0.3.
|
|
54
|
-
"@frockbot/client-core": "0.3.
|
|
55
|
-
"@frockbot/client-ui": "0.3.
|
|
56
|
-
"@frockbot/computer-core": "0.3.
|
|
57
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
58
|
-
"@frockbot/configuration-core": "0.3.
|
|
59
|
-
"@frockbot/connection-core": "0.3.
|
|
60
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
61
|
-
"@frockbot/kernel-composition": "0.3.
|
|
62
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
63
|
-
"@frockbot/kernel-do": "0.3.
|
|
64
|
-
"@frockbot/machine-protocol": "0.3.
|
|
65
|
-
"@frockbot/plugin-applets": "0.3.
|
|
66
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
67
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
68
|
-
"@frockbot/plugin-computer": "0.3.
|
|
69
|
-
"@frockbot/plugin-flock": "0.3.
|
|
70
|
-
"@frockbot/plugin-image": "0.3.
|
|
71
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
72
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
73
|
-
"@frockbot/plugin-memory": "0.3.
|
|
74
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
75
|
-
"@frockbot/plugin-routines": "0.3.
|
|
76
|
-
"@frockbot/plugin-skills": "0.3.
|
|
77
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
78
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
79
|
-
"@frockbot/protocol": "0.3.
|
|
53
|
+
"@frockbot/agent-runtime": "0.3.13",
|
|
54
|
+
"@frockbot/application-foundation": "0.3.13",
|
|
55
|
+
"@frockbot/catalog-core": "0.3.13",
|
|
56
|
+
"@frockbot/client-core": "0.3.13",
|
|
57
|
+
"@frockbot/client-ui": "0.3.13",
|
|
58
|
+
"@frockbot/computer-core": "0.3.13",
|
|
59
|
+
"@frockbot/computer-host-protocol": "0.3.13",
|
|
60
|
+
"@frockbot/configuration-core": "0.3.13",
|
|
61
|
+
"@frockbot/connection-core": "0.3.13",
|
|
62
|
+
"@frockbot/kernel-agent-loop": "0.3.13",
|
|
63
|
+
"@frockbot/kernel-composition": "0.3.13",
|
|
64
|
+
"@frockbot/kernel-contracts": "0.3.13",
|
|
65
|
+
"@frockbot/kernel-do": "0.3.13",
|
|
66
|
+
"@frockbot/machine-protocol": "0.3.13",
|
|
67
|
+
"@frockbot/plugin-applets": "0.3.13",
|
|
68
|
+
"@frockbot/plugin-authoring": "0.3.13",
|
|
69
|
+
"@frockbot/plugin-bot-template": "0.3.13",
|
|
70
|
+
"@frockbot/plugin-computer": "0.3.13",
|
|
71
|
+
"@frockbot/plugin-flock": "0.3.13",
|
|
72
|
+
"@frockbot/plugin-image": "0.3.13",
|
|
73
|
+
"@frockbot/plugin-machine-messages": "0.3.13",
|
|
74
|
+
"@frockbot/plugin-mcp": "0.3.13",
|
|
75
|
+
"@frockbot/plugin-memory": "0.3.13",
|
|
76
|
+
"@frockbot/plugin-package-catalog": "0.3.13",
|
|
77
|
+
"@frockbot/plugin-routines": "0.3.13",
|
|
78
|
+
"@frockbot/plugin-skills": "0.3.13",
|
|
79
|
+
"@frockbot/plugin-subagents": "0.3.13",
|
|
80
|
+
"@frockbot/plugin-user-machine": "0.3.13",
|
|
81
|
+
"@frockbot/protocol": "0.3.13",
|
|
80
82
|
"cordis": "4.0.0-rc.8",
|
|
81
83
|
"vue": "3.5.41"
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
86
|
"@cloudflare/workers-types": "latest",
|
|
85
|
-
"@frockbot/plugin-prompt": "0.3.
|
|
86
|
-
"@frockbot/plugin-tools": "0.3.
|
|
87
|
+
"@frockbot/plugin-prompt": "0.3.13",
|
|
88
|
+
"@frockbot/plugin-tools": "0.3.13",
|
|
87
89
|
"@types/bun": "1.3.6",
|
|
88
90
|
"@types/node": "26.2.0",
|
|
89
91
|
"@vitejs/plugin-vue": "6.0.8",
|
|
90
|
-
"typescript": "
|
|
92
|
+
"typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
|
|
91
93
|
"vite": "8.2.2",
|
|
92
94
|
"vue-tsc": "3.3.10"
|
|
93
95
|
},
|
package/src/agent.test.ts
CHANGED
|
@@ -448,3 +448,81 @@ describe("the conversation prompt section", () => {
|
|
|
448
448
|
}
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
// Two ends of one defect. The prompt tells the model to acknowledge with the
|
|
453
|
+
// call; the promotion catches the model that acknowledges in text anyway. The
|
|
454
|
+
// verification run watched a real model do exactly that — "On it — building
|
|
455
|
+
// the 2027 countdown applet now." as plain assistant text, then three tool
|
|
456
|
+
// calls — and nobody ever saw the line.
|
|
457
|
+
describe("the acknowledgement reaches the user", () => {
|
|
458
|
+
test("the prompt names the call, not just the line", () => {
|
|
459
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain(
|
|
460
|
+
"Your own text is not shown to the user",
|
|
461
|
+
);
|
|
462
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain(
|
|
463
|
+
"your first action is a `send_to_user` call",
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("assistant text in a tool-calling step is promoted to one send", async () => {
|
|
468
|
+
const mounted = await mount();
|
|
469
|
+
try {
|
|
470
|
+
await mounted.root.serial(
|
|
471
|
+
"agent/assistant-text",
|
|
472
|
+
{ session: mounted.session } as never,
|
|
473
|
+
"On it — building the 2027 countdown applet now.",
|
|
474
|
+
{ turn: 4, step: 2, requestId: "request-1" },
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const sends = mounted.session.events.filter(
|
|
478
|
+
(event) => event.type === "send/to-user",
|
|
479
|
+
);
|
|
480
|
+
expect(sends).toHaveLength(1);
|
|
481
|
+
expect(sends[0]).toMatchObject({
|
|
482
|
+
turn: 4,
|
|
483
|
+
step: 2,
|
|
484
|
+
occurrenceId: "assistant-text:request-1",
|
|
485
|
+
payload: {
|
|
486
|
+
type: "text",
|
|
487
|
+
text: "On it — building the 2027 countdown applet now.",
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
} finally {
|
|
491
|
+
await mounted.dispose();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a step that already spoke is left alone, and a replay adds nothing", async () => {
|
|
496
|
+
const mounted = await mount();
|
|
497
|
+
try {
|
|
498
|
+
await invoke(
|
|
499
|
+
mounted,
|
|
500
|
+
"chat",
|
|
501
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
502
|
+
payload: { type: "text", text: "On it." },
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
await mounted.root.serial(
|
|
506
|
+
"agent/assistant-text",
|
|
507
|
+
{ session: mounted.session } as never,
|
|
508
|
+
"On it — building the countdown applet now.",
|
|
509
|
+
{ turn: 4, step: 2, requestId: "request-1" },
|
|
510
|
+
);
|
|
511
|
+
// The same step replayed after an eviction promotes nothing new either.
|
|
512
|
+
await mounted.root.serial(
|
|
513
|
+
"agent/assistant-text",
|
|
514
|
+
{ session: mounted.session } as never,
|
|
515
|
+
"On it — building the countdown applet now.",
|
|
516
|
+
{ turn: 4, step: 2, requestId: "request-1" },
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const sends = mounted.session.events.filter(
|
|
520
|
+
(event) => event.type === "send/to-user",
|
|
521
|
+
);
|
|
522
|
+
expect(sends).toHaveLength(1);
|
|
523
|
+
expect(sends[0]).toMatchObject({ payload: { text: "On it." } });
|
|
524
|
+
} finally {
|
|
525
|
+
await mounted.dispose();
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
package/src/agent.ts
CHANGED
|
@@ -39,7 +39,15 @@ import {
|
|
|
39
39
|
} from "@frockbot/kernel-contracts";
|
|
40
40
|
// Merges the Agent loop's event declarations into the cordis Context type.
|
|
41
41
|
import type {} from "@frockbot/kernel-agent-loop/agent";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
automationParentPointerV1,
|
|
44
|
+
chatWindowV1,
|
|
45
|
+
CHAT_HISTORY_BUDGET_CHARS_V1,
|
|
46
|
+
turnScopedMessagesV1,
|
|
47
|
+
turnTypesByTurnV1,
|
|
48
|
+
} from "./history.js";
|
|
49
|
+
import { runCompactionV1 } from "./compaction.js";
|
|
50
|
+
import { compactionWorkV1 } from "./compaction-scheduler.js";
|
|
43
51
|
import type { Plugin } from "cordis";
|
|
44
52
|
import manifest from "../frockbot.json" with { type: "json" };
|
|
45
53
|
|
|
@@ -85,6 +93,11 @@ function refusal(reason: string): ToolExecutionResult {
|
|
|
85
93
|
return { content: reason, isError: true };
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
/** Adapts a cordis fiber to the plain disposer this Package's list holds. */
|
|
97
|
+
function disposeFiber(fiber: { dispose(): unknown }): () => void {
|
|
98
|
+
return () => void fiber.dispose();
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
/**
|
|
89
102
|
* The open step a Shell event belongs to. The session log is the
|
|
90
103
|
* reconstruction surface, so a send without its turn and step would not
|
|
@@ -111,6 +124,60 @@ function openStepPositionV1(
|
|
|
111
124
|
return { turn: started.turn, step: started.step };
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
/** The longest acknowledgement worth promoting; longer is not an ack. */
|
|
128
|
+
export const PROMOTED_ASSISTANT_TEXT_LIMIT_V1 = 600;
|
|
129
|
+
|
|
130
|
+
/** The occurrence id a promoted send is recorded under, per model request. */
|
|
131
|
+
export function promotedSendOccurrenceIdV1(requestId: string): string {
|
|
132
|
+
return `assistant-text:${requestId}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Promotes a step's assistant text to a send when the model wrote to the
|
|
137
|
+
* person and then called tools without sending.
|
|
138
|
+
*
|
|
139
|
+
* The verification run is the whole argument for this. The model did exactly
|
|
140
|
+
* what the contract asks — "On it — building the 2027 countdown applet now.",
|
|
141
|
+
* one short line, no narration — and then called its tools. It went nowhere:
|
|
142
|
+
* the thread draws one bubble per send (issue 153) and there was no send, so
|
|
143
|
+
* the person watched a spinner and then a failure, with the acknowledgement
|
|
144
|
+
* they were owed sitting in the log where only the debug surface could see it.
|
|
145
|
+
*
|
|
146
|
+
* A step that already sent is left alone: the model's own text is scratch
|
|
147
|
+
* space once it has spoken, and promoting it there is how the same line
|
|
148
|
+
* arrives twice. The occurrence id is derived from the model request, so a
|
|
149
|
+
* step replayed after an eviction promotes the same text to the same
|
|
150
|
+
* occurrence rather than a second bubble.
|
|
151
|
+
*
|
|
152
|
+
* The prompt asks for the call and this does not replace it — a model that
|
|
153
|
+
* calls `send_to_user` never reaches here.
|
|
154
|
+
*/
|
|
155
|
+
export async function promoteAssistantTextToSendV1(
|
|
156
|
+
session: Session,
|
|
157
|
+
text: string,
|
|
158
|
+
position: { turn: number; step: number; requestId: string },
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const trimmed = text.trim();
|
|
161
|
+
if (trimmed.length === 0 || trimmed.length > PROMOTED_ASSISTANT_TEXT_LIMIT_V1)
|
|
162
|
+
return;
|
|
163
|
+
const occurrenceId = promotedSendOccurrenceIdV1(position.requestId);
|
|
164
|
+
const spokeThisStep = session.events.some(
|
|
165
|
+
(event) =>
|
|
166
|
+
event.type === "send/to-user" &&
|
|
167
|
+
event.turn === position.turn &&
|
|
168
|
+
event.step === position.step,
|
|
169
|
+
);
|
|
170
|
+
if (spokeThisStep) return;
|
|
171
|
+
session.append({
|
|
172
|
+
type: "send/to-user",
|
|
173
|
+
turn: position.turn,
|
|
174
|
+
step: position.step,
|
|
175
|
+
occurrenceId,
|
|
176
|
+
payload: { type: "text", text: trimmed },
|
|
177
|
+
});
|
|
178
|
+
await session.flush();
|
|
179
|
+
}
|
|
180
|
+
|
|
114
181
|
/** What a recorded send tells the model it did. */
|
|
115
182
|
function sendAcknowledgement(payload: SendToUserPayloadV1): string {
|
|
116
183
|
switch (payload.type) {
|
|
@@ -147,7 +214,11 @@ export const CONVERSATION_PROMPT_TEXT_V1 = [
|
|
|
147
214
|
"## Talking to the user",
|
|
148
215
|
"",
|
|
149
216
|
"Everything the user sees is a `send_to_user` call; nothing else reaches them.",
|
|
150
|
-
|
|
217
|
+
// The first line said sends are the only thing the user sees, and a model
|
|
218
|
+
// still wrote its acknowledgement as plain assistant text and went straight
|
|
219
|
+
// on to call tools — nobody saw it. So the acknowledgement names the call.
|
|
220
|
+
"Your own text is not shown to the user. Writing a line in your reply instead of calling `send_to_user` means nobody reads it.",
|
|
221
|
+
'When a request will take more than a moment, your first action is a `send_to_user` call with one short line — "On it." or "Looking into that." — and then you go quiet and work.',
|
|
151
222
|
"After that, send only on a real beat: the result, a decision only the user can make, or a blocker you cannot get past.",
|
|
152
223
|
"Never narrate what you are doing, what you are about to do, or which tool you are using.",
|
|
153
224
|
"Never leave a question or a request hanging: before you stop, the user must have the answer, the result, or the reason there isn't one.",
|
|
@@ -348,6 +419,63 @@ export const shellAgentPlugin: Plugin.Function = (ctx) => {
|
|
|
348
419
|
createWakeParentTool(ctx.sessions),
|
|
349
420
|
parentHandoff ? { admissionCeiling: parentHandoff } : undefined,
|
|
350
421
|
),
|
|
422
|
+
// The safety net under the prompt: a model that acknowledges the request
|
|
423
|
+
// in its own text instead of calling `send_to_user` still reaches the
|
|
424
|
+
// person. See `promoteAssistantTextToSendV1`.
|
|
425
|
+
ctx.on("agent/assistant-text", async (agent, text, position) => {
|
|
426
|
+
await promoteAssistantTextToSendV1(agent.session, text, position);
|
|
427
|
+
}),
|
|
428
|
+
// ADR 0030. `ctx.inject` rather than a declared dependency: a host that
|
|
429
|
+
// mounts the Shell without a model still gets its tools and its transcript
|
|
430
|
+
// seam, and simply never compacts. The hook is evaluated after `turn/end`
|
|
431
|
+
// is on the log and flushed — but `agent/turn-stopping` is a hook the loop
|
|
432
|
+
// *awaits* inside its `finally`, so running the summariser here is exactly
|
|
433
|
+
// the latency ADR 0030 says a compaction never costs. It is handed to the
|
|
434
|
+
// detached scheduler instead and this returns at once: the Turn ends, the
|
|
435
|
+
// run settles, the response goes out, and the summariser carries on behind
|
|
436
|
+
// it. Nothing here may throw, and nothing here may wait.
|
|
437
|
+
disposeFiber(
|
|
438
|
+
ctx.inject(["llm"], (scoped) => {
|
|
439
|
+
scoped.on("agent/turn-stopping", async (agent, turn) => {
|
|
440
|
+
const session = agent.session;
|
|
441
|
+
const types = turnTypesByTurnV1(session.events);
|
|
442
|
+
if ((types.get(turn) ?? "chat") !== "chat") return;
|
|
443
|
+
compactionWorkV1(session.id).start(async (signal) => {
|
|
444
|
+
if (signal.aborted) return;
|
|
445
|
+
await runCompactionV1({
|
|
446
|
+
session,
|
|
447
|
+
window: chatWindowV1(session.events, session.deriveMessages()),
|
|
448
|
+
budget: CHAT_HISTORY_BUDGET_CHARS_V1,
|
|
449
|
+
currentTurn: turn,
|
|
450
|
+
newEffectId: () => `compaction-${crypto.randomUUID()}`,
|
|
451
|
+
summarise: async (request) => {
|
|
452
|
+
let text = "";
|
|
453
|
+
// Two deadlines, one call: the compaction's own, and the abort
|
|
454
|
+
// a newly admitted Turn raises when it takes the log back.
|
|
455
|
+
const cancelled = AbortSignal.any([request.signal, signal]);
|
|
456
|
+
for await (const event of scoped.llm.stream(
|
|
457
|
+
{
|
|
458
|
+
requestId: `compaction-${crypto.randomUUID()}`,
|
|
459
|
+
provider: request.provider,
|
|
460
|
+
model: request.model,
|
|
461
|
+
system: request.system,
|
|
462
|
+
messages: request.messages,
|
|
463
|
+
tools: [],
|
|
464
|
+
...(request.modelBinding
|
|
465
|
+
? { modelBinding: request.modelBinding }
|
|
466
|
+
: {}),
|
|
467
|
+
},
|
|
468
|
+
cancelled,
|
|
469
|
+
)) {
|
|
470
|
+
if (event.type === "text-delta") text += event.text;
|
|
471
|
+
}
|
|
472
|
+
return text;
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
}),
|
|
478
|
+
),
|
|
351
479
|
// Applied after the rest of the chain, so this Package has the last word on
|
|
352
480
|
// what history a request carries — the one rule the visible transcript
|
|
353
481
|
// rests on.
|
|
@@ -150,7 +150,7 @@ function configuredUser(): UserSettingsViewV1 {
|
|
|
150
150
|
connectionId: "flock-ai-ambient",
|
|
151
151
|
packageId: "provider-flock-ai",
|
|
152
152
|
connectionTypeId: "flock-ai-account",
|
|
153
|
-
displayName: "
|
|
153
|
+
displayName: "Frock AI",
|
|
154
154
|
state: "ready",
|
|
155
155
|
providerType: "flock-ai",
|
|
156
156
|
generation: "foundation-generation-1",
|
|
@@ -160,7 +160,7 @@ function configuredUser(): UserSettingsViewV1 {
|
|
|
160
160
|
state: "fresh",
|
|
161
161
|
models: [
|
|
162
162
|
{
|
|
163
|
-
providerModelId: "@
|
|
163
|
+
providerModelId: "@frock/auto",
|
|
164
164
|
displayName: "Platform",
|
|
165
165
|
capabilities: {
|
|
166
166
|
tools: true,
|
|
@@ -174,7 +174,7 @@ function configuredUser(): UserSettingsViewV1 {
|
|
|
174
174
|
safeMetadata: {},
|
|
175
175
|
},
|
|
176
176
|
],
|
|
177
|
-
platformModel: model("flock-ai-ambient", "@
|
|
177
|
+
platformModel: model("flock-ai-ambient", "@frock/auto"),
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -194,7 +194,7 @@ function host(storage: MemoryStorage, readUser: () => UserSettingsViewV1) {
|
|
|
194
194
|
},
|
|
195
195
|
MEMORY_FILES: {},
|
|
196
196
|
MEMORY_INDEX: {},
|
|
197
|
-
|
|
197
|
+
FROCK_AI: {
|
|
198
198
|
autoRoute: "flock-auto",
|
|
199
199
|
runChatCompletion: () =>
|
|
200
200
|
Promise.resolve(
|
|
@@ -527,7 +527,7 @@ describe("generic per-Turn model resolution", () => {
|
|
|
527
527
|
connectionId: "flock-ai-ambient",
|
|
528
528
|
packageId: "provider-flock-ai",
|
|
529
529
|
provider: "flock-ai",
|
|
530
|
-
providerModelId: "@
|
|
530
|
+
providerModelId: "@frock/auto",
|
|
531
531
|
connectionGeneration: "foundation-generation-1",
|
|
532
532
|
catalogGeneration: "catalog-1",
|
|
533
533
|
});
|
|
@@ -651,7 +651,7 @@ describe("generic per-Turn model resolution", () => {
|
|
|
651
651
|
),
|
|
652
652
|
};
|
|
653
653
|
expect(await contribution.resolveConfiguration(identity)).toMatchObject({
|
|
654
|
-
model: model("flock-ai-ambient", "@
|
|
654
|
+
model: model("flock-ai-ambient", "@frock/auto"),
|
|
655
655
|
});
|
|
656
656
|
expect(await contribution.getSettings(identity)).toMatchObject({
|
|
657
657
|
packageValues: {
|
|
@@ -701,16 +701,18 @@ describe("generic per-Turn model resolution", () => {
|
|
|
701
701
|
};
|
|
702
702
|
for (const bot of bots) {
|
|
703
703
|
const runId = `${bot.botId}-disabled-package`;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
704
|
+
// "Closed" is the durable record, not a rejected call. The Turn settles
|
|
705
|
+
// itself and the caller is handed that settlement: rethrowing over the
|
|
706
|
+
// top of it is what answered 500 and logged an uncaught error for a
|
|
707
|
+
// failure the Bot had already recorded properly.
|
|
708
|
+
await bot.contribution.run({
|
|
709
|
+
userId: "user-1",
|
|
710
|
+
botId: bot.botId,
|
|
711
|
+
runId,
|
|
712
|
+
sessionId: `user-1:${bot.botId}`,
|
|
713
|
+
acceptedAt: "2026-09-02T00:01:00.000Z",
|
|
714
|
+
text: "must fail",
|
|
715
|
+
});
|
|
714
716
|
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
715
717
|
status: "failed",
|
|
716
718
|
failure: expect.stringContaining(
|
|
@@ -723,16 +725,14 @@ describe("generic per-Turn model resolution", () => {
|
|
|
723
725
|
user.connections[0] = { ...user.connections[0]!, state: "revoked" };
|
|
724
726
|
for (const bot of bots) {
|
|
725
727
|
const runId = `${bot.botId}-revoked-connection`;
|
|
726
|
-
await
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}),
|
|
735
|
-
).rejects.toThrow("needs reconnecting");
|
|
728
|
+
await bot.contribution.run({
|
|
729
|
+
userId: "user-1",
|
|
730
|
+
botId: bot.botId,
|
|
731
|
+
runId,
|
|
732
|
+
sessionId: `user-1:${bot.botId}`,
|
|
733
|
+
acceptedAt: "2026-09-02T00:02:00.000Z",
|
|
734
|
+
text: "must fail",
|
|
735
|
+
});
|
|
736
736
|
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
737
737
|
status: "failed",
|
|
738
738
|
failure: expect.stringContaining("needs reconnecting"),
|
|
@@ -344,16 +344,16 @@ describe("Bot recovery", () => {
|
|
|
344
344
|
};
|
|
345
345
|
|
|
346
346
|
failRequests = true;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)
|
|
347
|
+
// The call resolves: the Turn settled itself, and the caller is handed
|
|
348
|
+
// that settlement rather than a rejection thrown over the top of it.
|
|
349
|
+
await host().run({
|
|
350
|
+
userId: "user-1",
|
|
351
|
+
botId: "primary",
|
|
352
|
+
runId: "ollama-run-uncertain",
|
|
353
|
+
sessionId: "user-1:primary",
|
|
354
|
+
acceptedAt: "2026-08-30T00:02:00.000Z",
|
|
355
|
+
text: "uncertain",
|
|
356
|
+
});
|
|
357
357
|
// Ollama keeps no addressable copy of a completion, so a failure raised
|
|
358
358
|
// before the first stream event is definitive rather than uncertain: the
|
|
359
359
|
// run settles as a failed Turn instead of parking on a retrieval this
|
package/src/backend-runner.ts
CHANGED
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
BotTurnRecoveryRequiredError,
|
|
18
18
|
} from "@frockbot/kernel-do";
|
|
19
19
|
import type { BotTurnCommand, BotTurnCompletion } from "./backend-contracts.js";
|
|
20
|
+
import {
|
|
21
|
+
compactionInFlightV1,
|
|
22
|
+
whenCompactionSettledV1,
|
|
23
|
+
} from "./compaction-scheduler.js";
|
|
20
24
|
|
|
21
25
|
export {
|
|
22
26
|
BotTurnExecutionError,
|
|
@@ -370,11 +374,24 @@ export async function executeBotTurn(
|
|
|
370
374
|
await runtime.agent.agent.whenIdle();
|
|
371
375
|
return settleBotTurn(runtime.agent, command, previousEvents);
|
|
372
376
|
} catch (error) {
|
|
373
|
-
turnExecutionError(error, previousEvents, [
|
|
377
|
+
return turnExecutionError(error, previousEvents, [
|
|
374
378
|
...runtime.agent.agent.session.events,
|
|
375
379
|
]);
|
|
376
380
|
} finally {
|
|
377
|
-
|
|
381
|
+
// ADR 0030: a compaction outlives the Turn that triggered it, and it runs
|
|
382
|
+
// on this Composition's model binding — so the Composition outlives the
|
|
383
|
+
// Turn too, and only by as long as the compaction does. Awaiting the
|
|
384
|
+
// disposal here would put the summariser back in the latency path, which
|
|
385
|
+
// is the whole defect. The next admission aborts anything still running,
|
|
386
|
+
// so this can never stack up.
|
|
387
|
+
if (compactionInFlightV1(command.sessionId)) {
|
|
388
|
+
void whenCompactionSettledV1(command.sessionId).then(
|
|
389
|
+
() => composition.dispose(),
|
|
390
|
+
() => composition.dispose(),
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
await composition.dispose();
|
|
394
|
+
}
|
|
378
395
|
}
|
|
379
396
|
}
|
|
380
397
|
|