@zq-silk/yui 2.7.1 → 2.8.1
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/dist/cli/commandCatalog.js +8 -7
- package/dist/cli/updateCommand.js +3 -0
- package/dist/cli/updatePorts.js +20 -0
- package/dist/commands/configCommands.js +1 -0
- package/dist/commands/globalRoleCommands.js +42 -35
- package/dist/commands/inboxCommands.js +60 -40
- package/dist/commands/taskCommands.js +7 -3
- package/dist/commands/taskRoleRuntimeStatus.js +2 -0
- package/dist/context/runContextPack.js +48 -3
- package/dist/context/taskContext.js +36 -12
- package/dist/controlPlaneCli.js +64 -17
- package/dist/controller/controller.js +5 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +9 -4
- package/dist/controller/globalInputDelivery.js +17 -5
- package/dist/controller/globalRuntimeAttention.js +135 -5
- package/dist/controller/runtime.js +4 -0
- package/dist/controller/structuredProviderObservation.js +51 -15
- package/dist/controller/taskHostTerminalRecovery.js +33 -0
- package/dist/executor/fileRoleLaunchPlanner.js +4 -9
- package/dist/message/inboxReceipt.js +30 -0
- package/dist/message/taskInboxOwnership.js +25 -3
- package/dist/models/modelManagement.js +469 -0
- package/dist/models/modelManagementConfig.js +46 -0
- package/dist/models/modelManagementFiles.js +90 -0
- package/dist/models/modelManagementHttp.js +90 -0
- package/dist/models/modelManagementTypes.js +1 -0
- package/dist/output/boundedRead.js +43 -14
- package/dist/runtime/agentEndpointIdentity.js +8 -4
- package/dist/runtime/agentEndpointOwnership.js +5 -6
- package/dist/runtime/agentHost.js +62 -10
- package/dist/runtime/agentHostImplementation.js +18 -0
- package/dist/runtime/managedCaller.js +25 -1
- package/dist/runtime/managedIdentity.js +1 -1
- package/dist/runtime/providerRetry.js +19 -1
- package/dist/runtime/sessionCaller.js +53 -0
- package/dist/storage/contextRecords.js +28 -0
- package/dist/storage/maintenanceCaller.js +69 -0
- package/dist/storage/sqliteSchema.js +12 -2
- package/dist/storage/sqliteStore.js +9 -5
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +4 -0
- package/dist/task/completionReadiness.js +3 -3
- package/dist/web/assets/assetManifest.js +28 -2
- package/dist/web/assets/client/app/manage.js +6 -0
- package/dist/web/assets/client/app/models.js +20 -0
- package/dist/web/assets/client/app/settings.js +41 -9
- package/dist/web/assets/client/i18n/en.js +180 -2
- package/dist/web/assets/client/i18n/zh-CN.js +178 -2
- package/dist/web/assets/client/index.js +8 -0
- package/dist/web/assets/client/layout/manageShell.js +94 -0
- package/dist/web/assets/client/lib/api.js +13 -1
- package/dist/web/assets/client/lib/i18n.js +5 -3
- package/dist/web/assets/client/views/dock/conversation.js +231 -80
- package/dist/web/assets/client/views/models.js +777 -0
- package/dist/web/assets/client/views/skills.js +22 -15
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shared/manageNav.js +13 -0
- package/dist/web/assets/shell/manage.js +54 -0
- package/dist/web/assets/shell/models.js +7 -0
- package/dist/web/assets/shell/settings.js +1 -35
- package/dist/web/assets/shell/settingsContent.js +30 -0
- package/dist/web/assets/shell/sidebar.js +1 -0
- package/dist/web/assets/styles/index.js +6 -0
- package/dist/web/assets/styles/views/conversation.js +133 -0
- package/dist/web/assets/styles/views/manage.js +71 -0
- package/dist/web/assets/styles/views/models.js +129 -0
- package/dist/web/assets/styles/views/settings.js +28 -14
- package/dist/web/assets/styles/views/sidebar.js +1 -0
- package/dist/web/webServer.js +59 -9
- package/docs/cli-information-contract.md +42 -11
- package/docs/cli-information-contract.zh-CN.md +28 -7
- package/docs/managed-turn-and-session-runtime.md +47 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +32 -0
- package/docs/release-workflow.md +80 -7
- package/docs/release-workflow.zh-CN.md +55 -4
- package/docs/storage-baseline.md +6 -1
- package/docs/storage-baseline.zh-CN.md +4 -1
- package/docs/testing/review-evaluation.md +148 -0
- package/docs/testing/review-evaluation.zh-CN.md +117 -0
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +3 -0
- package/package.json +1 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/execution.md +29 -0
- package/skills/yui-leader/references/replicated-execution.md +7 -0
- package/skills/yui-operator/SKILL.md +5 -1
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +35 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +53 -7
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/recovery.md +24 -5
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { request as httpsRequest } from "node:https";
|
|
4
|
+
import { BlockList, isIP } from "node:net";
|
|
5
|
+
import { modelUrl } from "./modelManagementConfig.js";
|
|
6
|
+
const denied = new BlockList();
|
|
7
|
+
for (const [address, bits] of [
|
|
8
|
+
["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["169.254.0.0", 16],
|
|
9
|
+
["172.16.0.0", 12], ["192.0.0.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15],
|
|
10
|
+
["224.0.0.0", 4], ["240.0.0.0", 4]
|
|
11
|
+
])
|
|
12
|
+
denied.addSubnet(address, bits, "ipv4");
|
|
13
|
+
for (const [address, bits] of [["::", 128], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8], ["::ffff:0:0", 96]]) {
|
|
14
|
+
denied.addSubnet(address, bits, "ipv6");
|
|
15
|
+
}
|
|
16
|
+
/** Small fixed-route client: pinned DNS, no redirects/proxies, deadline and byte cap. */
|
|
17
|
+
export async function readModelJson(base, suffix, key, timeoutMs) {
|
|
18
|
+
const url = modelUrl(`${base.replace(/\/+$/, "")}${suffix}`);
|
|
19
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
20
|
+
const explicitLoopback = ["127.0.0.1", "::1", "localhost"].includes(hostname);
|
|
21
|
+
const abort = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
23
|
+
try {
|
|
24
|
+
const addresses = await Promise.race([
|
|
25
|
+
isIP(hostname) ? Promise.resolve([{ address: hostname, family: isIP(hostname) }]) : lookup(hostname, { all: true }),
|
|
26
|
+
new Promise((_, reject) => abort.signal.addEventListener("abort", () => reject(new Error("timeout")), { once: true }))
|
|
27
|
+
]);
|
|
28
|
+
if (abort.signal.aborted || !addresses.length || addresses.some(({ address, family }) => {
|
|
29
|
+
const loopback = address === "::1" || address.startsWith("127.");
|
|
30
|
+
return (loopback && !explicitLoopback) || (!loopback && explicitLoopback)
|
|
31
|
+
|| (!loopback && denied.check(address, family === 6 ? "ipv6" : "ipv4"));
|
|
32
|
+
}))
|
|
33
|
+
throw new Error("target-rejected");
|
|
34
|
+
const selected = addresses[0];
|
|
35
|
+
return await new Promise((resolve, reject) => {
|
|
36
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
37
|
+
method: "GET", signal: abort.signal, agent: false,
|
|
38
|
+
headers: { accept: "application/json", ...(key ? { authorization: `Bearer ${key}` } : {}) },
|
|
39
|
+
lookup: (_name, options, callback) => options.all
|
|
40
|
+
? callback(null, addresses)
|
|
41
|
+
: callback(null, selected.address, selected.family)
|
|
42
|
+
}, response => {
|
|
43
|
+
if (response.statusCode !== 200) {
|
|
44
|
+
response.destroy();
|
|
45
|
+
reject(new Error(response.statusCode === 401 || response.statusCode === 403 ? "authentication-failed"
|
|
46
|
+
: response.statusCode === 404 ? "route-unavailable" : "service-failed"));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
const chunks = [];
|
|
51
|
+
response.on("data", chunk => {
|
|
52
|
+
bytes += chunk.length;
|
|
53
|
+
if (bytes > 2 * 1024 * 1024) {
|
|
54
|
+
response.destroy();
|
|
55
|
+
reject(new Error("response-too-large"));
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
});
|
|
60
|
+
response.on("error", () => reject(new Error("read-failed")));
|
|
61
|
+
response.on("end", () => {
|
|
62
|
+
try {
|
|
63
|
+
// Even schema-allowed upstream strings must not echo our credential.
|
|
64
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
65
|
+
const parsed = JSON.parse(text);
|
|
66
|
+
resolve(key ? redact(parsed, key) : parsed);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
reject(new Error("invalid-response"));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
request.on("error", () => reject(new Error(abort.signal.aborted ? "timeout" : "connection-failed")));
|
|
74
|
+
request.end();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function redact(value, key) {
|
|
82
|
+
if (typeof value === "string")
|
|
83
|
+
return value.split(key).join("[redacted]");
|
|
84
|
+
if (Array.isArray(value))
|
|
85
|
+
return value.map(item => redact(item, key));
|
|
86
|
+
if (value && typeof value === "object")
|
|
87
|
+
return Object.fromEntries(Object.entries(value)
|
|
88
|
+
.map(([name, item]) => [name.split(key).join("[redacted]"), redact(item, key)]));
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,6 +2,30 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { usageError } from "../errors/cliError.js";
|
|
3
3
|
export const READ_PAGE_BYTES = 32 * 1024;
|
|
4
4
|
export const INLINE_DOCUMENT_BYTES = 16 * 1024;
|
|
5
|
+
/** Compact JSON wire budget, including the CLI envelope and small command
|
|
6
|
+
* wrappers (for example Run Context's `context` field). Never count just items. */
|
|
7
|
+
export function fitsReadBudget(value, bytes = READ_PAGE_BYTES) {
|
|
8
|
+
return Buffer.byteLength(JSON.stringify({ ok: true, data: value })) + 256 <= bytes;
|
|
9
|
+
}
|
|
10
|
+
export function summaryText(text, bytes = 512) {
|
|
11
|
+
if (Buffer.byteLength(text) <= bytes)
|
|
12
|
+
return text;
|
|
13
|
+
let result = "";
|
|
14
|
+
let size = 0;
|
|
15
|
+
for (const character of text) {
|
|
16
|
+
const length = Buffer.byteLength(character);
|
|
17
|
+
if (size + length + 3 > bytes)
|
|
18
|
+
break;
|
|
19
|
+
result += character;
|
|
20
|
+
size += length;
|
|
21
|
+
}
|
|
22
|
+
return result + "…";
|
|
23
|
+
}
|
|
24
|
+
/** List-only nested metadata. The containing entry must expose an exact read. */
|
|
25
|
+
export function summaryMetadata(value, budget = 2048) {
|
|
26
|
+
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
27
|
+
return bytes <= budget ? value : { omitted: true, digest: readDigest(value), bytes };
|
|
28
|
+
}
|
|
5
29
|
function assertSource(source) {
|
|
6
30
|
if (Buffer.byteLength(source) > 1024)
|
|
7
31
|
throw usageError("Read source identity exceeds its 1024-byte budget.");
|
|
@@ -22,7 +46,7 @@ export function encodeReadCursor(cursor) {
|
|
|
22
46
|
export function decodeReadCursor(raw, source, digest) {
|
|
23
47
|
let cursor;
|
|
24
48
|
try {
|
|
25
|
-
if (raw.length >
|
|
49
|
+
if (raw.length > 16 * 1024)
|
|
26
50
|
throw new Error();
|
|
27
51
|
cursor = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
28
52
|
if (cursor.source !== source || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0
|
|
@@ -46,7 +70,7 @@ export function readLimit(value) {
|
|
|
46
70
|
/** For small in-memory authorized collections. Authorize/filter BEFORE calling.
|
|
47
71
|
* Cursors bind the selected collection, never grant access, and store no state.
|
|
48
72
|
*/
|
|
49
|
-
export function recordPage(records, source, options = {}) {
|
|
73
|
+
export function recordPage(records, source, options = {}, metadata = {}) {
|
|
50
74
|
assertSource(source);
|
|
51
75
|
const limit = readLimit(options.limit);
|
|
52
76
|
const digest = readDigest(records);
|
|
@@ -54,24 +78,29 @@ export function recordPage(records, source, options = {}) {
|
|
|
54
78
|
if (offset > records.length)
|
|
55
79
|
throw usageError("Read cursor exceeds this collection.");
|
|
56
80
|
const items = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
const page = () => {
|
|
82
|
+
const end = offset + items.length;
|
|
83
|
+
return {
|
|
84
|
+
...metadata,
|
|
85
|
+
source, digest, items, total: records.length, complete: end === records.length,
|
|
86
|
+
nextCursor: end < records.length ? encodeReadCursor({ source, digest, offset: end }) : null,
|
|
87
|
+
limits: { items: limit, bytes: READ_PAGE_BYTES }
|
|
88
|
+
};
|
|
89
|
+
};
|
|
60
90
|
for (const record of records.slice(offset, offset + limit)) {
|
|
61
91
|
const size = Buffer.byteLength(JSON.stringify(record)) + 1;
|
|
62
92
|
if (size > READ_PAGE_BYTES / 2)
|
|
63
93
|
throw usageError("List entry exceeds its summary budget; read the exact record.");
|
|
64
|
-
if (bytes + size > READ_PAGE_BYTES - 4096)
|
|
65
|
-
break;
|
|
66
94
|
items.push(record);
|
|
67
|
-
|
|
95
|
+
if (!fitsReadBudget(page())) {
|
|
96
|
+
items.pop();
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
68
99
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
limits: { items: limit, bytes: READ_PAGE_BYTES }
|
|
74
|
-
};
|
|
100
|
+
if (!fitsReadBudget(page()) || items.length === 0 && offset < records.length) {
|
|
101
|
+
throw usageError("List entry and read identity exceed the response budget; read the exact record.");
|
|
102
|
+
}
|
|
103
|
+
return page();
|
|
75
104
|
}
|
|
76
105
|
/** Small exact details stay native JSON. Large details explicitly become a
|
|
77
106
|
* contentPage. Concatenate text in order, then JSON.parse once, never each chunk.
|
|
@@ -78,12 +78,16 @@ export function validateAgentEndpointImplementation(ref) {
|
|
|
78
78
|
}
|
|
79
79
|
return ref;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The built-in adapter identity is the supported native connection contract.
|
|
83
|
+
* Build generations are provenance, not a requirement to retain old code.
|
|
84
|
+
* Actual native resume still validates the conversation/account/configuration.
|
|
85
|
+
*/
|
|
82
86
|
export function requireBuiltinAgentEndpointImplementation(adapterId, ref) {
|
|
83
87
|
validateAgentEndpointImplementation(ref);
|
|
84
88
|
const current = builtinAgentEndpointImplementation(adapterId);
|
|
85
|
-
if (ref.id !== current.id
|
|
86
|
-
throw new Error(`Session Endpoint ${ref.id}
|
|
89
|
+
if (ref.id !== current.id) {
|
|
90
|
+
throw new Error(`Session Endpoint ${ref.id} is incompatible with adapter ${adapterId}.`);
|
|
87
91
|
}
|
|
88
|
-
return
|
|
92
|
+
return current;
|
|
89
93
|
}
|
|
@@ -8,9 +8,9 @@ import { createAgentEndpointFactory } from "./agentEndpoint.js";
|
|
|
8
8
|
* owns its own Host in its own process and never holds an Endpoint client, so
|
|
9
9
|
* the reference facts for a Session live where the Session actually runs.
|
|
10
10
|
*
|
|
11
|
-
* Only the running code can be attached here.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Only the running code can be attached here. This owner proves the loaded
|
|
12
|
+
* generation is held and released; historical launch generations do not select
|
|
13
|
+
* old code or block a compatible native resume.
|
|
14
14
|
*/
|
|
15
15
|
export function createAgentEndpointOwner(factory = createAgentEndpointFactory()) {
|
|
16
16
|
const host = new InstanceHost();
|
|
@@ -48,9 +48,8 @@ export function createAgentEndpointOwner(factory = createAgentEndpointFactory())
|
|
|
48
48
|
await holder.handle.release();
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* resume, never a silent start on a different generation.
|
|
51
|
+
* Hold the current implementation for one Session. A historical reference
|
|
52
|
+
* must name the same built-in adapter, not the same package build.
|
|
54
53
|
*/
|
|
55
54
|
const pin = (adapterId, pinned) => {
|
|
56
55
|
if (pinned !== undefined)
|
|
@@ -5,7 +5,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
|
|
8
|
-
import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
|
|
8
|
+
import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, replayStructuredProviderPersistence, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
|
|
9
9
|
import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
|
|
10
10
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
11
11
|
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
@@ -14,6 +14,7 @@ import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
|
|
|
14
14
|
import { createAgentEndpointOwner } from "./agentEndpointOwnership.js";
|
|
15
15
|
import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
|
|
16
16
|
import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
|
|
17
|
+
import { AGENT_HOST_IMPLEMENTATION } from "./agentHostImplementation.js";
|
|
17
18
|
import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
|
|
18
19
|
import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
19
20
|
import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
|
|
@@ -188,6 +189,9 @@ export async function runAgentHost(input) {
|
|
|
188
189
|
&& previous.started.nativeSessionId === session?.nativeSessionId) {
|
|
189
190
|
await publishStructuredProviderTerminal({
|
|
190
191
|
home: input.home, environment: previous.payload.environment, terminal: previous.terminal
|
|
192
|
+
}).catch(() => {
|
|
193
|
+
// The publisher retains the original unpersisted bytes. Still
|
|
194
|
+
// observe the new start; a failed old write must not hide its writer.
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
if (session !== undefined && started.nativeSessionId === session.nativeSessionId
|
|
@@ -594,6 +598,8 @@ export async function runAgentHost(input) {
|
|
|
594
598
|
throw new Error("Agent Host accepts only managed Provider control launches.");
|
|
595
599
|
}
|
|
596
600
|
if (activeRunPayload !== undefined
|
|
601
|
+
|| sessionPayload !== undefined
|
|
602
|
+
&& (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
|
|
597
603
|
|| ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
598
604
|
throw new Error("Agent Host still owns an unsettled Provider Turn.");
|
|
599
605
|
}
|
|
@@ -624,8 +630,7 @@ export async function runAgentHost(input) {
|
|
|
624
630
|
|| providerControl.mode !== "resume"
|
|
625
631
|
|| providerControl.nativeSessionId !== session.nativeSessionId
|
|
626
632
|
|| (providerControl.endpointImplementation !== undefined
|
|
627
|
-
&&
|
|
628
|
-
|| providerControl.endpointImplementation.generation !== session.configuration.implementation.generation))) {
|
|
633
|
+
&& providerControl.endpointImplementation.id !== session.configuration.implementation.id)) {
|
|
629
634
|
throw new Error("Agent Host launch does not match its live Provider Conversation.");
|
|
630
635
|
}
|
|
631
636
|
sessionPayload = next;
|
|
@@ -638,9 +643,8 @@ export async function runAgentHost(input) {
|
|
|
638
643
|
}
|
|
639
644
|
// The Controller revalidates the adopted execution environment while
|
|
640
645
|
// redeeming this launch; a persistent Host must not reopen its schema.
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// fails here, rather than silently starting on a different generation.
|
|
646
|
+
// Hold the code actually loaded by this Host. Historical build identity
|
|
647
|
+
// is provenance; native resume and current authority remain mandatory.
|
|
644
648
|
endpointLease ??= endpointOwner.pin(providerControl.adapterId, providerControl.endpointImplementation);
|
|
645
649
|
endpointAdapterId = providerControl.adapterId;
|
|
646
650
|
const started = await (providerControl.mode === "new"
|
|
@@ -786,6 +790,7 @@ export async function runAgentHost(input) {
|
|
|
786
790
|
throw new Error("Agent Host rejected a stale Provider writer fence.");
|
|
787
791
|
}
|
|
788
792
|
if (activeRunPayload !== undefined || snapshot.state === "settling"
|
|
793
|
+
|| (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
|
|
789
794
|
|| session.inspect().activeNativeTurnId !== undefined) {
|
|
790
795
|
operation.failure = providerDeliveryFailure({
|
|
791
796
|
detail: "Agent Host still owns an unsettled Provider Turn.",
|
|
@@ -1042,7 +1047,9 @@ export async function runAgentHost(input) {
|
|
|
1042
1047
|
if (session === undefined || request.nativeSessionId !== session.nativeSessionId) {
|
|
1043
1048
|
throw new Error("Agent Host authority targets a different Provider Conversation.");
|
|
1044
1049
|
}
|
|
1045
|
-
if (activeRunPayload !== undefined
|
|
1050
|
+
if (activeRunPayload !== undefined || session.inspect().activeNativeTurnId !== undefined
|
|
1051
|
+
|| sessionPayload !== undefined
|
|
1052
|
+
&& (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
|
|
1046
1053
|
|| ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
1047
1054
|
throw new Error("Agent Host authority cannot transfer while a AgentRun is unsettled.");
|
|
1048
1055
|
}
|
|
@@ -1118,7 +1125,9 @@ export async function runAgentHost(input) {
|
|
|
1118
1125
|
return enqueueSerialized(async () => {
|
|
1119
1126
|
const current = session;
|
|
1120
1127
|
if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
|
|
1121
|
-
|| !sessionPayload ||
|
|
1128
|
+
|| !sessionPayload || activeRunPayload || activeNativeTurnId
|
|
1129
|
+
|| (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
|
|
1130
|
+
|| authority?.owner !== "controller"
|
|
1122
1131
|
|| (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
|
|
1123
1132
|
|| (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
|
|
1124
1133
|
throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
|
|
@@ -1192,6 +1201,27 @@ export async function runAgentHost(input) {
|
|
|
1192
1201
|
|| authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
|
|
1193
1202
|
throw new Error("Terminal reconciliation targets a different Session or authority.");
|
|
1194
1203
|
}
|
|
1204
|
+
const recovered = await enqueueSerialized(async () => {
|
|
1205
|
+
if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
|
|
1206
|
+
|| !sameProviderAuthorityFence(authority, request.authority)) {
|
|
1207
|
+
throw new Error("Session changed before original event persistence recovery.");
|
|
1208
|
+
}
|
|
1209
|
+
const persisted = await replayStructuredProviderPersistence(input.home, sessionPayload.environment);
|
|
1210
|
+
if (persisted.length > 0 && activeRunPayload === undefined) {
|
|
1211
|
+
const active = session.inspect().activeNativeTurnId;
|
|
1212
|
+
updateSnapshot(hostSnapshot(active === undefined ? "idle" : "busy", {
|
|
1213
|
+
adapterId: session.adapterId, processInstanceId: session.processInstanceId,
|
|
1214
|
+
nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
|
|
1215
|
+
nativeTurnId: active, ...authorityFields()
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
return persisted.some(event => ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.kind)
|
|
1219
|
+
&& event.fence.nativeSessionId === request.nativeSessionId
|
|
1220
|
+
&& (request.nativeTurnId === undefined || event.fence.nativeTurnId === request.nativeTurnId)
|
|
1221
|
+
&& (event.fence.receiptId === request.attemptId
|
|
1222
|
+
|| event.fence.receiptId === undefined && request.nativeTurnId !== undefined
|
|
1223
|
+
&& request.attemptId === `direct:${request.nativeTurnId}`));
|
|
1224
|
+
});
|
|
1195
1225
|
const direct = directObservation;
|
|
1196
1226
|
const directMatch = direct?.terminal !== undefined
|
|
1197
1227
|
&& request.nativeTurnId !== undefined
|
|
@@ -1205,7 +1235,8 @@ export async function runAgentHost(input) {
|
|
|
1205
1235
|
|| terminal.nativeSessionId !== request.nativeSessionId
|
|
1206
1236
|
|| !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
|
|
1207
1237
|
|| request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
|
|
1208
|
-
return { ...controlResult("status", snapshot),
|
|
1238
|
+
return { ...controlResult("status", snapshot),
|
|
1239
|
+
reconciliation: recovered ? "terminal-republished" : "unobserved" };
|
|
1209
1240
|
}
|
|
1210
1241
|
// Serialize with submission/terminal persistence. The original Host and
|
|
1211
1242
|
// original launch envelope remain the observer, including the full result.
|
|
@@ -1539,7 +1570,18 @@ export async function inspectAgentHost(input) {
|
|
|
1539
1570
|
export async function reconcileAgentHostTurn(input) {
|
|
1540
1571
|
// Periodic observation must not hold the scheduler behind an unavailable Host.
|
|
1541
1572
|
// A timeout is only an observation failure, never permission to submit again.
|
|
1542
|
-
|
|
1573
|
+
const path = agentHostControlSocketPath(input);
|
|
1574
|
+
const observed = await exchangeAgentHostControl(path, {
|
|
1575
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status"
|
|
1576
|
+
}, 1_000);
|
|
1577
|
+
if (observed.outcome === "rejected")
|
|
1578
|
+
return observed;
|
|
1579
|
+
if (!observed.snapshot.controlCapabilities?.includes("reconcile-turn")) {
|
|
1580
|
+
// Older v1 Hosts still serve status and their immutable Inbox remains
|
|
1581
|
+
// consumable. Neither idle nor absent eventDelivery proves a terminal.
|
|
1582
|
+
return { ...observed, reconciliation: "unsupported" };
|
|
1583
|
+
}
|
|
1584
|
+
return exchangeAgentHostControl(path, input.control, 1_000);
|
|
1543
1585
|
}
|
|
1544
1586
|
export async function waitForAgentHostLaunchAck(input) {
|
|
1545
1587
|
const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
|
|
@@ -1696,6 +1738,11 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
|
1696
1738
|
...result,
|
|
1697
1739
|
snapshot: {
|
|
1698
1740
|
...result.snapshot,
|
|
1741
|
+
hostImplementation: AGENT_HOST_IMPLEMENTATION,
|
|
1742
|
+
controlCapabilities: payload.providerControl === undefined ? ["status"] : [
|
|
1743
|
+
"status", "launch", "submit-turn", "steer-turn", "cancel", "set-authority",
|
|
1744
|
+
"native-open", "native-respond", "native-model", "reconcile-turn"
|
|
1745
|
+
],
|
|
1699
1746
|
...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
|
|
1700
1747
|
eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
|
|
1701
1748
|
})
|
|
@@ -1956,6 +2003,11 @@ function validateSnapshot(snapshot) {
|
|
|
1956
2003
|
&& snapshot.inputAcceptance !== "provider" && snapshot.inputAcceptance !== "transport") {
|
|
1957
2004
|
throw new Error("Agent Host input acceptance evidence is invalid.");
|
|
1958
2005
|
}
|
|
2006
|
+
if (snapshot.controlCapabilities !== undefined && (!Array.isArray(snapshot.controlCapabilities) || snapshot.controlCapabilities.length > 64
|
|
2007
|
+
|| snapshot.controlCapabilities.some(value => typeof value !== "string"
|
|
2008
|
+
|| value.length === 0 || value.length > 64 || value.includes("\0")))) {
|
|
2009
|
+
throw new Error("Agent Host control capability advertisement is invalid.");
|
|
2010
|
+
}
|
|
1959
2011
|
const authorityFields = [
|
|
1960
2012
|
snapshot.authorityEpoch,
|
|
1961
2013
|
snapshot.authorityOwner,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
5
|
+
// Capture once when this process loads its Host bridge. Never label a live
|
|
6
|
+
// process with the installation currently on disk at status-query time.
|
|
7
|
+
const release = detectRunningRelease(fileURLToPath(import.meta.url));
|
|
8
|
+
const modules = [
|
|
9
|
+
"agentHost.js", "agentHostProtocol.js", "agentHostImplementation.js",
|
|
10
|
+
"codexInteractiveHost.js", "../controller/structuredProviderObservation.js",
|
|
11
|
+
"../controller/runtimeEventInbox.js"
|
|
12
|
+
];
|
|
13
|
+
export const AGENT_HOST_IMPLEMENTATION = Object.freeze({
|
|
14
|
+
id: "yui.agent-host",
|
|
15
|
+
generation: release?.manifest.packageDigest ?? `checkout-${createHash("sha256")
|
|
16
|
+
.update(modules.map(module => `${module}\0${readFileSync(new URL(module, import.meta.url), "utf8")}`).join("\0"))
|
|
17
|
+
.digest("hex")}`
|
|
18
|
+
});
|
|
@@ -16,12 +16,31 @@ export function requireManagedGlobalCaller(store, env) {
|
|
|
16
16
|
|| !role || !binding || !set || !session || session.status !== "active"
|
|
17
17
|
|| set.activeAgentId !== role.activeAgentId || session.agentId !== binding.agentId
|
|
18
18
|
|| session.adapterId !== binding.adapterId || nativeSessionId !== session.nativeSessionId
|
|
19
|
+
|| (env.YUI_WORKSPACE !== undefined && env.YUI_WORKSPACE !== session.effective.workspace.root)
|
|
19
20
|
|| (env.YUI_AGENT_ID !== undefined && env.YUI_AGENT_ID !== session.agentId)
|
|
20
21
|
|| (env.YUI_ADAPTER_ID !== undefined && env.YUI_ADAPTER_ID !== session.adapterId)) {
|
|
21
22
|
throw new ManagedRuntimeDriftError("This command requires the current managed global native Session; the previous Session has no write authority.");
|
|
22
23
|
}
|
|
23
24
|
return { roleName: role.name, agentId: session.agentId, adapterId: session.adapterId, nativeSessionId };
|
|
24
25
|
}
|
|
26
|
+
/** Historical self diagnostics remain readable, but never authorize inbox
|
|
27
|
+
* handling, configuration, another Role's records, or lifecycle mutation. */
|
|
28
|
+
export function requireManagedGlobalReader(store, env) {
|
|
29
|
+
try {
|
|
30
|
+
requireManagedGlobalCaller(store, env);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const set = env.YUI_ROLE === undefined ? null : store.getGlobalRoleSessionSet(env.YUI_ROLE);
|
|
35
|
+
const known = [...Object.values(set?.sessions ?? {}), ...Object.values(set?.history ?? {})].some(session => session.nativeSessionId === identity(session.adapterId === "codex"
|
|
36
|
+
? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID)
|
|
37
|
+
&& (env.YUI_AGENT_ID === undefined || env.YUI_AGENT_ID === session.agentId)
|
|
38
|
+
&& (env.YUI_ADAPTER_ID === undefined || env.YUI_ADAPTER_ID === session.adapterId)
|
|
39
|
+
&& (env.YUI_WORKSPACE === undefined || env.YUI_WORKSPACE === session.effective.workspace.root));
|
|
40
|
+
if (env.YUI_SESSION_SCOPE !== "global" || env.YUI_TASK_ID !== undefined || !known)
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
45
|
* A managed Session presented valid self-identity but is no longer the current
|
|
27
46
|
* runtime of its Task Role. This is a bounded diagnosis rather than a bare
|
|
@@ -62,7 +81,12 @@ export function resolveManagedTaskCaller(store, environment) {
|
|
|
62
81
|
const self = managedTaskSessionIdentity(environment);
|
|
63
82
|
if (self === undefined)
|
|
64
83
|
return undefined;
|
|
65
|
-
|
|
84
|
+
const caller = requireCurrentRuntime(store, self);
|
|
85
|
+
if (environment?.YUI_AGENT_ID !== undefined && environment.YUI_AGENT_ID !== caller.agentId
|
|
86
|
+
|| environment?.YUI_ADAPTER_ID !== undefined && environment.YUI_ADAPTER_ID !== caller.adapterId) {
|
|
87
|
+
throw new ManagedRuntimeDriftError("Managed Task Agent/adapter hints conflict with the current native Session.");
|
|
88
|
+
}
|
|
89
|
+
return caller;
|
|
66
90
|
}
|
|
67
91
|
/** Same as resolveManagedTaskCaller, but requires a managed Task Session. */
|
|
68
92
|
export function requireManagedTaskCaller(store, environment) {
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* silently acquire the authority of an unbound public caller. */
|
|
3
3
|
export function hasManagedIdentity(environment) {
|
|
4
4
|
return ["YUI_SESSION_SCOPE", "YUI_ROLE", "YUI_AGENT_ID", "YUI_NATIVE_SESSION_ID",
|
|
5
|
-
"YUI_TASK_ID", "YUI_SESSION_MANIFEST"].some(key => environment[key] !== undefined);
|
|
5
|
+
"YUI_TASK_ID", "YUI_SESSION_MANIFEST", "CODEX_THREAD_ID"].some(key => environment[key] !== undefined);
|
|
6
6
|
}
|
|
@@ -176,7 +176,7 @@ export function providerRetryPrompt(retry, original) {
|
|
|
176
176
|
original
|
|
177
177
|
].join("\n");
|
|
178
178
|
}
|
|
179
|
-
/** The same presentation is consumed by
|
|
179
|
+
/** The same presentation is consumed by explicit detail reads and Web. */
|
|
180
180
|
export function providerRetryProjection(binding) {
|
|
181
181
|
const retry = binding?.retry;
|
|
182
182
|
if (retry === undefined)
|
|
@@ -209,6 +209,24 @@ export function providerRetryProjection(binding) {
|
|
|
209
209
|
...(retry.successorRunId === undefined ? {} : { successorRunId: retry.successorRunId })
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
/** Entry-point facts, never accumulated retry history or raw diagnostics.
|
|
213
|
+
* Exact Session retry/show and failureRef retain the original evidence. */
|
|
214
|
+
export function providerRetrySummary(binding) {
|
|
215
|
+
const view = providerRetryProjection(binding);
|
|
216
|
+
if (view === null)
|
|
217
|
+
return null;
|
|
218
|
+
const { history, error, currentTurn, ...facts } = view;
|
|
219
|
+
return {
|
|
220
|
+
...facts,
|
|
221
|
+
error: { source: error.source, phase: error.phase, category: error.category,
|
|
222
|
+
code: error.code, inputDisposition: error.inputDisposition,
|
|
223
|
+
sessionDisposition: error.sessionDisposition, retryable: error.retryable },
|
|
224
|
+
currentTurn: currentTurn === null ? null : {
|
|
225
|
+
attemptId: currentTurn.attemptId, status: currentTurn.status, nativeTurnId: currentTurn.nativeTurnId
|
|
226
|
+
},
|
|
227
|
+
historyCount: history.length, detailOmitted: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
212
230
|
export function controlProviderRetry(binding, action, at) {
|
|
213
231
|
if (action === "enable")
|
|
214
232
|
return { ...binding, retryDisabled: false };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { hasManagedIdentity } from "./managedIdentity.js";
|
|
2
|
+
import { ManagedRuntimeDriftError, requireManagedGlobalCaller, requireManagedTaskCaller } from "./managedCaller.js";
|
|
3
|
+
/** Resolve location from this Home's registry, not from a launch environment.
|
|
4
|
+
* The returned hints are not authority: consumers still validate the current
|
|
5
|
+
* binding (including revocation, Assignment and captured workspace).
|
|
6
|
+
* Native children with their own Codex thread cannot borrow a parent's hints.
|
|
7
|
+
*/
|
|
8
|
+
export function sessionCallerEnvironment(store, env) {
|
|
9
|
+
const native = env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID;
|
|
10
|
+
if (native === undefined) {
|
|
11
|
+
if (hasManagedIdentity(env))
|
|
12
|
+
throw new ManagedRuntimeDriftError("Managed Session identity has no native session id.");
|
|
13
|
+
return env;
|
|
14
|
+
}
|
|
15
|
+
if (!native || native.trim() !== native || native.includes("\0")) {
|
|
16
|
+
throw new ManagedRuntimeDriftError("Managed native Session identity is invalid.");
|
|
17
|
+
}
|
|
18
|
+
// Reuse the existing derived active-Session inventory; no new identity index
|
|
19
|
+
// or scan of every Task body/history is needed.
|
|
20
|
+
const matches = store.listRuntimeSessionCandidates().filter(session => session.nativeSessionId === (session.adapterId === "codex" ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID));
|
|
21
|
+
if (matches.length !== 1) {
|
|
22
|
+
throw new ManagedRuntimeDriftError(matches.length
|
|
23
|
+
? "Native Session identity is ambiguous in this YUI_HOME; no caller authority was selected."
|
|
24
|
+
: "Native Session is not registered in this YUI_HOME; refusing to infer public user authority.");
|
|
25
|
+
}
|
|
26
|
+
const match = matches[0];
|
|
27
|
+
const set = match.owner.scope === "global" ? store.getGlobalRoleSessionSet(match.owner.roleName)
|
|
28
|
+
: store.getTaskRoleSessionSet(match.owner.taskId, match.owner.roleName);
|
|
29
|
+
const session = set?.sessions[match.agentId];
|
|
30
|
+
if (set === null || session === undefined || session.nativeSessionId !== match.nativeSessionId) {
|
|
31
|
+
throw new ManagedRuntimeDriftError("Native Session registration changed during caller resolution; read current state again.");
|
|
32
|
+
}
|
|
33
|
+
const hints = {
|
|
34
|
+
YUI_SESSION_SCOPE: set.owner.scope, YUI_ROLE: set.owner.roleName,
|
|
35
|
+
YUI_AGENT_ID: session.agentId, YUI_ADAPTER_ID: session.adapterId,
|
|
36
|
+
YUI_WORKSPACE: session.effective.workspace.root,
|
|
37
|
+
...(set.owner.scope === "task" ? { YUI_TASK_ID: set.owner.taskId } : {})
|
|
38
|
+
};
|
|
39
|
+
for (const key of [...Object.keys(hints), "YUI_TASK_ID"]) {
|
|
40
|
+
if (env[key] !== undefined && env[key] !== hints[key]) {
|
|
41
|
+
throw new ManagedRuntimeDriftError(`Managed Session hint ${key} conflicts with this Home's native Session registration.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ...env, ...hints };
|
|
45
|
+
}
|
|
46
|
+
/** One current caller for inbox and other own-address operations. */
|
|
47
|
+
export function requireManagedCaller(store, env) {
|
|
48
|
+
const current = sessionCallerEnvironment(store, env);
|
|
49
|
+
if (current.YUI_SESSION_SCOPE === "global") {
|
|
50
|
+
return { scope: "global", ...requireManagedGlobalCaller(store, current) };
|
|
51
|
+
}
|
|
52
|
+
return { scope: "task", ...requireManagedTaskCaller(store, current) };
|
|
53
|
+
}
|
|
@@ -58,6 +58,34 @@ export function queryContextRecords(db, taskId, query) {
|
|
|
58
58
|
where.push(`${id} IN (SELECT value FROM json_each(@ids))`);
|
|
59
59
|
params.ids = JSON.stringify(query.ids);
|
|
60
60
|
}
|
|
61
|
+
if (query.unhandledMessages) {
|
|
62
|
+
if (query.family !== "task-message")
|
|
63
|
+
throw new Error("Unhandled selection requires Messages.");
|
|
64
|
+
where.push(`json_extract(r.payload, '$.inbox.ack') IS NULL`);
|
|
65
|
+
where.push(`NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id
|
|
66
|
+
AND e.type = 'task.record-retired'
|
|
67
|
+
AND json_extract(e.payload, '$.payload.recordKind') = 'message'
|
|
68
|
+
AND json_extract(e.payload, '$.payload.recordId') = r.message_id)`);
|
|
69
|
+
// Match taskMessageInboxReceipt: a direct claim owns the Message, otherwise
|
|
70
|
+
// a wake receipt covers its exact reference and latest revision event.
|
|
71
|
+
where.push(`(json_extract(r.payload, '$.inbox') IS NOT NULL OR (
|
|
72
|
+
SELECT json_extract(w.payload, '$.inbox.ack') FROM task_wakes w
|
|
73
|
+
WHERE w.task_id = r.task_id AND json_extract(w.payload, '$.inbox') IS NOT NULL
|
|
74
|
+
AND EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') ref
|
|
75
|
+
WHERE json_extract(ref.value, '$.type') = 'message'
|
|
76
|
+
AND json_extract(ref.value, '$.taskId') = r.task_id
|
|
77
|
+
AND json_extract(ref.value, '$.id') = r.message_id)
|
|
78
|
+
AND (NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
|
|
79
|
+
AND json_extract(e.payload, '$.payload.messageId') = r.message_id)
|
|
80
|
+
OR EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') update_ref
|
|
81
|
+
WHERE json_extract(update_ref.value, '$.type') = 'event'
|
|
82
|
+
AND json_extract(update_ref.value, '$.taskId') = r.task_id
|
|
83
|
+
AND json_extract(update_ref.value, '$.id') = (
|
|
84
|
+
SELECT e.event_id FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
|
|
85
|
+
AND json_extract(e.payload, '$.payload.messageId') = r.message_id
|
|
86
|
+
ORDER BY CAST(substr(e.event_id, 7) AS INTEGER) DESC LIMIT 1)))
|
|
87
|
+
ORDER BY w.seq ASC LIMIT 1) IS NULL)`);
|
|
88
|
+
}
|
|
61
89
|
for (const [label, statuses, not] of [
|
|
62
90
|
["statuses", query.statuses, ""],
|
|
63
91
|
["excludeStatuses", query.excludeStatuses, "NOT"]
|