@zq-silk/yui 2.5.0 → 2.7.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/README.md +23 -0
- package/dist/cli/commandCatalog.js +31 -4
- package/dist/commands/globalRoleCommands.js +22 -2
- package/dist/commands/inboxCommands.js +352 -0
- package/dist/commands/roleSkillValidation.js +2 -0
- package/dist/commands/taskCommands.js +40 -7
- package/dist/commands/taskContextCommand.js +3 -2
- package/dist/context/currentSkills.js +48 -0
- package/dist/context/roleSessionContext.js +13 -16
- package/dist/context/runContextPack.js +2 -0
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +27 -4
- package/dist/context/skillCatalog.js +2 -3
- package/dist/context/taskContext.js +9 -4
- package/dist/controlPlaneCli.js +18 -1
- package/dist/controller/controller.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
- package/dist/controller/globalInputDelivery.js +73 -15
- package/dist/controller/globalRuntimeAttention.js +1 -1
- package/dist/controller/operatorNotification.js +6 -1
- package/dist/controller/providerRetryDelivery.js +3 -5
- package/dist/controller/runtime.js +2 -2
- package/dist/doctor/doctor.js +25 -2
- package/dist/executor/effectiveLaunch.js +2 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -6
- package/dist/interaction/notificationPresentation.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +2 -1
- package/dist/message/globalInputBacklog.js +69 -0
- package/dist/message/globalMessageQuery.js +1 -0
- package/dist/message/inboxReceipt.js +32 -0
- package/dist/message/message.js +6 -1
- package/dist/message/messageContinuation.js +34 -13
- package/dist/message/taskInboxOwnership.js +21 -0
- package/dist/nativeAgent/session/backends.js +1 -1
- package/dist/observability/runtimeIdentity.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/runtime/agentHost.js +105 -16
- package/dist/runtime/builtinAgentErrorMappers.js +9 -5
- package/dist/runtime/providerRetry.js +38 -0
- package/dist/runtime/taskSessions.js +240 -0
- package/dist/scheduler/operatorEvent.js +49 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
- package/dist/scheduler/taskExecutionProjection.js +78 -6
- package/dist/scheduler/taskWake.js +5 -0
- package/dist/sqlite/driver.js +108 -0
- package/dist/storage/sqliteSchema.js +11 -1
- package/dist/storage/sqliteStore.js +60 -1
- package/dist/storage/storageSchema.js +1 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
- package/dist/task/completionReadiness.js +11 -4
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app/taskView.js +1 -0
- package/dist/web/assets/client/domain/records.js +70 -2
- package/dist/web/assets/client/domain/runs.js +3 -2
- package/dist/web/assets/client/domain/vocab.js +1 -1
- package/dist/web/assets/client/i18n/en.js +17 -1
- package/dist/web/assets/client/i18n/zh-CN.js +17 -1
- package/dist/web/assets/client/lib/api.js +4 -1
- package/dist/web/assets/client/lib/theme.js +31 -10
- package/dist/web/assets/client/views/task/actions.js +38 -21
- package/dist/web/assets/client/views/task/observation.js +12 -3
- package/dist/web/assets/client/views/task/overview.js +7 -40
- package/dist/web/assets/client/views/task/runtime.js +71 -34
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shell/index.js +1 -0
- package/dist/web/assets/shell/settings.js +1 -0
- package/dist/web/assets/styles/views/task.js +14 -0
- package/dist/web/webServer.js +12 -0
- package/dist/web/webSessions.js +2 -165
- package/dist/web/webSnapshot.js +9 -5
- package/dist/web/webTaskActivity.js +54 -0
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/managed-turn-and-session-runtime.md +81 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
- package/docs/observability/README.md +25 -1
- package/docs/observability/README.zh-CN.md +18 -1
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +74 -2
- package/docs/release-workflow.zh-CN.md +56 -1
- package/docs/storage-baseline.md +17 -7
- package/docs/storage-baseline.zh-CN.md +11 -5
- package/docs/task-delivery.md +17 -8
- package/docs/task-delivery.zh-CN.md +11 -5
- package/docs/testing/verification-levels.md +22 -4
- package/docs/testing/verification-levels.zh-CN.md +8 -1
- package/i18n/README.zh-CN.md +19 -0
- package/package.json +5 -2
- package/skills/yui-leader/SKILL.md +28 -11
- package/skills/yui-leader/VERSION +1 -0
- package/skills/yui-operator/SKILL.md +10 -2
- package/skills/yui-operator/VERSION +1 -0
- package/skills/yui-operator/references/task-delivery.md +2 -0
- package/skills/yui-reviewer/SKILL.md +8 -1
- package/skills/yui-reviewer/VERSION +1 -0
- package/skills/yui-runtime/SKILL.md +94 -11
- package/skills/yui-runtime/VERSION +1 -0
- package/skills/yui-runtime/references/publication.md +16 -5
- package/skills/yui-runtime/references/recovery.md +9 -0
- package/skills/yui-worker/SKILL.md +8 -1
- package/skills/yui-worker/VERSION +1 -0
|
@@ -28,8 +28,9 @@ export async function processOperatorInputNotifications(store, selection, now =
|
|
|
28
28
|
}
|
|
29
29
|
if (ref.type !== "event")
|
|
30
30
|
return [];
|
|
31
|
-
const event = store.
|
|
32
|
-
|
|
31
|
+
const event = store.getEvent !== undefined ? store.getEvent(ref.taskId, ref.id)
|
|
32
|
+
: store.listEvents(ref.taskId).find(({ id }) => id === ref.id);
|
|
33
|
+
return event == null ? [] : [{ kind: "event", event }];
|
|
33
34
|
}));
|
|
34
35
|
if (attentions.length === 0) {
|
|
35
36
|
store.completeWorkMailbox(targetMailbox, processing.batchId);
|
|
@@ -42,9 +43,9 @@ export async function processOperatorInputNotifications(store, selection, now =
|
|
|
42
43
|
receiptId: presentation.receiptId,
|
|
43
44
|
text: presentation.text
|
|
44
45
|
}, now);
|
|
45
|
-
if (outcome.status === "unavailable") {
|
|
46
|
+
if (outcome.status === "unavailable" || outcome.status === "deferred") {
|
|
46
47
|
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
47
|
-
return attentions.map((attention) => skipped(attention, "operator-unavailable"));
|
|
48
|
+
return attentions.map((attention) => skipped(attention, outcome.status === "deferred" ? "notice-pending" : "operator-unavailable"));
|
|
48
49
|
}
|
|
49
50
|
return attentions.map((attention) => ({
|
|
50
51
|
...attentionIdentity(attention),
|
|
@@ -5,6 +5,7 @@ import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
|
5
5
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
6
6
|
import { actionableExecutionLaneRecoveries, summarizeExecutionGroupHealth } from "../execution/executionHealth.js";
|
|
7
7
|
import { buildTaskObservabilityProjection } from "./taskObservabilityProjection.js";
|
|
8
|
+
import { projectTaskSessions } from "../runtime/taskSessions.js";
|
|
8
9
|
import { isRoleRunStalled, latestStallProgressAt } from "./roleRunStall.js";
|
|
9
10
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
10
11
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
@@ -13,7 +14,7 @@ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runt
|
|
|
13
14
|
* aggregate. This function only reads; it never starts a Controller, queues a
|
|
14
15
|
* wake, writes a Message, or mutates any record.
|
|
15
16
|
*/
|
|
16
|
-
export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
17
|
+
export function buildTaskExecutionProjection(store, taskId, now = new Date(), nativeSessions) {
|
|
17
18
|
const task = store.getTask(taskId);
|
|
18
19
|
if (task === null)
|
|
19
20
|
return null;
|
|
@@ -23,6 +24,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
23
24
|
const workItems = store.listWorkItems(taskId);
|
|
24
25
|
const reviewRounds = store.listReviewRounds(taskId);
|
|
25
26
|
const runs = operationalTaskRecords(usageRuns, events, "run");
|
|
27
|
+
const brief = store.getTaskBrief(taskId);
|
|
26
28
|
const leaderMailbox = store.getWorkMailbox({
|
|
27
29
|
kind: "role",
|
|
28
30
|
taskId,
|
|
@@ -36,6 +38,12 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
36
38
|
});
|
|
37
39
|
return projectTaskExecution({
|
|
38
40
|
task,
|
|
41
|
+
jobs: store.listDurableJobs(taskId),
|
|
42
|
+
nativeSessions: nativeSessions ?? projectTaskSessions({
|
|
43
|
+
taskId, sessionSets: store.listRoleSessionSets(taskId), events, now,
|
|
44
|
+
...(brief?.updatedAt === undefined ? {} : { semanticProgressAt: brief.updatedAt }),
|
|
45
|
+
policy: resolveRuntimeHealth(store.getConfig().runtimeHealth)
|
|
46
|
+
}),
|
|
39
47
|
usageFacts: store.readTaskUsageFacts(taskId),
|
|
40
48
|
roles,
|
|
41
49
|
runs,
|
|
@@ -56,7 +64,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
56
64
|
changeSets: store.listChangeSets(taskId),
|
|
57
65
|
integrations: store.listIntegrationAttempts(taskId),
|
|
58
66
|
events,
|
|
59
|
-
brief
|
|
67
|
+
brief,
|
|
60
68
|
pendingWakeup: store.getPendingWakeup(taskId),
|
|
61
69
|
leaderMailbox,
|
|
62
70
|
leaderFailure: store.getLeaderFailure(taskId),
|
|
@@ -118,8 +126,27 @@ export function projectTaskExecution(facts) {
|
|
|
118
126
|
now
|
|
119
127
|
});
|
|
120
128
|
const laneRecovery = actionableExecutionLaneRecoveries(groupSummaries)[0];
|
|
129
|
+
const jobs = facts.jobs ?? [];
|
|
130
|
+
const native = facts.nativeSessions;
|
|
131
|
+
const activity = {
|
|
132
|
+
jobs: {
|
|
133
|
+
queued: jobs.filter(job => job.status === "queued").length,
|
|
134
|
+
running: jobs.filter(job => job.status === "running").length,
|
|
135
|
+
unknown: jobs.filter(job => job.status === "unknown-needs-attention").length
|
|
136
|
+
},
|
|
137
|
+
openInputs: inputRequests.filter(input => input.status === "open").length,
|
|
138
|
+
native: {
|
|
139
|
+
active: (native?.counts.active ?? 0) + (native?.counts.quiet ?? 0),
|
|
140
|
+
waiting: native?.counts.waiting ?? 0,
|
|
141
|
+
unknown: (native?.counts.unknown ?? 0) + (native?.counts.diagnostic ?? 0)
|
|
142
|
+
+ (native?.sessions.reduce((sum, session) => sum
|
|
143
|
+
+ session.background.filter(child => child.execution === "unknown").length, 0) ?? 0),
|
|
144
|
+
background: native?.sessions.reduce((sum, session) => sum + session.background.length, 0) ?? 0
|
|
145
|
+
}
|
|
146
|
+
};
|
|
121
147
|
const render = (input) => projection({
|
|
122
148
|
...input,
|
|
149
|
+
activity,
|
|
123
150
|
executionGroups: groupSummaries,
|
|
124
151
|
observability
|
|
125
152
|
});
|
|
@@ -128,10 +155,12 @@ export function projectTaskExecution(facts) {
|
|
|
128
155
|
const known = facts.runDelivery?.[run.id];
|
|
129
156
|
if (known !== undefined && known !== "unobserved")
|
|
130
157
|
return known;
|
|
131
|
-
const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.
|
|
158
|
+
const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.fence.taskId === task.id
|
|
159
|
+
&& event.fence.roleName === run.roleName && event.fence.agentId === run.effective.agentId
|
|
160
|
+
&& event.authority !== "transport"
|
|
132
161
|
&& ["turn.accepted", "turn.completed", "turn.failed", "turn.cancelled"].includes(event.kind)).at(-1);
|
|
133
162
|
return observed === undefined || observed === null ? "unobserved"
|
|
134
|
-
: observed.kind.slice("
|
|
163
|
+
: observed.kind.slice("turn.".length);
|
|
135
164
|
};
|
|
136
165
|
const runHasSession = (run) => roleSessions.some((session) => (session.roleName === run.roleName
|
|
137
166
|
&& session.agentId === run.effective.agentId
|
|
@@ -206,7 +235,7 @@ export function projectTaskExecution(facts) {
|
|
|
206
235
|
const activeLeader = activeRuns.find((run) => run.roleName === "leader");
|
|
207
236
|
const pendingDeliveryRuns = activeRuns.filter((run) => deliveryOf(run) !== "accepted");
|
|
208
237
|
const hasPendingLeaderWork = pendingWakeup !== null
|
|
209
|
-
|| leaderMailbox?.pending
|
|
238
|
+
|| leaderMailbox?.pending != null;
|
|
210
239
|
const recoveryPending = isRecoveryPending(pendingWakeup, leaderMailbox, leaderFailure);
|
|
211
240
|
const providerRecoveries = (facts.providerRetries ?? []).filter(retry => retry.status === "waiting" || retry.status === "in-flight");
|
|
212
241
|
const recoveringRunIds = new Set(providerRecoveries.flatMap(retry => [retry.previousRunId, retry.successorRunId].filter((id) => id !== undefined)));
|
|
@@ -257,13 +286,30 @@ export function projectTaskExecution(facts) {
|
|
|
257
286
|
summary: openInputs[0].question,
|
|
258
287
|
reason: "open-input-request",
|
|
259
288
|
monitoring,
|
|
260
|
-
failClosed:
|
|
289
|
+
failClosed: activity.jobs.unknown > 0 || activity.native.unknown > 0,
|
|
261
290
|
activeRuns: activeRunViews,
|
|
262
291
|
attention,
|
|
263
292
|
blockers,
|
|
264
293
|
pendingWakeup
|
|
265
294
|
});
|
|
266
295
|
}
|
|
296
|
+
if (activity.jobs.unknown > 0 || activity.native.unknown > 0) {
|
|
297
|
+
return render({
|
|
298
|
+
task, status: "attention", owner: "leader", action: "inspect-attention",
|
|
299
|
+
summary: `${activity.jobs.unknown} Job(s) have unknown outcomes; ${activity.native.unknown} native observation(s) need attention. Other parallel activity remains visible; do not infer completion or replay.`,
|
|
300
|
+
reason: "parallel-execution-unknown", monitoring, failClosed: true,
|
|
301
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const nativeWait = native?.sessions.find(session => session.group === "waiting" && ["user", "permission"].includes(session.waitingReason ?? ""));
|
|
305
|
+
if (nativeWait) {
|
|
306
|
+
return render({
|
|
307
|
+
task, status: "waiting-user", owner: "user", action: "inspect-attention",
|
|
308
|
+
summary: `Native Session ${nativeWait.roleName} is waiting for ${nativeWait.waitingReason}; other activity remains visible.`,
|
|
309
|
+
reason: "native-input-wait", monitoring, failClosed: false,
|
|
310
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
311
|
+
});
|
|
312
|
+
}
|
|
267
313
|
if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
|
|
268
314
|
return render({
|
|
269
315
|
task,
|
|
@@ -382,6 +428,15 @@ export function projectTaskExecution(facts) {
|
|
|
382
428
|
pendingWakeup
|
|
383
429
|
});
|
|
384
430
|
}
|
|
431
|
+
if (native?.sessions.some(session => session.roleName === "leader"
|
|
432
|
+
&& ["active", "quiet"].includes(session.group))) {
|
|
433
|
+
return render({
|
|
434
|
+
task, status: "working", owner: "leader", action: "advance-task",
|
|
435
|
+
summary: "Leader native input is accepted; managed AgentRuns and parallel background work remain separate facts.",
|
|
436
|
+
reason: "leader-native-turn", monitoring, failClosed: false,
|
|
437
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
438
|
+
});
|
|
439
|
+
}
|
|
385
440
|
if (hasPendingLeaderWork) {
|
|
386
441
|
return render({
|
|
387
442
|
task,
|
|
@@ -449,6 +504,22 @@ export function projectTaskExecution(facts) {
|
|
|
449
504
|
pendingWakeup
|
|
450
505
|
});
|
|
451
506
|
}
|
|
507
|
+
if (activity.jobs.queued + activity.jobs.running > 0) {
|
|
508
|
+
return render({
|
|
509
|
+
task, status: "waiting-on-jobs", owner: "none", action: "wait-for-jobs",
|
|
510
|
+
summary: `${activity.jobs.running} background Job(s) running; ${activity.jobs.queued} queued. Leader is not currently executing.`,
|
|
511
|
+
reason: "background-jobs", monitoring, failClosed: false,
|
|
512
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (activity.native.active + activity.native.waiting + activity.native.background > 0) {
|
|
516
|
+
return render({
|
|
517
|
+
task, status: "waiting-on-agents", owner: "none", action: "wait-for-agents",
|
|
518
|
+
summary: "Recorded native execution or retained background activity remains unsettled; this is separate from managed AgentRuns.",
|
|
519
|
+
reason: "native-execution", monitoring, failClosed: false,
|
|
520
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
521
|
+
});
|
|
522
|
+
}
|
|
452
523
|
if (candidateReady || unresolvedIntegration) {
|
|
453
524
|
return render({
|
|
454
525
|
task,
|
|
@@ -496,6 +567,7 @@ function projection(input) {
|
|
|
496
567
|
monitoring: input.monitoring,
|
|
497
568
|
failClosed: input.failClosed,
|
|
498
569
|
activeRuns: input.activeRuns,
|
|
570
|
+
activity: input.activity,
|
|
499
571
|
executionGroups: input.executionGroups ?? [],
|
|
500
572
|
observability: input.observability,
|
|
501
573
|
attention: input.attention,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
2
|
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
3
3
|
import { copyRef } from "../coordination/workMailbox.js";
|
|
4
|
+
import { validateInboxReceipt } from "../message/inboxReceipt.js";
|
|
4
5
|
export const CURRENT_TASK_WAKE_SCHEMA_VERSION = 1;
|
|
5
6
|
export function createTaskWake(input) {
|
|
6
7
|
if (Object.hasOwn(input, "runId"))
|
|
@@ -21,6 +22,10 @@ export function createTaskWake(input) {
|
|
|
21
22
|
return wake;
|
|
22
23
|
}
|
|
23
24
|
export function validateTaskWake(wake) {
|
|
25
|
+
validateInboxReceipt(wake.inbox);
|
|
26
|
+
if (wake.inboxPendingId !== undefined && (!/^pending-[a-f0-9]{64}$/.test(wake.inboxPendingId) || !wake.inbox)) {
|
|
27
|
+
throw new Error("An active inbox wake requires its exact pending identity and handling claim.");
|
|
28
|
+
}
|
|
24
29
|
if (Object.hasOwn(wake, "runId"))
|
|
25
30
|
throw new Error("Run-backed wakes are not current notification records.");
|
|
26
31
|
if (wake.refs !== undefined) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
export const SQLITE_CANDIDATES = Object.freeze([
|
|
3
|
+
// N-API avoids node::ObjectWrap's GC cleanup abort on Node 24.19/24.20.
|
|
4
|
+
Object.freeze({ name: "sqlite-modern", version: "13.0.3", minimumNapi: 10 }),
|
|
5
|
+
Object.freeze({ name: "sqlite-compat", version: "10.1.0", minimumNapi: 0 })
|
|
6
|
+
]);
|
|
7
|
+
/** Only errors attributable to loading this native candidate, never database errors. */
|
|
8
|
+
function nativeUnavailable(error, candidate) {
|
|
9
|
+
if (!(error instanceof Error))
|
|
10
|
+
return false;
|
|
11
|
+
const code = error.code;
|
|
12
|
+
const firstLine = error.message.split("\n")[0];
|
|
13
|
+
if (code === "MODULE_NOT_FOUND") {
|
|
14
|
+
if (firstLine === `Cannot find module '${candidate.name}'`)
|
|
15
|
+
return true;
|
|
16
|
+
// v13 loads its bundled prebuild or these node-gyp paths directly.
|
|
17
|
+
// Do not swallow an unrelated missing transitive JS dependency.
|
|
18
|
+
const missing = /^Cannot find module '([^']+)'$/.exec(firstLine)?.[1];
|
|
19
|
+
return missing !== undefined && ["Debug", "Release"].some((mode) => missing.endsWith(`/node_modules/${candidate.name}/build/${mode}/better_sqlite3.node`));
|
|
20
|
+
}
|
|
21
|
+
const tries = error.tries;
|
|
22
|
+
if (error.message.startsWith("Could not locate the bindings file.") && Array.isArray(tries)) {
|
|
23
|
+
return tries.length > 0 && tries.every((path) => typeof path === "string" && path.includes(`/node_modules/${candidate.name}/`)
|
|
24
|
+
&& path.endsWith("/better_sqlite3.node"));
|
|
25
|
+
}
|
|
26
|
+
if (code !== "ERR_DLOPEN_FAILED")
|
|
27
|
+
return false;
|
|
28
|
+
return /NODE_MODULE_VERSION \d+/.test(error.message)
|
|
29
|
+
|| /(?:GLIBC|GLIBCXX|CXXABI)_[\d.]+['’] not found/.test(error.message)
|
|
30
|
+
|| /(?:cannot open shared object file|Library not loaded:|wrong ELF class:|invalid ELF header|incompatible architecture)/.test(error.message);
|
|
31
|
+
}
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
function selectionFailure(failures) {
|
|
34
|
+
return new AggregateError(failures, "No usable SQLite driver. " + SQLITE_CANDIDATES.map((candidate, index) => `${candidate.name}@${candidate.version}: ${failures[index] instanceof Error ? failures[index].message : String(failures[index])}`).join("; ")
|
|
35
|
+
+ ". Reinstall with npm optional dependencies enabled (do not use --omit=optional); "
|
|
36
|
+
+ "check Node ABI, system libraries and native build tools. Older systems may require an OS/toolchain upgrade.");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Selection is isolated from every real database open. No real Home operation
|
|
40
|
+
* is inside a fallback catch, and successful selection stays fixed per process.
|
|
41
|
+
* Loader injection supports deterministic native-failure tests, not user config.
|
|
42
|
+
*/
|
|
43
|
+
export function createSqliteDriverSelector(load = (candidate) => require(candidate.name), napiVersion = Number(process.versions.napi ?? 0)) {
|
|
44
|
+
let selected;
|
|
45
|
+
return () => {
|
|
46
|
+
if (selected)
|
|
47
|
+
return selected;
|
|
48
|
+
const failures = [];
|
|
49
|
+
for (const candidate of SQLITE_CANDIDATES) {
|
|
50
|
+
// An unsupported Node-API constructor can crash outside JS exception
|
|
51
|
+
// handling. Exclude it before loading native code, not after probing.
|
|
52
|
+
if (napiVersion < candidate.minimumNapi) {
|
|
53
|
+
failures.push(new Error(`${candidate.name} requires Node-API ${candidate.minimumNapi}; runtime provides ${napiVersion}`));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let Driver;
|
|
57
|
+
let probe;
|
|
58
|
+
try {
|
|
59
|
+
Driver = load(candidate);
|
|
60
|
+
probe = new Driver(":memory:");
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!nativeUnavailable(error, candidate)) {
|
|
64
|
+
// Stop on unknown errors, but retain the modern failure when the
|
|
65
|
+
// compatibility candidate also fails. This does not permit retry.
|
|
66
|
+
if (failures.length)
|
|
67
|
+
throw selectionFailure([...failures, error]);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
failures.push(error);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// SQL and close failures are deliberately outside the native-load catch.
|
|
74
|
+
let sqliteVersion;
|
|
75
|
+
try {
|
|
76
|
+
try {
|
|
77
|
+
const result = probe.prepare("SELECT sqlite_version() AS version, 1 AS value").get();
|
|
78
|
+
if (typeof result?.version !== "string" || result.value !== 1) {
|
|
79
|
+
throw new Error(`SQLite probe returned an invalid result for ${candidate.name}.`);
|
|
80
|
+
}
|
|
81
|
+
sqliteVersion = result.version;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
probe.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (failures.length)
|
|
89
|
+
throw selectionFailure([...failures, error]);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
selected = Object.freeze({
|
|
93
|
+
...candidate, Database: Driver, sqliteVersion,
|
|
94
|
+
...(failures.length ? { fallbackReason: failures[0].message } : {})
|
|
95
|
+
});
|
|
96
|
+
return selected;
|
|
97
|
+
}
|
|
98
|
+
throw selectionFailure(failures);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export const selectSqliteDriver = createSqliteDriverSelector();
|
|
102
|
+
// Preserve the existing constructor/type contract without loading native code
|
|
103
|
+
// during module import. All runtime consumers share the same selector.
|
|
104
|
+
const Database = function (filename, options) {
|
|
105
|
+
const Driver = selectSqliteDriver().Database;
|
|
106
|
+
return new Driver(filename, options);
|
|
107
|
+
};
|
|
108
|
+
export default Database;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import Database from "
|
|
2
|
+
import Database from "../sqlite/driver.js";
|
|
3
3
|
import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES } from "./baselineSchema.js";
|
|
4
4
|
import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.js";
|
|
5
5
|
import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
|
|
@@ -76,6 +76,16 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
76
76
|
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
77
77
|
// Optional sourced monetary observations; historical missing costs stay unknown.
|
|
78
78
|
sql: ""
|
|
79
|
+
}, {
|
|
80
|
+
fromVersion: "1.6",
|
|
81
|
+
toVersion: "1.7",
|
|
82
|
+
name: "agent-inbox-consumption",
|
|
83
|
+
introducedIn: "next",
|
|
84
|
+
sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
85
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
86
|
+
// Additive handling receipts on canonical Messages and wakes. Never invent
|
|
87
|
+
// historical processing or Provider acceptance; existing records stay intact.
|
|
88
|
+
sql: ""
|
|
79
89
|
}]);
|
|
80
90
|
export function storageMinorUpgradePlan(from) {
|
|
81
91
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* including writes, reads and explicit Home diagnostics. There is no weaker
|
|
33
33
|
* in-process contract or deferred validation phase.
|
|
34
34
|
*/
|
|
35
|
-
import Database from "
|
|
35
|
+
import Database from "../sqlite/driver.js";
|
|
36
36
|
import { existsSync, mkdirSync } from "node:fs";
|
|
37
37
|
import { join } from "node:path";
|
|
38
38
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -40,8 +40,10 @@ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
|
40
40
|
import { validateTaskBrief } from "../brief/taskBrief.js";
|
|
41
41
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
42
42
|
import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
43
|
+
import { enqueueDurableOperatorFact } from "../scheduler/operatorEvent.js";
|
|
43
44
|
import { validateDurableJob, validDurableJobTransition } from "../job/durableJob.js";
|
|
44
45
|
import { validateGlobalRoleMessage } from "../message/message.js";
|
|
46
|
+
import { assertInboxReceiptPreserved } from "../message/inboxReceipt.js";
|
|
45
47
|
import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
|
|
46
48
|
import { validatePluginValidation } from "../plugins/pluginPackage.js";
|
|
47
49
|
import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
|
|
@@ -975,10 +977,58 @@ export class SqliteTaskStore {
|
|
|
975
977
|
listGlobalRoleMessages(roleName) {
|
|
976
978
|
return this.#sortById(this.#listPayload("global_role_messages", "name = ?", [roleName]), (message) => message.id);
|
|
977
979
|
}
|
|
980
|
+
queryGlobalRoleMessages(roleName, query) {
|
|
981
|
+
if (!Number.isSafeInteger(query.limit) || query.limit < 0 || query.limit > 64
|
|
982
|
+
|| !Number.isSafeInteger(query.afterSequence ?? 0) || (query.afterSequence ?? 0) < 0) {
|
|
983
|
+
throw new Error("Global Message query exceeds its bounded contract.");
|
|
984
|
+
}
|
|
985
|
+
const where = ["name = @name"];
|
|
986
|
+
const params = { name: roleName };
|
|
987
|
+
if (query.handling)
|
|
988
|
+
where.push(`json_extract(payload, '$.inbox.claim') IS NOT NULL
|
|
989
|
+
AND json_extract(payload, '$.inbox.ack') IS NULL`);
|
|
990
|
+
if (query.pending)
|
|
991
|
+
where.push(`json_extract(payload, '$.delivery') IS NULL
|
|
992
|
+
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
993
|
+
AND json_extract(payload, '$.notDelivered') IS NULL
|
|
994
|
+
AND (json_extract(payload, '$.inputControl.action') = 'queue'
|
|
995
|
+
OR json_extract(payload, '$.interruptThen') IS NOT NULL)`);
|
|
996
|
+
if (query.interruptThen)
|
|
997
|
+
where.push("json_extract(payload, '$.interruptThen') IS NOT NULL");
|
|
998
|
+
if (query.operatorNotice)
|
|
999
|
+
where.push("json_extract(payload, '$.inputControl.requestId') LIKE 'operator-notice:%'");
|
|
1000
|
+
if (query.afterSequence !== undefined) {
|
|
1001
|
+
where.push("seq > @after");
|
|
1002
|
+
params.after = query.afterSequence;
|
|
1003
|
+
}
|
|
1004
|
+
if (query.id !== undefined) {
|
|
1005
|
+
where.push("message_id = @id");
|
|
1006
|
+
params.id = query.id;
|
|
1007
|
+
}
|
|
1008
|
+
if (query.requestId !== undefined) {
|
|
1009
|
+
where.push("COALESCE(json_extract(payload, '$.inputControl.requestId'), json_extract(payload, '$.interruptThen.reusedInput.requestId')) = @request");
|
|
1010
|
+
params.request = query.requestId;
|
|
1011
|
+
}
|
|
1012
|
+
const filter = where.join(" AND ");
|
|
1013
|
+
const summary = this.#db.prepare(`SELECT count(*) AS count, min(created_at) AS oldestCreatedAt
|
|
1014
|
+
FROM global_role_messages WHERE ${filter}`).get(params);
|
|
1015
|
+
const rows = this.#db.prepare(`SELECT payload FROM global_role_messages WHERE ${filter}
|
|
1016
|
+
ORDER BY seq ${query.descending ? "DESC" : "ASC"} LIMIT @limit`)
|
|
1017
|
+
.all({ ...params, limit: query.limit });
|
|
1018
|
+
return { ...summary, messages: rows.map(row => {
|
|
1019
|
+
const message = this.#parse(row.payload);
|
|
1020
|
+
validateGlobalRoleMessage(message);
|
|
1021
|
+
return message;
|
|
1022
|
+
}) };
|
|
1023
|
+
}
|
|
978
1024
|
updateGlobalRoleMessage(message) {
|
|
979
1025
|
validateGlobalRoleMessage(message);
|
|
980
1026
|
this.#requireGlobalRole(message.roleName);
|
|
981
1027
|
this.#mutate(() => {
|
|
1028
|
+
const existing = this.#db.prepare("SELECT payload FROM global_role_messages WHERE name = ? AND message_id = ?")
|
|
1029
|
+
.get(message.roleName, message.id);
|
|
1030
|
+
assertInboxReceiptPreserved(existing === undefined ? undefined
|
|
1031
|
+
: this.#parse(existing.payload).inbox, message.inbox);
|
|
982
1032
|
const seq = this.#globalMessageSequence(message.id);
|
|
983
1033
|
const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#recordJson("global_role_messages", message), message.roleName, message.id);
|
|
984
1034
|
if (result.changes === 0) {
|
|
@@ -2001,6 +2051,10 @@ export class SqliteTaskStore {
|
|
|
2001
2051
|
}
|
|
2002
2052
|
this.#requireTask(taskId);
|
|
2003
2053
|
this.#mutate(() => {
|
|
2054
|
+
const existing = this.#db.prepare("SELECT payload FROM messages WHERE task_id = ? AND message_id = ?")
|
|
2055
|
+
.get(taskId, message.id);
|
|
2056
|
+
assertInboxReceiptPreserved(existing === undefined ? undefined
|
|
2057
|
+
: this.#parse(existing.payload).inbox, message.inbox);
|
|
2004
2058
|
const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#recordJson("messages", message), taskId, message.id);
|
|
2005
2059
|
if (result.changes !== 1) {
|
|
2006
2060
|
throw new StorageRecordError(`Message does not exist: ${taskId}/${message.id}`);
|
|
@@ -2086,6 +2140,7 @@ export class SqliteTaskStore {
|
|
|
2086
2140
|
// Events are terminal/semantic: retained individually, never pruned (§9).
|
|
2087
2141
|
this.#db.prepare(`INSERT INTO events (task_id, event_id, type, occurred_at, payload) VALUES (?, ?, ?, ?, ?)`).run(taskId, event.id, event.type, event.createdAt, this.#recordJson("events", event));
|
|
2088
2142
|
this.#observeHighWater(taskId, "event", seq);
|
|
2143
|
+
enqueueDurableOperatorFact(this, event);
|
|
2089
2144
|
});
|
|
2090
2145
|
}
|
|
2091
2146
|
listEvents(taskId) {
|
|
@@ -2113,6 +2168,10 @@ export class SqliteTaskStore {
|
|
|
2113
2168
|
validateTaskWake(wake);
|
|
2114
2169
|
this.#requireTask(taskId);
|
|
2115
2170
|
this.#mutate(() => {
|
|
2171
|
+
const existing = this.#db.prepare("SELECT payload FROM task_wakes WHERE task_id = ? AND wake_id = ?")
|
|
2172
|
+
.get(taskId, wake.id);
|
|
2173
|
+
assertInboxReceiptPreserved(existing === undefined ? undefined
|
|
2174
|
+
: this.#parse(existing.payload).inbox, wake.inbox);
|
|
2116
2175
|
const seq = this.#idSequence(wake.id, "taskWake");
|
|
2117
2176
|
this.#db.prepare(`INSERT INTO task_wakes
|
|
2118
2177
|
(task_id, wake_id, seq, status, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import Database from "
|
|
3
|
+
import Database from "../sqlite/driver.js";
|
|
4
4
|
import { inspectSqliteSchema, storageMinorUpgradePlan, SqliteSchemaError } from "./sqliteSchema.js";
|
|
5
5
|
import { compareStorageVersions, CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
|
|
6
6
|
export { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, rmdirSync, rmSync } from "node:fs";
|
|
3
|
-
import Database from "
|
|
3
|
+
import Database from "../../sqlite/driver.js";
|
|
4
4
|
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
|
|
5
5
|
import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
|
|
6
6
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { governingWorkItemDeliveries, integrationAttemptRequiresSettlement, workItemDeliverySettled } from "../integration/deliveryObligation.js";
|
|
2
|
+
import { isLeaderSteer, taskMessageInboxReceipt } from "../message/taskInboxOwnership.js";
|
|
2
3
|
import { operationalTaskRecords } from "./taskRecordRetirement.js";
|
|
3
|
-
/** Mailbox refs locate original durable user intent.
|
|
4
|
-
*
|
|
5
|
-
* notification delivery present the new input before declaring completion. */
|
|
4
|
+
/** Mailbox refs locate original durable user intent. Active inbox handling
|
|
5
|
+
* retains the same completion fence until the exact window is acknowledged. */
|
|
6
6
|
export function pendingCompletionMessages(store, taskId) {
|
|
7
7
|
const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" });
|
|
8
8
|
const refs = [...(mailbox?.pending?.refs ?? [])];
|
|
9
|
+
for (const wake of store.listTaskWakes(taskId)) {
|
|
10
|
+
if (wake.inbox !== undefined && wake.inbox.ack === undefined)
|
|
11
|
+
refs.push(...(wake.refs ?? []));
|
|
12
|
+
}
|
|
9
13
|
const processing = mailbox?.processing;
|
|
14
|
+
if (processing?.owner.startsWith("agent-inbox:"))
|
|
15
|
+
refs.push(...processing.batch.refs);
|
|
10
16
|
if (processing?.owner.startsWith("leader-notification:")) {
|
|
11
17
|
const native = store.getTaskRoleSessionSet(taskId, "leader")?.providerBinding?.run;
|
|
12
18
|
if (native?.attemptId !== processing.batchId
|
|
@@ -16,7 +22,8 @@ export function pendingCompletionMessages(store, taskId) {
|
|
|
16
22
|
}
|
|
17
23
|
const ids = new Set(refs.filter(ref => ref.type === "message" && ref.taskId === taskId).map(ref => ref.id));
|
|
18
24
|
return operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message")
|
|
19
|
-
.filter(message => ids.has(message.id)
|
|
25
|
+
.filter(message => (ids.has(message.id) || isLeaderSteer(message) && message.inbox !== undefined)
|
|
26
|
+
&& taskMessageInboxReceipt(store, message)?.ack === undefined
|
|
20
27
|
&& (message.kind === "user" || message.kind === "operator"));
|
|
21
28
|
}
|
|
22
29
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
|
|
7
|
+
import { THEME_BOOTSTRAP_SCRIPT } from "./client/lib/theme.js";
|
|
7
8
|
import { FONT_WOFF2_BASE64 } from "./fontData.js";
|
|
8
9
|
import { DASHBOARD_HTML } from "./shell/index.js";
|
|
9
10
|
import { STYLESHEETS } from "./styles/index.js";
|
|
@@ -13,6 +14,7 @@ const JS = "text/javascript; charset=utf-8";
|
|
|
13
14
|
export const WEB_ASSETS = Object.freeze({
|
|
14
15
|
...Object.fromEntries(STYLESHEETS.map(({ name, body }) => [`/assets/css/${name}.css`, { contentType: CSS, body }])),
|
|
15
16
|
...Object.fromEntries(Object.entries(CLIENT_MODULES).map(([path, body]) => [`/assets/js/${path}.js`, { contentType: JS, body }])),
|
|
17
|
+
"/assets/theme.js": { contentType: JS, body: THEME_BOOTSTRAP_SCRIPT },
|
|
16
18
|
"/assets/app.js": { contentType: JS, body: APP_SCRIPT },
|
|
17
19
|
...fontAssets(),
|
|
18
20
|
"/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", JS),
|
|
@@ -4,9 +4,77 @@ export const RECORDS_SCRIPT = String.raw `
|
|
|
4
4
|
// never acknowledges or accepts anything.
|
|
5
5
|
import { h, icon } from "/assets/js/lib/dom.js";
|
|
6
6
|
import { button, jsonBlock, mono, note } from "/assets/js/ui/primitives.js";
|
|
7
|
-
import { rawDisclosure } from "/assets/js/ui/containers.js";
|
|
7
|
+
import { rawDisclosure, listCard, setCardCount } from "/assets/js/ui/containers.js";
|
|
8
8
|
import { richText } from "/assets/js/ui/text.js";
|
|
9
|
-
import { cachedExact, readExact, summaryFields } from "/assets/js/domain/context.js";
|
|
9
|
+
import { cachedExact, readExact, summaryFields, entriesOf, totalOf, exactCache } from "/assets/js/domain/context.js";
|
|
10
|
+
|
|
11
|
+
// Current-family counts and pages use exactly the Context current filter.
|
|
12
|
+
// A changed Context revalidates already-loaded membership with one bounded
|
|
13
|
+
// page. Only the family's own digest invalidates its pages, not unrelated
|
|
14
|
+
// runtime events. Loading a list does not eagerly inspect its bodies.
|
|
15
|
+
export function currentRecordCard(data, store, t, actions, options) {
|
|
16
|
+
const view = data.viewState;
|
|
17
|
+
exactCache(view);
|
|
18
|
+
view.openRows = view.openRows || {};
|
|
19
|
+
view.currentLists = view.currentLists || {};
|
|
20
|
+
let state = view.currentLists[store];
|
|
21
|
+
if (!state || (!state.started && state.key !== data.core.coreCursor)) {
|
|
22
|
+
state = view.currentLists[store] = {
|
|
23
|
+
key: data.core.coreCursor, items: entriesOf(data.core, store),
|
|
24
|
+
total: totalOf(data.core, store), started: false, cursor: null, error: null, pending: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const revalidate = state.key !== data.core.coreCursor;
|
|
28
|
+
state.key = data.core.coreCursor;
|
|
29
|
+
const element = listCard({ id: options.id, title: options.title, icon: options.icon || "pulse", count: state.total });
|
|
30
|
+
const rows = h("div.stack"), footer = h("div.stack", { "aria-live": "polite" });
|
|
31
|
+
element.list.append(rows, footer);
|
|
32
|
+
function draw(updateRows) {
|
|
33
|
+
setCardCount(element, state.total);
|
|
34
|
+
if (updateRows !== false) {
|
|
35
|
+
rows.replaceChildren();
|
|
36
|
+
state.items.forEach(function (item) {
|
|
37
|
+
rows.append(!item.omitted && item.value && options.render ? options.render(item.value, null, item)
|
|
38
|
+
: lazyRow(item, data.task.id, t, actions, {
|
|
39
|
+
cache: view.exact,
|
|
40
|
+
openRows: view.openRows,
|
|
41
|
+
head: h("span", null, item.ref.refId + (item.summary ? " · " + item.summary : "")),
|
|
42
|
+
render: options.render
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
footer.replaceChildren(note(t("activity.loaded") + " " + state.items.length + " / " + state.total));
|
|
47
|
+
if (!state.total) footer.append(note(t("activity.none")));
|
|
48
|
+
if (state.error) footer.append(note(state.error + " · " + t("activity.refreshHint"), "bad"));
|
|
49
|
+
if ((!state.started && state.items.length < state.total) || state.cursor) {
|
|
50
|
+
footer.append(button(t("activity.more") + " (" + Math.max(0, state.total - state.items.length) + ")", {
|
|
51
|
+
disabled: state.pending, onClick: function () { load(state.started); }
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
footer.append(button(t("activity.refresh"), { disabled: state.pending, onClick: function () { load(false); } }));
|
|
55
|
+
element.dataset.reading = String(state.pending);
|
|
56
|
+
}
|
|
57
|
+
async function load(more) {
|
|
58
|
+
if (state.pending) return;
|
|
59
|
+
const busy = function () { return !!rows.querySelector('[data-unsent="true"], [data-reading="true"]'); };
|
|
60
|
+
if (busy()) { state.error = t("activity.busy"); draw(false); return; }
|
|
61
|
+
state.pending = true;
|
|
62
|
+
draw(false);
|
|
63
|
+
try {
|
|
64
|
+
const page = await actions.list(data.task.id, store, { view: "current", limit: 20, cursor: more ? state.cursor : null });
|
|
65
|
+
if (busy()) throw new Error(t("activity.busy"));
|
|
66
|
+
if (more || !state.started || state.digest !== page.digest) {
|
|
67
|
+
state.items = more ? state.items.concat(page.items) : page.items;
|
|
68
|
+
state.cursor = page.nextCursor;
|
|
69
|
+
}
|
|
70
|
+
state.digest = page.digest; state.total = page.total; state.started = true; state.error = null;
|
|
71
|
+
} catch (error) { state.error = error.message; }
|
|
72
|
+
finally { state.pending = false; draw(!state.error); }
|
|
73
|
+
}
|
|
74
|
+
draw();
|
|
75
|
+
if (revalidate) load(false);
|
|
76
|
+
return element;
|
|
77
|
+
}
|
|
10
78
|
|
|
11
79
|
// A row drawn from its exact value: synchronously when cached, otherwise the
|
|
12
80
|
// summary card until the read returns and replaces it in place.
|