@frockbot/plugin-shell 0.3.8 → 0.3.10
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 +32 -31
- package/src/backend-configuration.test.ts +3 -1
- package/src/backend-package-catalog.ts +59 -31
- package/src/backend-routines.ts +4 -0
- package/src/backend-subagent-blocking.test.ts +222 -0
- package/src/backend.ts +144 -23
- package/src/client/index.test.ts +73 -0
- package/src/client/index.ts +8 -1
- package/src/client/model-presentation.test.ts +14 -0
- package/src/client/model-presentation.ts +6 -0
- package/src/composition-manifest.test.ts +126 -0
- package/src/composition-manifest.ts +55 -0
- package/src/notification-id.test.ts +67 -0
- package/src/notification-id.ts +58 -0
- package/src/run-protocol.test.ts +81 -0
- package/src/run-protocol.ts +49 -3
- package/src/unread.test.ts +27 -0
- package/src/unread.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./frockbot.json": "./frockbot.json",
|
|
31
31
|
"./host": "./src/host.ts",
|
|
32
32
|
"./manifest": "./src/manifest.ts",
|
|
33
|
+
"./notification-id": "./src/notification-id.ts",
|
|
33
34
|
"./package.json": "./package.json",
|
|
34
35
|
"./run-cursor": "./src/run-cursor.ts",
|
|
35
36
|
"./run-protocol": "./src/run-protocol.ts",
|
|
@@ -47,41 +48,41 @@
|
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
50
|
-
"@frockbot/agent-runtime": "0.3.
|
|
51
|
-
"@frockbot/application-foundation": "0.3.
|
|
52
|
-
"@frockbot/catalog-core": "0.3.
|
|
53
|
-
"@frockbot/client-core": "0.3.
|
|
54
|
-
"@frockbot/client-ui": "0.3.
|
|
55
|
-
"@frockbot/computer-core": "0.3.
|
|
56
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
57
|
-
"@frockbot/configuration-core": "0.3.
|
|
58
|
-
"@frockbot/connection-core": "0.3.
|
|
59
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
60
|
-
"@frockbot/kernel-composition": "0.3.
|
|
61
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
62
|
-
"@frockbot/kernel-do": "0.3.
|
|
63
|
-
"@frockbot/machine-protocol": "0.3.
|
|
64
|
-
"@frockbot/plugin-applets": "0.3.
|
|
65
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
66
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
67
|
-
"@frockbot/plugin-computer": "0.3.
|
|
68
|
-
"@frockbot/plugin-flock": "0.3.
|
|
69
|
-
"@frockbot/plugin-image": "0.3.
|
|
70
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
71
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
72
|
-
"@frockbot/plugin-memory": "0.3.
|
|
73
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
74
|
-
"@frockbot/plugin-routines": "0.3.
|
|
75
|
-
"@frockbot/plugin-skills": "0.3.
|
|
76
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
77
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
78
|
-
"@frockbot/protocol": "0.3.
|
|
51
|
+
"@frockbot/agent-runtime": "0.3.10",
|
|
52
|
+
"@frockbot/application-foundation": "0.3.10",
|
|
53
|
+
"@frockbot/catalog-core": "0.3.10",
|
|
54
|
+
"@frockbot/client-core": "0.3.10",
|
|
55
|
+
"@frockbot/client-ui": "0.3.10",
|
|
56
|
+
"@frockbot/computer-core": "0.3.10",
|
|
57
|
+
"@frockbot/computer-host-protocol": "0.3.10",
|
|
58
|
+
"@frockbot/configuration-core": "0.3.10",
|
|
59
|
+
"@frockbot/connection-core": "0.3.10",
|
|
60
|
+
"@frockbot/kernel-agent-loop": "0.3.10",
|
|
61
|
+
"@frockbot/kernel-composition": "0.3.10",
|
|
62
|
+
"@frockbot/kernel-contracts": "0.3.10",
|
|
63
|
+
"@frockbot/kernel-do": "0.3.10",
|
|
64
|
+
"@frockbot/machine-protocol": "0.3.10",
|
|
65
|
+
"@frockbot/plugin-applets": "0.3.10",
|
|
66
|
+
"@frockbot/plugin-authoring": "0.3.10",
|
|
67
|
+
"@frockbot/plugin-bot-template": "0.3.10",
|
|
68
|
+
"@frockbot/plugin-computer": "0.3.10",
|
|
69
|
+
"@frockbot/plugin-flock": "0.3.10",
|
|
70
|
+
"@frockbot/plugin-image": "0.3.10",
|
|
71
|
+
"@frockbot/plugin-machine-messages": "0.3.10",
|
|
72
|
+
"@frockbot/plugin-mcp": "0.3.10",
|
|
73
|
+
"@frockbot/plugin-memory": "0.3.10",
|
|
74
|
+
"@frockbot/plugin-package-catalog": "0.3.10",
|
|
75
|
+
"@frockbot/plugin-routines": "0.3.10",
|
|
76
|
+
"@frockbot/plugin-skills": "0.3.10",
|
|
77
|
+
"@frockbot/plugin-subagents": "0.3.10",
|
|
78
|
+
"@frockbot/plugin-user-machine": "0.3.10",
|
|
79
|
+
"@frockbot/protocol": "0.3.10",
|
|
79
80
|
"cordis": "4.0.0-rc.8",
|
|
80
81
|
"vue": "3.5.41"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@cloudflare/workers-types": "latest",
|
|
84
|
-
"@frockbot/plugin-tools": "0.3.
|
|
85
|
+
"@frockbot/plugin-tools": "0.3.10",
|
|
85
86
|
"@types/bun": "1.3.6",
|
|
86
87
|
"@types/node": "26.2.0",
|
|
87
88
|
"@vitejs/plugin-vue": "6.0.8",
|
|
@@ -600,8 +600,10 @@ describe("generic per-Turn model resolution", () => {
|
|
|
600
600
|
});
|
|
601
601
|
await expect(contribution.listNotifications()).resolves.toEqual([
|
|
602
602
|
expect.objectContaining({
|
|
603
|
+
// Colons would fail the acknowledge decoder, so the mint replaces
|
|
604
|
+
// them; a notification nobody can acknowledge 400s forever.
|
|
603
605
|
notificationId:
|
|
604
|
-
"package-connection-unavailable
|
|
606
|
+
"package-connection-unavailable-run-1-bot-authored-flock-ai-ambient",
|
|
605
607
|
title: "Connection unavailable",
|
|
606
608
|
}),
|
|
607
609
|
]);
|
|
@@ -320,7 +320,7 @@ export function createPackageCatalogHost(
|
|
|
320
320
|
change.action === "remove"
|
|
321
321
|
? change.input.packageId
|
|
322
322
|
: change.input.catalogId,
|
|
323
|
-
...(change.action === "remove"
|
|
323
|
+
...(change.action === "remove" || change.input.contentHash === undefined
|
|
324
324
|
? {}
|
|
325
325
|
: { contentHash: change.input.contentHash }),
|
|
326
326
|
}),
|
|
@@ -520,9 +520,12 @@ export function createPackageCatalogHost(
|
|
|
520
520
|
index,
|
|
521
521
|
catalogId: provenance.catalogId,
|
|
522
522
|
});
|
|
523
|
+
// A first-party member pins no bundle, so "the entry still offers
|
|
524
|
+
// what the target generation recorded" is the two hashes agreeing,
|
|
525
|
+
// both present or both absent.
|
|
523
526
|
if (
|
|
524
|
-
!targetEntry
|
|
525
|
-
targetEntry.bundle
|
|
527
|
+
!targetEntry ||
|
|
528
|
+
targetEntry.bundle?.contentHash !== provenance.contentHash
|
|
526
529
|
) {
|
|
527
530
|
return undoRefused(
|
|
528
531
|
request,
|
|
@@ -538,7 +541,9 @@ export function createPackageCatalogHost(
|
|
|
538
541
|
version: targetCatalogMember.version,
|
|
539
542
|
catalogId: provenance.catalogId,
|
|
540
543
|
catalogGeneration: provenance.catalogGeneration,
|
|
541
|
-
|
|
544
|
+
...(provenance.contentHash === undefined
|
|
545
|
+
? {}
|
|
546
|
+
: { contentHash: provenance.contentHash }),
|
|
542
547
|
};
|
|
543
548
|
}
|
|
544
549
|
const receipt = await options.user.execute(command);
|
|
@@ -673,17 +678,22 @@ export function createPackageCatalogHost(
|
|
|
673
678
|
const resolved = await pinnedEntry(request.change.input.catalogId);
|
|
674
679
|
user = resolved.user;
|
|
675
680
|
entry = resolved.entry;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
// A first-party entry names reviewed compiled-in code and publishes
|
|
682
|
+
// no bundle: there is no artifact to verify and no hash to pin. The
|
|
683
|
+
// Plugins page has always installed such an entry; refusing it here
|
|
684
|
+
// is what made `package_install` by chat disagree with the UI (F4).
|
|
685
|
+
if (entry.bundle) {
|
|
686
|
+
if (entry.bundle.contentHash !== request.change.input.contentHash) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`Catalog entry "${entry.catalogId}" has content hash "${entry.bundle.contentHash}", not "${request.change.input.contentHash}"`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
await verifyArtifact(entry);
|
|
692
|
+
} else if (request.change.input.contentHash !== undefined) {
|
|
682
693
|
throw new Error(
|
|
683
|
-
`Catalog entry "${entry.catalogId}"
|
|
694
|
+
`Catalog entry "${entry.catalogId}" does not carry a Package bundle`,
|
|
684
695
|
);
|
|
685
696
|
}
|
|
686
|
-
await verifyArtifact(entry);
|
|
687
697
|
const member = base.members.find(
|
|
688
698
|
(candidate) => candidate.packageId === entry.packageId,
|
|
689
699
|
);
|
|
@@ -716,7 +726,7 @@ export function createPackageCatalogHost(
|
|
|
716
726
|
version: entry.version,
|
|
717
727
|
catalogId: entry.catalogId,
|
|
718
728
|
catalogGeneration: resolved.generation,
|
|
719
|
-
contentHash: entry.bundle.contentHash,
|
|
729
|
+
...(entry.bundle ? { contentHash: entry.bundle.contentHash } : {}),
|
|
720
730
|
manifestHash: entry.manifestHash,
|
|
721
731
|
summary:
|
|
722
732
|
request.change.input.summary ??
|
|
@@ -744,16 +754,24 @@ export function createPackageCatalogHost(
|
|
|
744
754
|
// Intent and manifest before the User settings effect. The manifest key
|
|
745
755
|
// is the existing isolate loader seam used by authored Packages.
|
|
746
756
|
if (!existingIntent) {
|
|
757
|
+
const bundle = entry.bundle;
|
|
747
758
|
await options.storage.put({
|
|
748
759
|
[packageCatalogChangeIntentKey(request.effectId)]: intent,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
760
|
+
// A bundle-less entry has no manifest document to store; the mount
|
|
761
|
+
// resolves it from the compiled-in application instead, still
|
|
762
|
+
// against the `manifestHash` the generation records.
|
|
763
|
+
...(bundle
|
|
764
|
+
? {
|
|
765
|
+
[authorshipManifestKey(intent.manifestHash)]: {
|
|
766
|
+
schemaVersion: 1,
|
|
767
|
+
manifestHash: intent.manifestHash,
|
|
768
|
+
packageId: intent.packageId,
|
|
769
|
+
version: intent.version,
|
|
770
|
+
manifest: bundle.manifest,
|
|
771
|
+
createdAt: intent.recordedAt,
|
|
772
|
+
} satisfies AuthoredManifestRecordV1,
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
757
775
|
});
|
|
758
776
|
existingIntent = intent;
|
|
759
777
|
}
|
|
@@ -776,7 +794,9 @@ export function createPackageCatalogHost(
|
|
|
776
794
|
version: intent.version,
|
|
777
795
|
catalogId: intent.catalogId,
|
|
778
796
|
catalogGeneration: intent.catalogGeneration,
|
|
779
|
-
|
|
797
|
+
...(intent.contentHash === undefined
|
|
798
|
+
? {}
|
|
799
|
+
: { contentHash: intent.contentHash }),
|
|
780
800
|
};
|
|
781
801
|
const receipt = await options.user.execute(command);
|
|
782
802
|
if (receipt.status === "rejected") {
|
|
@@ -789,7 +809,7 @@ export function createPackageCatalogHost(
|
|
|
789
809
|
(member) => member.packageId !== intent.packageId,
|
|
790
810
|
);
|
|
791
811
|
if (action !== "remove") {
|
|
792
|
-
const bundle = entry.bundle
|
|
812
|
+
const bundle = entry.bundle;
|
|
793
813
|
const member: CompositionMemberV1 = {
|
|
794
814
|
packageId: entry.packageId,
|
|
795
815
|
specifier: `catalog:${entry.catalogId}`,
|
|
@@ -801,14 +821,20 @@ export function createPackageCatalogHost(
|
|
|
801
821
|
version: entry.version,
|
|
802
822
|
catalogId: entry.catalogId,
|
|
803
823
|
catalogGeneration: intent.catalogGeneration,
|
|
804
|
-
contentHash: bundle.contentHash,
|
|
805
|
-
},
|
|
806
|
-
artifact: {
|
|
807
|
-
contentHash: bundle.contentHash,
|
|
808
|
-
size: bundle.size,
|
|
809
|
-
mediaType: bundle.mediaType,
|
|
810
|
-
bundlerVersion: bundle.bundlerVersion,
|
|
824
|
+
...(bundle ? { contentHash: bundle.contentHash } : {}),
|
|
811
825
|
},
|
|
826
|
+
// No artifact ⇒ the member mounts in the kernel isolate from the
|
|
827
|
+
// compiled-in Package, exactly as the UI install records it.
|
|
828
|
+
...(bundle
|
|
829
|
+
? {
|
|
830
|
+
artifact: {
|
|
831
|
+
contentHash: bundle.contentHash,
|
|
832
|
+
size: bundle.size,
|
|
833
|
+
mediaType: bundle.mediaType,
|
|
834
|
+
bundlerVersion: bundle.bundlerVersion,
|
|
835
|
+
},
|
|
836
|
+
}
|
|
837
|
+
: {}),
|
|
812
838
|
};
|
|
813
839
|
members.push(member);
|
|
814
840
|
}
|
|
@@ -850,7 +876,9 @@ export function createPackageCatalogHost(
|
|
|
850
876
|
packageId: intent.packageId,
|
|
851
877
|
displayName: intent.displayName,
|
|
852
878
|
...(action === "remove" ? {} : { version: intent.version }),
|
|
853
|
-
...(action === "remove"
|
|
879
|
+
...(action === "remove" || intent.contentHash === undefined
|
|
880
|
+
? {}
|
|
881
|
+
: { contentHash: intent.contentHash }),
|
|
854
882
|
generationId: generation.generationId,
|
|
855
883
|
missingConnectionTypes: intent.missingConnectionTypes,
|
|
856
884
|
recordedAt: intent.recordedAt,
|
package/src/backend-routines.ts
CHANGED
|
@@ -301,6 +301,10 @@ export function routineInboxEntryViewV1(
|
|
|
301
301
|
...(entry.acknowledgedAt === undefined
|
|
302
302
|
? {}
|
|
303
303
|
: { acknowledgedAt: entry.acknowledgedAt }),
|
|
304
|
+
...(entry.repeatCount === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { repeatCount: entry.repeatCount }),
|
|
307
|
+
...(entry.failure === undefined ? {} : { failure: entry.failure }),
|
|
304
308
|
};
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// The foreground `Task` wait (memory-v2 F4).
|
|
2
|
+
//
|
|
3
|
+
// A `Task {background:false}` that answered "dispatched" the instant anything
|
|
4
|
+
// went wrong read to the model as a call that had finished with no output. It
|
|
5
|
+
// then spent two more steps on `task_check` and `task_resume`. The wait must
|
|
6
|
+
// hold until the child settles, and give up only when the record is really
|
|
7
|
+
// unreadable.
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import type { UserSettingsViewV1 } from "@frockbot/configuration-core";
|
|
10
|
+
import type {
|
|
11
|
+
TaskOutcomeV1,
|
|
12
|
+
TaskRecordV1,
|
|
13
|
+
} from "@frockbot/plugin-subagents/records";
|
|
14
|
+
import { taskKeyV1 } from "@frockbot/plugin-subagents/storage-keys";
|
|
15
|
+
import type { ShellBotBackendHost } from "./backend.js";
|
|
16
|
+
|
|
17
|
+
const identity = { userId: "user-1", botId: "primary" };
|
|
18
|
+
const TASK_ID = "task-1";
|
|
19
|
+
|
|
20
|
+
class MemoryStorage {
|
|
21
|
+
readonly values = new Map<string, unknown>();
|
|
22
|
+
/** Reads to fail before the first success, to stand in for contention. */
|
|
23
|
+
failReads = 0;
|
|
24
|
+
reads = 0;
|
|
25
|
+
|
|
26
|
+
get<T>(key: string): Promise<T | undefined> {
|
|
27
|
+
this.reads += 1;
|
|
28
|
+
if (this.failReads > 0) {
|
|
29
|
+
this.failReads -= 1;
|
|
30
|
+
return Promise.reject(new Error("storage is busy"));
|
|
31
|
+
}
|
|
32
|
+
return Promise.resolve(
|
|
33
|
+
structuredClone(this.values.get(key)) as T | undefined,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
put(key: string | Record<string, unknown>, value?: unknown): Promise<void> {
|
|
38
|
+
if (typeof key === "string") this.values.set(key, structuredClone(value));
|
|
39
|
+
else {
|
|
40
|
+
for (const [entry, item] of Object.entries(key)) {
|
|
41
|
+
this.values.set(entry, structuredClone(item));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
delete(key: string): Promise<boolean> {
|
|
48
|
+
return Promise.resolve(this.values.delete(key));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
list<T>(options: { prefix?: string }): Promise<Map<string, T>> {
|
|
52
|
+
return Promise.resolve(
|
|
53
|
+
new Map(
|
|
54
|
+
[...this.values.entries()].filter(([key]) =>
|
|
55
|
+
key.startsWith(options.prefix ?? ""),
|
|
56
|
+
) as Array<[string, T]>,
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
transaction<T>(callback: (storage: MemoryStorage) => Promise<T>): Promise<T> {
|
|
62
|
+
return callback(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setAlarm(): Promise<void> {
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
deleteAlarm(): Promise<void> {
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const user: UserSettingsViewV1 = {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
revision: 0,
|
|
77
|
+
profile: { name: "User" },
|
|
78
|
+
packages: [],
|
|
79
|
+
connections: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function host(storage: MemoryStorage): ShellBotBackendHost {
|
|
83
|
+
return {
|
|
84
|
+
state: { storage } as unknown as DurableObjectState,
|
|
85
|
+
env: {
|
|
86
|
+
USER_CONFIGURATIONS: {
|
|
87
|
+
idFromName: () => "user-id",
|
|
88
|
+
get: () => ({ readConfiguration: () => Promise.resolve(user) }),
|
|
89
|
+
},
|
|
90
|
+
} as unknown as ShellBotBackendHost["env"],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Waiting {
|
|
95
|
+
sleeps: number;
|
|
96
|
+
materializeSettings(
|
|
97
|
+
identity: { userId: string; botId: string },
|
|
98
|
+
profile: { name: string },
|
|
99
|
+
): Promise<unknown>;
|
|
100
|
+
wait(taskId: string): Promise<TaskOutcomeV1 | undefined>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Exposes the wait and removes its sleep, which is the whole point of it.
|
|
105
|
+
*
|
|
106
|
+
* The class is reached through a dynamic import rather than a top-level one:
|
|
107
|
+
* the Shell's backend and the foundation application import each other, and a
|
|
108
|
+
* static import from a test module reads the class while it is still
|
|
109
|
+
* initializing.
|
|
110
|
+
*/
|
|
111
|
+
async function waiting(host: ShellBotBackendHost): Promise<Waiting> {
|
|
112
|
+
// The application first: it is the half of the cycle that has to finish
|
|
113
|
+
// initializing before the Shell's class is readable.
|
|
114
|
+
await import("@frockbot/application-foundation/contributions");
|
|
115
|
+
const { ShellBotBackendContribution } = await import("./backend.js");
|
|
116
|
+
return new (class extends ShellBotBackendContribution {
|
|
117
|
+
sleeps = 0;
|
|
118
|
+
|
|
119
|
+
protected override sleep(): Promise<void> {
|
|
120
|
+
this.sleeps += 1;
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
wait(taskId: string): Promise<TaskOutcomeV1 | undefined> {
|
|
125
|
+
return this.awaitBlockingTask(identity, taskId, taskId);
|
|
126
|
+
}
|
|
127
|
+
})(host) as unknown as Waiting;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function record(overrides: Partial<TaskRecordV1> = {}): TaskRecordV1 {
|
|
131
|
+
return {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
taskId: TASK_ID,
|
|
134
|
+
type: "executor",
|
|
135
|
+
description: "read it",
|
|
136
|
+
promptDigest: "sha256:digest",
|
|
137
|
+
model: {
|
|
138
|
+
binding: {
|
|
139
|
+
packageId: "models",
|
|
140
|
+
capabilityId: "llm",
|
|
141
|
+
connectionId: "connection-1",
|
|
142
|
+
provider: "foundation",
|
|
143
|
+
providerModelId: "foundation-model",
|
|
144
|
+
},
|
|
145
|
+
slug: "foundation-model",
|
|
146
|
+
},
|
|
147
|
+
compositionGenerationId: "generation-1",
|
|
148
|
+
background: false,
|
|
149
|
+
depth: 1,
|
|
150
|
+
status: "running",
|
|
151
|
+
dispatch: {
|
|
152
|
+
runId: "run-1",
|
|
153
|
+
turnId: "turn-1",
|
|
154
|
+
sessionId: "user-1:primary",
|
|
155
|
+
},
|
|
156
|
+
childSessionId: `task:${TASK_ID}`,
|
|
157
|
+
attachments: [],
|
|
158
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
159
|
+
deadlineAt: "2026-09-03T00:30:00.000Z",
|
|
160
|
+
...overrides,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function fixture(
|
|
165
|
+
stored: TaskRecordV1,
|
|
166
|
+
): Promise<{ storage: MemoryStorage; contribution: Waiting }> {
|
|
167
|
+
const storage = new MemoryStorage();
|
|
168
|
+
const contribution = await waiting(host(storage));
|
|
169
|
+
await contribution.materializeSettings(identity, { name: "Primary" });
|
|
170
|
+
storage.values.set(taskKeyV1(stored.taskId), structuredClone(stored));
|
|
171
|
+
return { storage, contribution };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe("the foreground Task wait", () => {
|
|
175
|
+
test("answers with the child's settled outcome", async () => {
|
|
176
|
+
const { contribution } = await fixture(
|
|
177
|
+
record({
|
|
178
|
+
status: "completed",
|
|
179
|
+
outcome: {
|
|
180
|
+
status: "completed",
|
|
181
|
+
settledAt: "2026-09-03T00:00:10.000Z",
|
|
182
|
+
summary: "The root has six entries.",
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(await contribution.wait(TASK_ID)).toMatchObject({
|
|
188
|
+
status: "completed",
|
|
189
|
+
summary: "The root has six entries.",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// The regression: one failed read used to end the wait, and the Task tool
|
|
194
|
+
// then told the model the child was still running — for a child that had
|
|
195
|
+
// already settled.
|
|
196
|
+
test("rides out a transient read failure instead of giving up", async () => {
|
|
197
|
+
const { storage, contribution } = await fixture(
|
|
198
|
+
record({
|
|
199
|
+
status: "completed",
|
|
200
|
+
outcome: {
|
|
201
|
+
status: "completed",
|
|
202
|
+
settledAt: "2026-09-03T00:00:10.000Z",
|
|
203
|
+
summary: "The root has six entries.",
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
storage.failReads = 2;
|
|
208
|
+
|
|
209
|
+
expect(await contribution.wait(TASK_ID)).toMatchObject({
|
|
210
|
+
status: "completed",
|
|
211
|
+
summary: "The root has six entries.",
|
|
212
|
+
});
|
|
213
|
+
expect(contribution.sleeps).toBeGreaterThan(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("gives up when the record stays unreadable", async () => {
|
|
217
|
+
const { storage, contribution } = await fixture(record());
|
|
218
|
+
storage.failReads = Number.MAX_SAFE_INTEGER;
|
|
219
|
+
|
|
220
|
+
expect(await contribution.wait(TASK_ID)).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
});
|