@zq-silk/yui 2.8.1 → 2.10.0
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 +5 -3
- package/dist/cli/commandCatalog.js +56 -21
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/globalRoleCommands.js +30 -9
- package/dist/commands/inboxCommands.js +79 -22
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +8 -2
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskProjectDeliveryCommand.js +56 -0
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
- package/dist/context/runContextPack.js +25 -4
- package/dist/context/runInputContract.js +1 -0
- package/dist/context/taskContext.js +53 -16
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +38 -17
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
- package/dist/controller/globalInputDelivery.js +17 -7
- package/dist/controller/inboxTurnCheck.js +64 -0
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/providerRetryAdmission.js +2 -0
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +12 -29
- package/dist/grant/taskAuthorization.js +7 -2
- package/dist/interaction/notificationPresentation.js +12 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/message/globalInboxRecovery.js +44 -0
- package/dist/message/globalInterrupt.js +1 -1
- package/dist/message/inboxReceipt.js +14 -6
- package/dist/message/inboxTurn.js +86 -0
- package/dist/message/message.js +11 -0
- package/dist/message/taskInboxOwnership.js +20 -6
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/repository/workspaceCleanupInspection.js +6 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +1 -1
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +95 -24
- package/dist/storage/sqliteStore.js +123 -10
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/archiveDiagnostics.js +1 -1
- package/dist/task/archivePreflight.js +1 -1
- package/dist/task/projectDeliveryScope.js +20 -0
- package/dist/task/publicationToolObservation.js +126 -0
- package/dist/task/remoteDelivery.js +19 -8
- package/dist/task/remoteDeliveryService.js +5 -4
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +65 -2
- package/dist/web/assets/client/i18n/zh-CN.js +65 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/controllerWeb.js +73 -21
- package/dist/web/taskPreviews.js +21 -13
- package/dist/web/webAccess.js +109 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +76 -44
- package/docs/architecture/capabilities-and-resources.md +6 -4
- package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
- package/docs/cli-information-contract.md +57 -4
- package/docs/cli-information-contract.zh-CN.md +41 -2
- package/docs/job-runs.md +224 -0
- package/docs/project-delivery-scope.md +59 -0
- package/docs/release-workflow.md +86 -0
- package/docs/release-workflow.zh-CN.md +68 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +31 -1
- package/docs/storage-baseline.zh-CN.md +25 -1
- package/docs/task-delivery.md +74 -2
- package/docs/task-delivery.zh-CN.md +39 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/i18n/README.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +44 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-operator/SKILL.md +31 -0
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +67 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-runtime/references/recovery.md +21 -0
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
package/dist/job/durableJob.js
CHANGED
|
@@ -11,6 +11,9 @@ export const DURABLE_JOB_TERMINAL_STATUSES = [
|
|
|
11
11
|
"cancelled",
|
|
12
12
|
"unknown-needs-attention"
|
|
13
13
|
];
|
|
14
|
+
export function executionNamespace(run) {
|
|
15
|
+
return "taskId" in run ? run.taskId : "global";
|
|
16
|
+
}
|
|
14
17
|
export function isDurableJobTerminal(status) {
|
|
15
18
|
return DURABLE_JOB_TERMINAL_STATUSES.includes(status);
|
|
16
19
|
}
|
|
@@ -22,6 +25,9 @@ export function createDurableJob(input, now) {
|
|
|
22
25
|
return validateDurableJob({
|
|
23
26
|
schemaVersion: CURRENT_DURABLE_JOB_SCHEMA_VERSION,
|
|
24
27
|
id: input.id,
|
|
28
|
+
definitionId: input.definitionId ?? input.id,
|
|
29
|
+
definitionRevision: input.definitionRevision ?? 1,
|
|
30
|
+
trigger: input.trigger ?? { source: "manual", key: input.operation.requestId },
|
|
25
31
|
taskId: input.taskId,
|
|
26
32
|
owner: input.owner,
|
|
27
33
|
projectId: input.projectId,
|
|
@@ -233,50 +239,26 @@ export function validateDurableJob(job) {
|
|
|
233
239
|
throw new Error(`DurableJob must use schemaVersion ${CURRENT_DURABLE_JOB_SCHEMA_VERSION}.`);
|
|
234
240
|
}
|
|
235
241
|
validateOperationFacts(job.operation);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
stepNames.add(name);
|
|
258
|
-
requireText(step.command, `DurableJob step command (${name})`);
|
|
259
|
-
if (step.argv !== undefined) {
|
|
260
|
-
if (!Array.isArray(step.argv) || step.argv.length === 0) {
|
|
261
|
-
throw new Error(`DurableJob step argv must be a non-empty array (${name}).`);
|
|
262
|
-
}
|
|
263
|
-
for (const value of step.argv) {
|
|
264
|
-
requireText(value, `DurableJob step argv (${name})`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (step.cwd !== undefined) {
|
|
268
|
-
requireText(step.cwd, `DurableJob step cwd (${name})`);
|
|
269
|
-
if (!step.cwd.startsWith("/")) {
|
|
270
|
-
throw new Error(`DurableJob step cwd must be absolute (${name}).`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (step.env !== undefined) {
|
|
274
|
-
validateEnvMap(step.env);
|
|
275
|
-
}
|
|
276
|
-
if (step.timeoutMs !== undefined) {
|
|
277
|
-
requirePositiveInteger(step.timeoutMs, `DurableJob step timeout (${name})`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
242
|
+
requireIdentity(job.definitionId, "Job definition id");
|
|
243
|
+
requirePositiveInteger(job.definitionRevision, "Job definition revision");
|
|
244
|
+
requireText(job.trigger?.key, "Job trigger key");
|
|
245
|
+
if (!["manual", "retry", "schedule", "integration", "migration"].includes(job.trigger.source)) {
|
|
246
|
+
throw new Error("Invalid JobRun trigger source.");
|
|
247
|
+
}
|
|
248
|
+
if ("taskId" in job) {
|
|
249
|
+
validateTaskRecordReference({ taskId: job.taskId, localId: job.id }, "durableJob");
|
|
250
|
+
validateDurableJobOwner(job.owner, job.taskId);
|
|
251
|
+
requireIdentity(job.projectId, "DurableJob project id");
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (job.scope !== "global")
|
|
255
|
+
throw new Error("Invalid global JobRun scope.");
|
|
256
|
+
requireIdentity(job.id, "JobRun id");
|
|
257
|
+
requireText(job.action, "JobRun builtin action");
|
|
258
|
+
if (job.projectId !== undefined)
|
|
259
|
+
requireIdentity(job.projectId, "JobRun Project id");
|
|
260
|
+
}
|
|
261
|
+
validateJobExecutionConfiguration(job);
|
|
280
262
|
requireText(job.idempotencyKey, "DurableJob idempotency key");
|
|
281
263
|
if (!/^[a-f0-9]{64}$/u.test(job.idempotencyKey)) {
|
|
282
264
|
throw new Error("DurableJob idempotency key is invalid.");
|
|
@@ -321,7 +303,10 @@ export function validateDurableJob(job) {
|
|
|
321
303
|
normalizeDurableJobResult(job.result);
|
|
322
304
|
}
|
|
323
305
|
if (job.retryOf !== undefined) {
|
|
324
|
-
|
|
306
|
+
if ("taskId" in job)
|
|
307
|
+
validateTaskRecordReference({ taskId: job.taskId, localId: job.retryOf }, "durableJob");
|
|
308
|
+
else
|
|
309
|
+
requireIdentity(job.retryOf, "JobRun retryOf");
|
|
325
310
|
}
|
|
326
311
|
if (job.cancelRequestedAt !== undefined) {
|
|
327
312
|
requireTimestamp(job.cancelRequestedAt, "DurableJob cancelRequestedAt");
|
|
@@ -345,14 +330,18 @@ export function validateDurableJob(job) {
|
|
|
345
330
|
}
|
|
346
331
|
export function validDurableJobTransition(before, after) {
|
|
347
332
|
if (before.id !== after.id
|
|
348
|
-
|| before.
|
|
333
|
+
|| before.definitionId !== after.definitionId
|
|
334
|
+
|| before.definitionRevision !== after.definitionRevision
|
|
335
|
+
|| !isDeepStrictEqual(before.trigger, after.trigger)
|
|
336
|
+
|| before.retryOf !== after.retryOf
|
|
337
|
+
|| executionNamespace(before) !== executionNamespace(after)
|
|
349
338
|
|| before.projectId !== after.projectId
|
|
350
339
|
|| before.head !== after.head
|
|
351
340
|
|| before.workspace !== after.workspace
|
|
352
341
|
|| before.idempotencyKey !== after.idempotencyKey
|
|
353
342
|
|| before.artifactsLocator !== after.artifactsLocator
|
|
354
343
|
|| before.createdAt !== after.createdAt
|
|
355
|
-
|| !isDeepStrictEqual(before.owner, after.owner)
|
|
344
|
+
|| !isDeepStrictEqual("owner" in before ? before.owner : [before.action, before.actionInput], "owner" in after ? after.owner : [after.action, after.actionInput])
|
|
356
345
|
|| !isDeepStrictEqual(before.env, after.env)
|
|
357
346
|
|| !isDeepStrictEqual(before.steps, after.steps)
|
|
358
347
|
|| before.operation.requestId !== after.operation.requestId
|
|
@@ -374,7 +363,10 @@ export function validDurableJobTransition(before, after) {
|
|
|
374
363
|
const wakeupFlip = before.wakeupNotified !== true && after.wakeupNotified === true;
|
|
375
364
|
const acknowledgeFlip = before.acknowledgedAt === undefined
|
|
376
365
|
&& after.acknowledgedAt !== undefined;
|
|
377
|
-
|
|
366
|
+
const { wakeupNotified: _beforeWake, acknowledgedAt: _beforeAck, updatedAt: _beforeTime, ...beforeFacts } = before;
|
|
367
|
+
const { wakeupNotified: _afterWake, acknowledgedAt: _afterAck, updatedAt: _afterTime, ...afterFacts } = after;
|
|
368
|
+
return isDeepStrictEqual(beforeFacts, afterFacts)
|
|
369
|
+
&& before.status === after.status
|
|
378
370
|
&& isDeepStrictEqual(before.result, after.result)
|
|
379
371
|
&& isDeepStrictEqual(before.operation, after.operation)
|
|
380
372
|
&& before.terminalAt === after.terminalAt
|
|
@@ -398,7 +390,7 @@ export function validDurableJobTransition(before, after) {
|
|
|
398
390
|
};
|
|
399
391
|
return (allowed[before.status] ?? []).includes(after.status);
|
|
400
392
|
}
|
|
401
|
-
function validateDurableJobOwner(owner, taskId) {
|
|
393
|
+
export function validateDurableJobOwner(owner, taskId) {
|
|
402
394
|
if (owner.kind === "integration-attempt") {
|
|
403
395
|
validateTaskRecordReference({ taskId, localId: owner.integrationAttemptId }, "integrationAttempt");
|
|
404
396
|
}
|
|
@@ -409,6 +401,37 @@ function validateDurableJobOwner(owner, taskId) {
|
|
|
409
401
|
throw new Error(`DurableJob owner kind is invalid: ${String(owner.kind)}.`);
|
|
410
402
|
}
|
|
411
403
|
}
|
|
404
|
+
export function validateJobExecutionConfiguration(job) {
|
|
405
|
+
requireCommit(job.head, "DurableJob head");
|
|
406
|
+
requireText(job.workspace, "DurableJob workspace");
|
|
407
|
+
if (!job.workspace.startsWith("/"))
|
|
408
|
+
throw new Error("DurableJob workspace must be an absolute path.");
|
|
409
|
+
validateEnvMap(job.env);
|
|
410
|
+
if (!Array.isArray(job.steps) || job.steps.length === 0) {
|
|
411
|
+
throw new Error("DurableJob requires at least one step.");
|
|
412
|
+
}
|
|
413
|
+
const names = new Set();
|
|
414
|
+
for (const step of job.steps) {
|
|
415
|
+
const name = requireText(step.name, "DurableJob step name");
|
|
416
|
+
if (names.has(name))
|
|
417
|
+
throw new Error(`DurableJob step names must be unique: ${name}.`);
|
|
418
|
+
names.add(name);
|
|
419
|
+
requireText(step.command, `DurableJob step command (${name})`);
|
|
420
|
+
if (step.argv !== undefined) {
|
|
421
|
+
if (!Array.isArray(step.argv) || step.argv.length === 0)
|
|
422
|
+
throw new Error("Step argv must be nonempty.");
|
|
423
|
+
for (const value of step.argv)
|
|
424
|
+
requireText(value, `DurableJob step argv (${name})`);
|
|
425
|
+
}
|
|
426
|
+
if (step.cwd !== undefined && !requireText(step.cwd, "Step cwd").startsWith("/")) {
|
|
427
|
+
throw new Error("DurableJob step cwd must be absolute.");
|
|
428
|
+
}
|
|
429
|
+
if (step.env !== undefined)
|
|
430
|
+
validateEnvMap(step.env);
|
|
431
|
+
if (step.timeoutMs !== undefined)
|
|
432
|
+
requirePositiveInteger(step.timeoutMs, "Step timeout");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
412
435
|
function validateEnvMap(env) {
|
|
413
436
|
if (typeof env !== "object" || env === null || Array.isArray(env)) {
|
|
414
437
|
throw new Error("DurableJob env must be a map.");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { requireIdentity, requireKnownFields, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { validateJobExecutionConfiguration, validateDurableJobOwner } from "./durableJob.js";
|
|
3
|
+
export function jobScopeKey(scope) {
|
|
4
|
+
if (scope.kind === "global")
|
|
5
|
+
return "global";
|
|
6
|
+
requireIdentity(scope.taskId, "Job Task id");
|
|
7
|
+
if (!/^task-\d+$/u.test(scope.taskId))
|
|
8
|
+
throw new Error("Invalid Job Task id.");
|
|
9
|
+
return scope.taskId;
|
|
10
|
+
}
|
|
11
|
+
export function validateJobDefinition(job) {
|
|
12
|
+
requireKnownFields(job, ["schemaVersion", "id", "scope", "title", "revision", "configuration", "createdAt", "updatedAt"], "Job");
|
|
13
|
+
if (job.schemaVersion !== 1)
|
|
14
|
+
throw new Error("Job schemaVersion must be 1.");
|
|
15
|
+
requireIdentity(job.id, "Job id");
|
|
16
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(job.id))
|
|
17
|
+
throw new Error("Invalid Job definition id.");
|
|
18
|
+
requireText(job.title, "Job title");
|
|
19
|
+
requirePositiveInteger(job.revision, "Job revision");
|
|
20
|
+
requireTimestamp(job.createdAt, "Job createdAt");
|
|
21
|
+
requireTimestamp(job.updatedAt, "Job updatedAt");
|
|
22
|
+
if (job.scope.kind !== "task" && job.scope.kind !== "global")
|
|
23
|
+
throw new Error("Invalid Job scope.");
|
|
24
|
+
requireKnownFields(job.scope, job.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Job scope");
|
|
25
|
+
jobScopeKey(job.scope);
|
|
26
|
+
const config = job.configuration;
|
|
27
|
+
if (job.scope.kind === "global") {
|
|
28
|
+
if (config.kind !== "builtin")
|
|
29
|
+
throw new Error("Global Jobs require a controlled builtin action.");
|
|
30
|
+
requireKnownFields(config, ["kind", "action", "projectId", "input"], "Builtin Job");
|
|
31
|
+
requireText(config.action, "Builtin action");
|
|
32
|
+
if (config.projectId !== undefined)
|
|
33
|
+
requireIdentity(config.projectId, "Job Project id");
|
|
34
|
+
if (config.input === undefined)
|
|
35
|
+
throw new Error("Builtin Job input is required.");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (config.kind !== "shell")
|
|
39
|
+
throw new Error("Task Jobs require a managed shell configuration.");
|
|
40
|
+
requireKnownFields(config, ["kind", "owner", "projectId", "head", "workspace", "env", "steps"], "Task Job");
|
|
41
|
+
requireIdentity(config.projectId, "Job Project id");
|
|
42
|
+
validateDurableJobOwner(config.owner, job.scope.taskId);
|
|
43
|
+
validateJobExecutionConfiguration(config);
|
|
44
|
+
}
|
|
45
|
+
return job;
|
|
46
|
+
}
|
|
47
|
+
export function createJobDefinition(input, now) {
|
|
48
|
+
return validateJobDefinition({
|
|
49
|
+
schemaVersion: 1, ...structuredClone(input), revision: 1,
|
|
50
|
+
createdAt: now.toISOString(), updatedAt: now.toISOString()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function reviseJobDefinition(job, input, now) {
|
|
54
|
+
return validateJobDefinition({
|
|
55
|
+
...job, title: input.title, configuration: structuredClone(input.configuration),
|
|
56
|
+
revision: job.revision + 1, updatedAt: now.toISOString()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Old execution IDs are preserved one-to-one; command equality never merges history. */
|
|
60
|
+
export function definitionFromTaskRun(run) {
|
|
61
|
+
return createJobDefinition({
|
|
62
|
+
id: run.definitionId, scope: { kind: "task", taskId: run.taskId }, title: run.steps[0].name,
|
|
63
|
+
configuration: {
|
|
64
|
+
kind: "shell", owner: run.owner, projectId: run.projectId, head: run.head,
|
|
65
|
+
workspace: run.workspace, env: run.env, steps: run.steps
|
|
66
|
+
}
|
|
67
|
+
}, new Date(run.createdAt));
|
|
68
|
+
}
|
package/dist/job/jobRunner.js
CHANGED
|
@@ -267,7 +267,8 @@ function readSpec(specPath) {
|
|
|
267
267
|
if (typeof spec.jobId !== "string" || spec.jobId.length === 0) {
|
|
268
268
|
throw new Error("DurableJob spec is missing jobId.");
|
|
269
269
|
}
|
|
270
|
-
if (
|
|
270
|
+
if (spec.scope === "global" ? spec.taskId !== undefined
|
|
271
|
+
: typeof spec.taskId !== "string" || spec.taskId.length === 0) {
|
|
271
272
|
throw new Error("DurableJob spec is missing taskId.");
|
|
272
273
|
}
|
|
273
274
|
if (typeof spec.workspace !== "string" || !spec.workspace.startsWith("/")) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controlled Project domain action invoked by the artifact-only Job runner.
|
|
3
|
+
* Its catalog connection is read-only; only the shared domain service writes Git.
|
|
4
|
+
*/
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { SqliteTaskStore } from "../storage/sqliteStore.js";
|
|
8
|
+
import { authorizeGlobalJobStart } from "../controller/jobDefinitionControl.js";
|
|
9
|
+
import { refreshRegisteredProject } from "../repository/projectRefresh.js";
|
|
10
|
+
import { projectRefreshDigest } from "./builtinJob.js";
|
|
11
|
+
import { CliError } from "../errors/cliError.js";
|
|
12
|
+
export async function runProjectRefresh(home, runId) {
|
|
13
|
+
const store = new SqliteTaskStore(home, { readonly: true });
|
|
14
|
+
try {
|
|
15
|
+
const run = store.getGlobalJobRun(runId);
|
|
16
|
+
if (!run || run.action !== "project.refresh" || !["queued", "running"].includes(run.status)
|
|
17
|
+
|| run.operation.effect !== "possible" || run.cancelRequestedAt) {
|
|
18
|
+
throw new Error("No active admitted project.refresh Run.");
|
|
19
|
+
}
|
|
20
|
+
authorizeGlobalJobStart(store, run);
|
|
21
|
+
const result = await refreshRegisteredProject(store, run.projectId, {
|
|
22
|
+
automatic: true,
|
|
23
|
+
expectedDigest: projectRefreshDigest({
|
|
24
|
+
kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
|
|
25
|
+
}),
|
|
26
|
+
beforeRefresh: () => {
|
|
27
|
+
const current = store.getGlobalJobRun(runId);
|
|
28
|
+
if (!current || !["queued", "running"].includes(current.status) || current.cancelRequestedAt) {
|
|
29
|
+
throw new Error("Project refresh Run stopped while waiting for maintenance.");
|
|
30
|
+
}
|
|
31
|
+
authorizeGlobalJobStart(store, current);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
process.stdout.write(JSON.stringify({ action: run.action, projectId: run.projectId,
|
|
35
|
+
trigger: run.trigger, outcome: "succeeded", observedAt: new Date().toISOString(),
|
|
36
|
+
remoteCheck: { status: "verified", commit: result.toCommit }, refresh: result }) + "\n");
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
store.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
43
|
+
runProjectRefresh(process.argv[2], process.argv[3]).catch(error => {
|
|
44
|
+
process.stderr.write(JSON.stringify({ outcome: "failed", reason: error instanceof Error ? error.message : String(error),
|
|
45
|
+
...(error instanceof CliError ? { details: error.details } : {}) }) + "\n");
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createGlobalRoleMessage } from "./message.js";
|
|
3
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
+
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
5
|
+
import { INBOX_CHECK_PREFIX } from "./inboxTurn.js";
|
|
6
|
+
const requestId = (agentId, nativeSessionId) => `${INBOX_CHECK_PREFIX}recovery:${createHash("sha256").update(JSON.stringify([agentId, nativeSessionId])).digest("hex")}`;
|
|
7
|
+
/** Derive a single initialization notice from existing retained claims. New
|
|
8
|
+
* Sessions are created only by the existing authorized lifecycle path. A
|
|
9
|
+
* restart can finish notice creation, but cannot select or replace a Session. */
|
|
10
|
+
export function globalInboxRecovery(store, roleName, now) {
|
|
11
|
+
return store.transaction(tx => {
|
|
12
|
+
const sessions = tx.getGlobalRoleSessionSet(roleName);
|
|
13
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
14
|
+
const binding = sessions?.providerBinding;
|
|
15
|
+
if (!session || session.status !== "active" || !session.nativeSessionId || !binding
|
|
16
|
+
|| binding.authority.owner !== "controller"
|
|
17
|
+
|| currentProviderConversation(binding).conversationId !== session.nativeSessionId
|
|
18
|
+
|| binding.run && ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status)
|
|
19
|
+
|| hasRuntimeCleanupObligation(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName }))))
|
|
20
|
+
return;
|
|
21
|
+
if (tx.queryGlobalRoleMessages(roleName, { handling: true, handlingExceptSession: {
|
|
22
|
+
agentId: session.agentId, nativeSessionId: session.nativeSessionId
|
|
23
|
+
}, limit: 0 }).count === 0)
|
|
24
|
+
return;
|
|
25
|
+
const id = requestId(session.agentId, session.nativeSessionId);
|
|
26
|
+
const existing = tx.queryGlobalRoleMessages(roleName, { requestId: id, limit: 1 }).messages[0];
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
const notice = {
|
|
30
|
+
...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), roleName, [
|
|
31
|
+
"Yui-Summary: This successor has retained inbox handling obligations; inspect original evidence before settlement.",
|
|
32
|
+
"Read current Session Context and yui inbox list --view history --json. Read the exact original, original claim and prior effects.",
|
|
33
|
+
"This is initialization only: do not replay old input, retarget its delivery, or infer business completion.",
|
|
34
|
+
"After handling under current authority, explicitly acknowledge the original entry without replacing its claim.",
|
|
35
|
+
"Uncertain external effects require verification or a real user decision. New queued input remains separate."
|
|
36
|
+
].join("\n"), "system", { type: "system" }, now, {
|
|
37
|
+
inputControl: { action: "queue", requestId: id, expectedSessionId: session.nativeSessionId }
|
|
38
|
+
}),
|
|
39
|
+
deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId }
|
|
40
|
+
};
|
|
41
|
+
tx.saveGlobalRoleMessage(notice);
|
|
42
|
+
return notice;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -11,7 +11,7 @@ export function recordGlobalSteerResult(store, roleName, messageId, receipt) {
|
|
|
11
11
|
const now = new Date();
|
|
12
12
|
const outcome = receipt.state === "steered" ? "accepted"
|
|
13
13
|
: receipt.state === "steer-unknown" ? "delivery-unknown" : "rejected";
|
|
14
|
-
const updated = { ...message, control: {
|
|
14
|
+
const updated = { ...message, control: { ...message.control,
|
|
15
15
|
requestId: source.requestId, receiptId: `steer:${roleName}/${messageId}`,
|
|
16
16
|
outcome, observedAt: now.toISOString()
|
|
17
17
|
} };
|
|
@@ -3,7 +3,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
3
3
|
/** Semantic handling and transport delivery are independent facts. Projections
|
|
4
4
|
* must not infer handling from delivery, a native terminal, or an empty queue. */
|
|
5
5
|
export function inboxHandlingState(receipt) {
|
|
6
|
-
return receipt?.
|
|
6
|
+
return receipt?.retirement !== undefined ? "retired" : receipt?.ack !== undefined ? "handled"
|
|
7
|
+
: receipt !== undefined ? "claimed-unacknowledged" : "unclaimed";
|
|
7
8
|
}
|
|
8
9
|
export function isGlobalInboxMessage(message) {
|
|
9
10
|
return message.inputControl !== undefined || message.interruptThen !== undefined;
|
|
@@ -11,7 +12,8 @@ export function isGlobalInboxMessage(message) {
|
|
|
11
12
|
/** Unhandled own-role input, including accepted and unknown delivery. This is
|
|
12
13
|
* NOT the scheduler's deliverable queue; selecting it never authorizes replay. */
|
|
13
14
|
export function pendingGlobalInboxMessages(messages) {
|
|
14
|
-
return messages.filter(message => isGlobalInboxMessage(message)
|
|
15
|
+
return messages.filter(message => isGlobalInboxMessage(message)
|
|
16
|
+
&& !["handled", "retired"].includes(inboxHandlingState(message.inbox)));
|
|
15
17
|
}
|
|
16
18
|
/** Original message evidence only. A missing receipt is not proof of
|
|
17
19
|
* non-submission; admission additionally inspects the exact native attempt. */
|
|
@@ -22,20 +24,22 @@ export function globalInboxDeliveryState(message) {
|
|
|
22
24
|
export function validateInboxReceipt(receipt) {
|
|
23
25
|
if (receipt === undefined)
|
|
24
26
|
return;
|
|
25
|
-
requireKnownFields(receipt, ["claim", "ack"], "Inbox receipt");
|
|
27
|
+
requireKnownFields(receipt, ["claim", "ack", "retirement"], "Inbox receipt");
|
|
28
|
+
if (receipt.ack && receipt.retirement)
|
|
29
|
+
throw new Error("Inbox disposition cannot be both acknowledgement and retirement.");
|
|
26
30
|
for (const [kind, entry] of Object.entries(receipt)) {
|
|
27
31
|
requireKnownFields(entry, kind === "claim" ? ["requestId", "actor", "at"]
|
|
28
32
|
: ["requestId", "actor", "at", "reason"], "Inbox action");
|
|
29
33
|
requireText(entry.requestId, "Inbox request id");
|
|
30
34
|
requireTimestamp(entry.at, "Inbox timestamp");
|
|
31
35
|
const actor = entry.actor;
|
|
32
|
-
requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId"], "Inbox actor");
|
|
36
|
+
requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId", "attemptId"], "Inbox actor");
|
|
33
37
|
if (!["task", "global"].includes(actor.scope)
|
|
34
38
|
|| (actor.scope === "task") !== (actor.taskId !== undefined))
|
|
35
39
|
throw new Error("Inbox actor scope is invalid.");
|
|
36
40
|
for (const value of Object.values(actor))
|
|
37
41
|
requireText(value, "Inbox actor identity");
|
|
38
|
-
if (kind
|
|
42
|
+
if (kind !== "claim")
|
|
39
43
|
requireText(entry.reason, "Inbox disposition");
|
|
40
44
|
}
|
|
41
45
|
if (!receipt.claim)
|
|
@@ -45,7 +49,8 @@ export function validateInboxReceipt(receipt) {
|
|
|
45
49
|
* Host admission. The original claim and any ack are immutable audit facts. */
|
|
46
50
|
export function assertInboxReceiptPreserved(previous, next) {
|
|
47
51
|
if (previous !== undefined && (!isDeepStrictEqual(previous.claim, next?.claim)
|
|
48
|
-
|| previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack)
|
|
52
|
+
|| previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack)
|
|
53
|
+
|| previous.retirement !== undefined && !isDeepStrictEqual(previous.retirement, next?.retirement))) {
|
|
49
54
|
throw new Error("Inbox handling ownership changed; reread the original instead of overwriting its receipt.");
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -55,6 +60,9 @@ export function assertInboxReceiptPreserved(previous, next) {
|
|
|
55
60
|
*/
|
|
56
61
|
export function assertInboxOriginalPreserved(previous, next) {
|
|
57
62
|
assertInboxReceiptPreserved(previous?.inbox, next.inbox);
|
|
63
|
+
if (previous?.control?.target !== undefined && !isDeepStrictEqual(previous.control.target, next.control?.target)) {
|
|
64
|
+
throw new Error("The exact admitted control target cannot be replaced or erased.");
|
|
65
|
+
}
|
|
58
66
|
if (previous?.inbox !== undefined && (previous.body !== next.body || previous.kind !== next.kind
|
|
59
67
|
|| !isDeepStrictEqual(previous.author, next.author) || !isDeepStrictEqual(previous.inputControl, next.inputControl))) {
|
|
60
68
|
throw new Error("Inbox receipt binds the exact original Message; submit changed content as a new Message.");
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { globalInboxDeliveryState } from "./inboxReceipt.js";
|
|
2
|
+
export const INBOX_CHECK_PREFIX = "inbox-check:";
|
|
3
|
+
export const INBOX_CHECK_REASON = "inbox-handling-missing";
|
|
4
|
+
export const isInboxWake = (entry) => "fromCursor" in entry;
|
|
5
|
+
export const isHandlingReminder = (entry) => isInboxWake(entry)
|
|
6
|
+
? entry.reasons.every(reason => reason === INBOX_CHECK_REASON)
|
|
7
|
+
: entry.kind === "system" && entry.inputControl?.requestId.startsWith(INBOX_CHECK_PREFIX) === true;
|
|
8
|
+
/** Correlation uses existing input identities, not timestamps, output text or
|
|
9
|
+
* Task state. Active consumption adds only the exact attempt to its claim. */
|
|
10
|
+
export function belongsToInboxTurn(entry, actor, turn) {
|
|
11
|
+
if (!turn)
|
|
12
|
+
return false;
|
|
13
|
+
const claim = entry.inbox?.claim.actor;
|
|
14
|
+
if (claim?.attemptId === turn.attemptId && claim.scope === actor.scope && claim.taskId === actor.taskId
|
|
15
|
+
&& claim.roleName === actor.roleName && claim.agentId === actor.agentId
|
|
16
|
+
&& claim.nativeSessionId === actor.nativeSessionId)
|
|
17
|
+
return true;
|
|
18
|
+
if (isInboxWake(entry))
|
|
19
|
+
return actor.scope === "task" && actor.roleName === "leader"
|
|
20
|
+
&& entry.taskId === actor.taskId && (turn.input?.kind === "wake" && turn.input.wakeId === entry.id
|
|
21
|
+
&& turn.input.taskId === entry.taskId
|
|
22
|
+
|| turn.attemptId.startsWith(`notification:${entry.taskId}/${entry.id}/`));
|
|
23
|
+
if ("roleName" in entry) {
|
|
24
|
+
if (actor.scope !== "global" || entry.roleName !== actor.roleName)
|
|
25
|
+
return false;
|
|
26
|
+
if (entry.deliveryTarget && (entry.deliveryTarget.agentId !== actor.agentId
|
|
27
|
+
|| entry.deliveryTarget.nativeSessionId !== actor.nativeSessionId))
|
|
28
|
+
return false;
|
|
29
|
+
if (turn.input?.kind === "message" && turn.input.roleName === actor.roleName
|
|
30
|
+
&& turn.input.messageId === entry.id || turn.attemptId === `global-input:${actor.roleName}/${entry.id}`)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (actor.scope !== "task" || entry.taskId !== actor.taskId)
|
|
35
|
+
return false;
|
|
36
|
+
if (turn.runId !== undefined && entry.recipient?.roleName === actor.roleName
|
|
37
|
+
&& entry.continuation?.runId === turn.runId)
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const target = entry.control?.target;
|
|
41
|
+
return entry.control?.outcome === "accepted" && target?.agentId === actor.agentId
|
|
42
|
+
&& target.nativeSessionId === actor.nativeSessionId && target.attemptId === turn.attemptId;
|
|
43
|
+
}
|
|
44
|
+
/** Historical absence of ACK is not fresh work. Current unsubmitted input
|
|
45
|
+
* remains discoverable, but never steals an unsettled transport owner. */
|
|
46
|
+
export function inboxEntryCategory(entry, actor, turn) {
|
|
47
|
+
if (belongsToInboxTurn(entry, actor, turn))
|
|
48
|
+
return "current";
|
|
49
|
+
if (entry.inbox !== undefined)
|
|
50
|
+
return "history";
|
|
51
|
+
if (isInboxWake(entry))
|
|
52
|
+
return "history";
|
|
53
|
+
if ("roleName" in entry) {
|
|
54
|
+
if (entry.delivery !== undefined || entry.notDelivered !== undefined
|
|
55
|
+
|| entry.deliveryTarget && (entry.deliveryTarget.agentId !== actor.agentId
|
|
56
|
+
|| entry.deliveryTarget.nativeSessionId !== actor.nativeSessionId))
|
|
57
|
+
return "history";
|
|
58
|
+
}
|
|
59
|
+
else if (entry.continuation?.runId !== undefined)
|
|
60
|
+
return "history";
|
|
61
|
+
if (entry.inputControl?.expectedSessionId !== undefined
|
|
62
|
+
&& entry.inputControl.expectedSessionId !== actor.nativeSessionId)
|
|
63
|
+
return "history";
|
|
64
|
+
if (entry.control?.outcome === "accepted")
|
|
65
|
+
return "history";
|
|
66
|
+
return "available";
|
|
67
|
+
}
|
|
68
|
+
export function inboxSettled(receipt) {
|
|
69
|
+
return receipt?.ack !== undefined || receipt?.retirement !== undefined;
|
|
70
|
+
}
|
|
71
|
+
export function missingTurnHandling(entries, actor, turn) {
|
|
72
|
+
return entries.filter(entry => !inboxSettled(entry.inbox) && !isHandlingReminder(entry)
|
|
73
|
+
&& belongsToInboxTurn(entry, actor, turn));
|
|
74
|
+
}
|
|
75
|
+
/** Accepted historical input and explicit nondelivery do not impose an input
|
|
76
|
+
* order on independent new messages. Unknown delivery still does. */
|
|
77
|
+
export function blocksInboxOrder(entry, actor, turn) {
|
|
78
|
+
if (isInboxWake(entry) || inboxSettled(entry.inbox))
|
|
79
|
+
return false;
|
|
80
|
+
if ("roleName" in entry && (globalInboxDeliveryState(entry) === "accepted" || entry.notDelivered !== undefined))
|
|
81
|
+
return false;
|
|
82
|
+
if (entry.control?.outcome === "accepted")
|
|
83
|
+
return false;
|
|
84
|
+
return inboxEntryCategory(entry, actor, turn) !== "history"
|
|
85
|
+
|| entry.control !== undefined && ["pending", "delivery-unknown"].includes(entry.control.outcome);
|
|
86
|
+
}
|
package/dist/message/message.js
CHANGED
|
@@ -147,6 +147,7 @@ export function recordTaskMessageControlOutcome(message, record) {
|
|
|
147
147
|
requestId: requireSafeIdentity(record.requestId, "Message control requestId"),
|
|
148
148
|
receiptId: requireText(record.receiptId, "Message control receiptId"),
|
|
149
149
|
outcome: record.outcome,
|
|
150
|
+
...(existing?.target ?? record.target ? { target: existing?.target ?? record.target } : {}),
|
|
150
151
|
observedAt: record.observedAt.toISOString()
|
|
151
152
|
}
|
|
152
153
|
};
|
|
@@ -282,6 +283,7 @@ export function validateTaskMessage(message) {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
if (message.control !== undefined) {
|
|
286
|
+
validateControlTarget(message.control);
|
|
285
287
|
requireSafeIdentity(message.control.requestId, "Message control requestId");
|
|
286
288
|
requireText(message.control.receiptId, "Message control receiptId");
|
|
287
289
|
if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)) {
|
|
@@ -408,6 +410,7 @@ export function validateGlobalRoleMessage(message) {
|
|
|
408
410
|
requireText(message.deliveryTarget.nativeSessionId, "Global input Session");
|
|
409
411
|
}
|
|
410
412
|
if (message.control !== undefined) {
|
|
413
|
+
validateControlTarget(message.control);
|
|
411
414
|
requireSafeIdentity(message.control.requestId, "Global input requestId");
|
|
412
415
|
requireText(message.control.receiptId, "Global input receiptId");
|
|
413
416
|
if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)
|
|
@@ -479,6 +482,14 @@ export function validateGlobalRoleMessage(message) {
|
|
|
479
482
|
throw new Error("Message createdAt is invalid.");
|
|
480
483
|
}
|
|
481
484
|
}
|
|
485
|
+
function validateControlTarget(control) {
|
|
486
|
+
if (control.target === undefined)
|
|
487
|
+
return;
|
|
488
|
+
requireKnownFields(control.target, ["agentId", "nativeSessionId", "attemptId"], "Control target");
|
|
489
|
+
requireText(control.target.agentId, "Control target Agent");
|
|
490
|
+
requireText(control.target.nativeSessionId, "Control target Session");
|
|
491
|
+
requireText(control.target.attemptId, "Control target attempt");
|
|
492
|
+
}
|
|
482
493
|
/** A Global message local id is `global-message-<n>`; it carries no Task id. */
|
|
483
494
|
export function validateGlobalRoleMessageId(localId) {
|
|
484
495
|
const normalized = requireSafeIdentity(localId, "Global message id");
|
|
@@ -26,10 +26,15 @@ export function isLeaderSteer(message) {
|
|
|
26
26
|
export function taskMessageInboxReceipt(store, message) {
|
|
27
27
|
return message.inbox ?? messageRevisionWakes(store, message).find(wake => wake.inbox !== undefined)?.inbox;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
export function
|
|
32
|
-
return messageRevisionWakes(store, message).find(wake => wake.status === "consumed");
|
|
29
|
+
/** Acceptance or explicit no-replay release suppresses repeat notification,
|
|
30
|
+
* never handling. The original fixed window stays in the own inbox. */
|
|
31
|
+
export function nonReplayableMessageWake(store, message) {
|
|
32
|
+
return messageRevisionWakes(store, message).find(wake => wake.status === "consumed" || releasedNotificationWake(store, message.taskId, wake.id));
|
|
33
|
+
}
|
|
34
|
+
/** Operational release is not acceptance. Keep its originals out of automatic
|
|
35
|
+
* delivery while permitting explicit inspection and settlement in the inbox. */
|
|
36
|
+
export function releasedNotificationWake(store, taskId, wakeId) {
|
|
37
|
+
return store.listEventsByType(taskId, ["notification.resolved"]).some(event => event.payload.wakeId === wakeId && event.payload.outcome === "released-without-replay");
|
|
33
38
|
}
|
|
34
39
|
function messageRevisionWakes(store, message) {
|
|
35
40
|
const revision = latestMessageUpdate(store, message);
|
|
@@ -37,7 +42,16 @@ function messageRevisionWakes(store, message) {
|
|
|
37
42
|
&& wake.refs?.some(ref => ref.type === "message" && ref.taskId === message.taskId && ref.id === message.id));
|
|
38
43
|
}
|
|
39
44
|
export function unfinishedLeaderInbox(store, taskId) {
|
|
40
|
-
|
|
45
|
+
const sessions = store.getTaskRoleSessionSet(taskId, "leader");
|
|
46
|
+
const binding = sessions?.providerBinding;
|
|
47
|
+
const turn = binding?.run;
|
|
48
|
+
const nativeSessionId = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId;
|
|
49
|
+
const active = (receipt) => receipt !== undefined && receipt.ack === undefined
|
|
50
|
+
&& (receipt.claim.actor.attemptId === undefined || turn?.attemptId === receipt.claim.actor.attemptId
|
|
51
|
+
&& receipt.claim.actor.agentId === sessions?.activeAgentId
|
|
52
|
+
&& receipt.claim.actor.nativeSessionId === nativeSessionId
|
|
53
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(turn.status));
|
|
54
|
+
return store.listTaskWakes(taskId).some(wake => active(wake.inbox))
|
|
41
55
|
|| operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message").some(message => isLeaderSteer(message)
|
|
42
|
-
&& message.inbox
|
|
56
|
+
&& active(message.inbox));
|
|
43
57
|
}
|