@zq-silk/yui 2.19.0 → 2.21.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/dist/cli/commandCatalog.js +5 -3
- package/dist/cli/invocationAuthority.js +2 -6
- package/dist/cli/managedDiagnostics.js +24 -16
- package/dist/commands/durableJobCommands.js +2 -3
- package/dist/commands/roleConfiguration.js +17 -0
- package/dist/commands/taskCommands.js +9 -36
- package/dist/commands/taskFactCommands.js +5 -5
- package/dist/commands/taskUsageCommand.js +2 -6
- package/dist/context/taskCatalog.js +4 -13
- package/dist/context/taskContext.js +35 -87
- package/dist/context/taskSearch.js +3 -7
- package/dist/controlPlaneCli.js +6 -18
- package/dist/controller/fileSchedulerStoreAdapter.js +22 -2
- package/dist/controller/jobClient.js +2 -2
- package/dist/controller/jobControl.js +2 -2
- package/dist/controller/jobDefinitionControl.js +5 -1
- package/dist/controller/runtimeLaunchCoordinator.js +12 -1
- package/dist/executor/agentExecutor.js +7 -0
- package/dist/executor/fileRoleLaunchPlanner.js +22 -6
- package/dist/executor/taskAgentCapabilities.js +2 -2
- package/dist/output/rolePresentation.js +1 -0
- package/dist/role/instanceCode.js +7 -0
- package/dist/role/role.js +10 -0
- package/dist/runtime/agentEndpoint.js +1 -0
- package/dist/runtime/agentHost.js +9 -1
- package/dist/runtime/launchBroker.js +3 -0
- package/dist/runtime/runtimeBinding.js +3 -0
- package/dist/runtime/sessionTitle.js +29 -9
- package/dist/runtime/structuredProviderHost.js +11 -1
- package/dist/runtime/tmuxAdapters.js +4 -0
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/task/taskAuthority.js +7 -3
- package/docs/cli-information-contract.md +17 -0
- package/docs/cli-information-contract.zh-CN.md +12 -0
- package/docs/release-workflow.md +54 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/docs/roles-and-configuration.md +49 -0
- package/docs/roles-and-configuration.zh-CN.md +38 -0
- package/docs/storage-baseline.md +4 -1
- package/docs/storage-baseline.zh-CN.md +1 -1
- package/docs/task-discovery.md +5 -5
- package/docs/task-discovery.zh-CN.md +4 -4
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +10 -6
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-operator/SKILL.md +5 -3
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-operator/references/task-delivery.md +4 -1
- package/skills/yui-reviewer/SKILL.md +4 -2
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +34 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/publication.md +16 -9
- package/skills/yui-worker/SKILL.md +7 -4
- package/skills/yui-worker/VERSION +1 -1
|
@@ -9,7 +9,6 @@ import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryServic
|
|
|
9
9
|
import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
10
10
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
11
11
|
import { contextContentDigest } from "./contextSnapshot.js";
|
|
12
|
-
import { buildRunContextPack } from "./runContextPack.js";
|
|
13
12
|
import { sourceRunContextValue } from "./sourceRunContext.js";
|
|
14
13
|
import { boundedDocument, fitsReadBudget, summaryText, recordPage } from "../output/boundedRead.js";
|
|
15
14
|
import { taskBriefGuard, taskPhase } from "./taskPhase.js";
|
|
@@ -27,12 +26,12 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
27
26
|
const sources = authorizedSources(reader, scope, true);
|
|
28
27
|
const count = sources.reduce((total, source) => total + source.count, 0);
|
|
29
28
|
const cursor = currentCursor(reader, taskId);
|
|
30
|
-
const phase =
|
|
29
|
+
const phase = taskPhase(reader, scope.task);
|
|
31
30
|
const phaseSource = phase?.activation?.source;
|
|
32
31
|
const priorMessageId = phaseSource?.priorMessageId;
|
|
33
32
|
// Reserve a small, classified view before ordinary history consumes the
|
|
34
33
|
// page. Counts and samples come from the exact same authorized read.
|
|
35
|
-
const attention = summarizeAttention(reader, taskId
|
|
34
|
+
const attention = summarizeAttention(reader, taskId);
|
|
36
35
|
let bytes = Buffer.byteLength(JSON.stringify(attention));
|
|
37
36
|
const records = [];
|
|
38
37
|
let runtimeEvents;
|
|
@@ -75,13 +74,13 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
75
74
|
const result = {
|
|
76
75
|
taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
|
|
77
76
|
view: "current",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
briefWriteGuard: taskBriefGuard(reader, scope.task)
|
|
81
|
-
} : {}),
|
|
77
|
+
phase,
|
|
78
|
+
briefWriteGuard: taskBriefGuard(reader, scope.task),
|
|
82
79
|
caller: scope.caller === undefined ? { kind: "unmanaged-or-global" } : {
|
|
80
|
+
taskId: scope.caller.taskId,
|
|
83
81
|
role: scope.caller.roleName,
|
|
84
|
-
executionAuthority:
|
|
82
|
+
executionAuthority: scope.caller.taskId !== taskId ? "reference-only"
|
|
83
|
+
: currentManagedRuntime(reader, environment, taskId)?.executionAuthority ?? "historical-read-only"
|
|
85
84
|
},
|
|
86
85
|
collections: Object.entries(sources.reduce((counts, source) => {
|
|
87
86
|
counts[source.family] = (counts[source.family] ?? 0) + source.count;
|
|
@@ -118,7 +117,7 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
118
117
|
return result;
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
|
-
function summarizeAttention(store, taskId
|
|
120
|
+
function summarizeAttention(store, taskId) {
|
|
122
121
|
const group = () => ({ count: 0, refs: [], omittedRefs: 0 });
|
|
123
122
|
const attention = {
|
|
124
123
|
openInputs: group(), pendingOperations: group(), unknownOperations: group(), messages: group()
|
|
@@ -131,7 +130,7 @@ function summarizeAttention(store, taskId, allow) {
|
|
|
131
130
|
]) {
|
|
132
131
|
const page = store.queryContextRecords(taskId, {
|
|
133
132
|
family, statuses, descending: category === "messages",
|
|
134
|
-
unhandledMessages: category === "messages",
|
|
133
|
+
unhandledMessages: category === "messages", limit: MAX_ATTENTION_REFS
|
|
135
134
|
});
|
|
136
135
|
const summary = attention[category];
|
|
137
136
|
summary.count = page.count;
|
|
@@ -154,7 +153,7 @@ function summarizeAttention(store, taskId, allow) {
|
|
|
154
153
|
*/
|
|
155
154
|
export function readTaskContextDelta(store, taskId, input, environment = {}) {
|
|
156
155
|
return store.readTransaction((reader) => {
|
|
157
|
-
|
|
156
|
+
authorizeContext(reader, taskId, environment);
|
|
158
157
|
const start = decode(input.after, taskId);
|
|
159
158
|
const now = currentCursor(reader, taskId);
|
|
160
159
|
const continuation = input.continuation === undefined ? undefined : decodePage(input.continuation, taskId);
|
|
@@ -174,7 +173,7 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
|
|
|
174
173
|
throw usageError(`Context delta limit must be between 1 and ${MAX_EVENTS}.`);
|
|
175
174
|
}
|
|
176
175
|
const selected = reader.queryContextRecords(taskId, {
|
|
177
|
-
family: "task-event",
|
|
176
|
+
family: "task-event",
|
|
178
177
|
afterSequence: after, throughSequence: bound.sequence, limit
|
|
179
178
|
});
|
|
180
179
|
const page = [];
|
|
@@ -213,8 +212,7 @@ export function inspectTaskContext(store, taskId, selector, environment = {}) {
|
|
|
213
212
|
export function readTaskContextResource(store, taskId, selector, environment = {}) {
|
|
214
213
|
return store.readTransaction((reader) => {
|
|
215
214
|
const scope = authorizeContext(reader, taskId, environment);
|
|
216
|
-
const value =
|
|
217
|
-
? inspectValue(reader, scope, selector) : null;
|
|
215
|
+
const value = inspectValue(reader, scope, selector);
|
|
218
216
|
if (value === null)
|
|
219
217
|
throw usageError("Context reference is unavailable in the caller's current scope.");
|
|
220
218
|
const entry = materialize(selector.store, selector.refId, value);
|
|
@@ -280,9 +278,8 @@ export const CONTEXT_FAMILIES = [
|
|
|
280
278
|
];
|
|
281
279
|
export function listContextMessages(store, taskId, environment = {}) {
|
|
282
280
|
return store.readTransaction((reader) => {
|
|
283
|
-
|
|
281
|
+
authorizeContext(reader, taskId, environment);
|
|
284
282
|
return reader.listMessages(taskId)
|
|
285
|
-
.filter((message) => isAllowed(allow, "task-message", message.id))
|
|
286
283
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
287
284
|
});
|
|
288
285
|
}
|
|
@@ -334,7 +331,11 @@ export async function withContextObservations(core, providers, timeoutMs = 250)
|
|
|
334
331
|
}
|
|
335
332
|
return { ...result, omittedObservations: observations.length - result.observations.length };
|
|
336
333
|
}
|
|
337
|
-
/**
|
|
334
|
+
/** Authenticate ordinary reads of the local user's resource. Task/Assignment
|
|
335
|
+
* ownership is execution scope, not a confidentiality boundary. This does not
|
|
336
|
+
* switch Home, impersonate a target Role, or relax explicit global Job limits.
|
|
337
|
+
* Filesystem access and the authenticated Web/Controller transport remain the
|
|
338
|
+
* resource boundary of this single-user local product. */
|
|
338
339
|
export function resolveContextReader(store, environment) {
|
|
339
340
|
const caller = resolveManagedTaskReader(store, environment);
|
|
340
341
|
if (caller === undefined && environment.YUI_SESSION_SCOPE === "global" && environment.YUI_ROLE !== "operator") {
|
|
@@ -351,57 +352,14 @@ export function resolveContextReader(store, environment) {
|
|
|
351
352
|
}
|
|
352
353
|
function authorizeContext(store, taskId, environment) {
|
|
353
354
|
const caller = resolveContextReader(store, environment);
|
|
354
|
-
if (caller !== undefined && caller.taskId !== taskId)
|
|
355
|
-
throw usageError("Context is outside the caller's Task.");
|
|
356
355
|
const task = store.getTask(taskId);
|
|
357
356
|
if (task === null)
|
|
358
357
|
throw taskNotFound(taskId);
|
|
359
|
-
|
|
360
|
-
if (caller !== undefined && caller.roleName !== "leader") {
|
|
361
|
-
if (caller.currentRunId === undefined)
|
|
362
|
-
throw usageError("A managed Role needs a current AgentRun to read its scoped Context.");
|
|
363
|
-
const pack = buildRunContextPack(store, taskId, caller.currentRunId);
|
|
364
|
-
allow = new Set(pack.pointers.map((ref) => `${ref.store}:${ref.refId}`));
|
|
365
|
-
allow.add(`role:${caller.roleName}`);
|
|
366
|
-
allow.add(`turn:${caller.currentRunId}`);
|
|
367
|
-
// Shared current user intent and this exact Assignment's live steers augment
|
|
368
|
-
// the frozen refs. Query identities only: another Role's results, ordinary
|
|
369
|
-
// addressed messages and Leader steers remain outside this scope.
|
|
370
|
-
const run = store.getRun(taskId, caller.currentRunId);
|
|
371
|
-
const current = store.contextInputReferences(taskId, {
|
|
372
|
-
roleName: caller.roleName, workItemId: run?.workItemId, reviewRoundId: run?.reviewRoundId
|
|
373
|
-
});
|
|
374
|
-
for (const id of current.messages)
|
|
375
|
-
allow.add(`task-message:${id}`);
|
|
376
|
-
for (const id of current.events)
|
|
377
|
-
allow.add(`task-event:${id}`);
|
|
378
|
-
if (run?.workItemId !== undefined) {
|
|
379
|
-
for (const job of store.listDurableJobs(taskId)) {
|
|
380
|
-
if (job.owner.kind === "work-item" && job.owner.workItemId === run.workItemId)
|
|
381
|
-
allow.add(`job:${job.id}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return { task, allow, caller };
|
|
386
|
-
}
|
|
387
|
-
/** Independent record commands share Context's exact read boundary, not a
|
|
388
|
-
* second role-based approximation of Assignment visibility. */
|
|
389
|
-
export function contextRecordReader(store, taskId, environment = {}) {
|
|
390
|
-
const { allow } = authorizeContext(store, taskId, environment);
|
|
391
|
-
return (family, id) => isAllowed(allow, family, id);
|
|
392
|
-
}
|
|
393
|
-
export function assertContextRecordReadable(store, taskId, family, id, environment = {}) {
|
|
394
|
-
if (!contextRecordReader(store, taskId, environment)(family, id)) {
|
|
395
|
-
throw usageError("Context reference is outside the caller's Assignment.");
|
|
396
|
-
}
|
|
358
|
+
return { task, caller };
|
|
397
359
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
function allowedIds(allow, family) {
|
|
402
|
-
const prefix = `${family}:`;
|
|
403
|
-
return allow === undefined ? undefined : [...allow]
|
|
404
|
-
.filter(ref => ref.startsWith(prefix)).map(ref => ref.slice(prefix.length));
|
|
360
|
+
/** Independent record commands authenticate against the same resource boundary. */
|
|
361
|
+
export function assertTaskReadable(store, taskId, environment = {}) {
|
|
362
|
+
authorizeContext(store, taskId, environment);
|
|
405
363
|
}
|
|
406
364
|
function roleProfile(role) {
|
|
407
365
|
return {
|
|
@@ -413,12 +371,12 @@ function roleProfile(role) {
|
|
|
413
371
|
/** Resolve just the requested family. Families without a singular Store read
|
|
414
372
|
* use their existing list primitive, without constructing unrelated Context.
|
|
415
373
|
*/
|
|
416
|
-
function inspectValue(store, { task,
|
|
374
|
+
function inspectValue(store, { task, caller }, { store: family, refId }) {
|
|
417
375
|
const taskId = task.id;
|
|
418
376
|
switch (family) {
|
|
419
377
|
case "task": return refId === taskId ? task : null;
|
|
420
378
|
case "task-brief": return refId === taskId ? store.getTaskBrief(taskId) : null;
|
|
421
|
-
case "remote-delivery": return refId === taskId
|
|
379
|
+
case "remote-delivery": return refId === taskId
|
|
422
380
|
? projectTaskRemoteDeliveryFromStore(store, task) : null;
|
|
423
381
|
case "role": return store.getRole(taskId, refId);
|
|
424
382
|
case "role-profile": {
|
|
@@ -442,8 +400,10 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
442
400
|
}
|
|
443
401
|
case "input-request": return store.getInputRequest(taskId, refId);
|
|
444
402
|
case "work-item": return store.getWorkItem(taskId, refId);
|
|
445
|
-
case "accepted-work-item":
|
|
446
|
-
|
|
403
|
+
case "accepted-work-item": {
|
|
404
|
+
const item = store.getWorkItem(taskId, refId);
|
|
405
|
+
return item?.status === "accepted" ? item : null;
|
|
406
|
+
}
|
|
447
407
|
case "candidate": {
|
|
448
408
|
const parts = refId.split("/");
|
|
449
409
|
if (parts.length !== 2)
|
|
@@ -457,8 +417,6 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
457
417
|
return store.queryContextRecords(taskId, { family, ids: [refId], limit: 1 }).records[0]?.value ?? null;
|
|
458
418
|
case "run": return store.getRun(taskId, refId);
|
|
459
419
|
case "source-run": {
|
|
460
|
-
if (!allow?.has(`source-turn:${refId}`))
|
|
461
|
-
return null;
|
|
462
420
|
const run = store.getRun(taskId, refId);
|
|
463
421
|
return run === null ? null : sourceRunContextValue(run);
|
|
464
422
|
}
|
|
@@ -488,15 +446,13 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
488
446
|
case "managed-workspace": {
|
|
489
447
|
// Existing frozen AgentRun overlays use the Task/Role alias; owner keys
|
|
490
448
|
// identify the durable workspace in current Task reads.
|
|
491
|
-
if (caller?.currentRunId !== undefined && refId === `${taskId}/${caller.roleName}`) {
|
|
449
|
+
if (caller?.taskId === taskId && caller.currentRunId !== undefined && refId === `${taskId}/${caller.roleName}`) {
|
|
492
450
|
return store.getRun(taskId, caller.currentRunId)?.workspace ?? null;
|
|
493
451
|
}
|
|
494
452
|
return store.listManagedWorkspaces(taskId)
|
|
495
453
|
.find((workspace) => managedWorkspaceKey(workspace.owner) === refId) ?? null;
|
|
496
454
|
}
|
|
497
455
|
case "mailbox": {
|
|
498
|
-
if (allow !== undefined)
|
|
499
|
-
return null;
|
|
500
456
|
if (refId === "task")
|
|
501
457
|
return store.getWorkMailbox({ kind: "task", taskId });
|
|
502
458
|
const roleName = refId.startsWith("role:") ? refId.slice("role:".length) : undefined;
|
|
@@ -509,18 +465,18 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
509
465
|
default: return null;
|
|
510
466
|
}
|
|
511
467
|
}
|
|
512
|
-
function authorizedSources(store, { task,
|
|
468
|
+
function authorizedSources(store, { task, caller }, current = false, requestedFamily, discovery = false) {
|
|
513
469
|
const taskId = task.id;
|
|
514
470
|
const sources = [];
|
|
515
471
|
const add = (family, id, value) => {
|
|
516
|
-
if ((requestedFamily === undefined || requestedFamily === family) && value !== null
|
|
472
|
+
if ((requestedFamily === undefined || requestedFamily === family) && value !== null) {
|
|
517
473
|
sources.push({ family, count: 1, read: () => [materialize(family, id, value)] });
|
|
518
474
|
}
|
|
519
475
|
};
|
|
520
476
|
const query = (family, options = {}) => {
|
|
521
477
|
if (requestedFamily !== undefined && requestedFamily !== family)
|
|
522
478
|
return;
|
|
523
|
-
const selection = { ...options, family
|
|
479
|
+
const selection = { ...options, family };
|
|
524
480
|
const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
|
|
525
481
|
sources.push({ family, count, *read(offset = 0) {
|
|
526
482
|
for (const { id, value } of store.queryContextRecords(taskId, {
|
|
@@ -531,7 +487,7 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
|
|
|
531
487
|
};
|
|
532
488
|
add("task", taskId, task);
|
|
533
489
|
add("task-brief", taskId, store.getTaskBrief(taskId));
|
|
534
|
-
if (
|
|
490
|
+
if (requestedFamily === undefined || requestedFamily === "remote-delivery") {
|
|
535
491
|
add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
|
|
536
492
|
}
|
|
537
493
|
for (const role of store.listRoles(taskId)) {
|
|
@@ -552,9 +508,6 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
|
|
|
552
508
|
// Current responsibilities and unanswered inputs precede historical bulk.
|
|
553
509
|
query("input-request", { statuses: ["open"] });
|
|
554
510
|
query("work-item", current ? { excludeStatuses: ["accepted", "retired"] } : {});
|
|
555
|
-
for (const id of allowedIds(allow, "accepted-work-item") ?? []) {
|
|
556
|
-
add("accepted-work-item", id, store.getWorkItem(taskId, id));
|
|
557
|
-
}
|
|
558
511
|
if (!current)
|
|
559
512
|
query("candidate");
|
|
560
513
|
// File artifacts are not enumerated into the synchronous Context listing: the
|
|
@@ -565,22 +518,17 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
|
|
|
565
518
|
if (!current)
|
|
566
519
|
query("environment-preparation");
|
|
567
520
|
query("managed-workspace");
|
|
568
|
-
if (caller?.currentRunId !== undefined) {
|
|
521
|
+
if (caller?.taskId === taskId && caller.currentRunId !== undefined) {
|
|
569
522
|
add("managed-workspace", `${taskId}/${caller.roleName}`, store.getRun(taskId, caller.currentRunId)?.workspace ?? null);
|
|
570
523
|
}
|
|
571
524
|
if (!current)
|
|
572
525
|
query("change-set");
|
|
573
526
|
query("task-message", { descending: true, unhandledMessages: current });
|
|
574
527
|
query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
|
|
575
|
-
for (const id of allowedIds(allow, "source-turn") ?? []) {
|
|
576
|
-
const run = store.getRun(taskId, id);
|
|
577
|
-
if (run !== null)
|
|
578
|
-
add("source-run", id, sourceRunContextValue(run));
|
|
579
|
-
}
|
|
580
528
|
query("review-round", { descending: true, ...(current ? { statuses: ["pending", "running"] } : {}) });
|
|
581
529
|
if (!current)
|
|
582
530
|
query("input-request", { excludeStatuses: ["open"] });
|
|
583
|
-
if (!current
|
|
531
|
+
if (!current) {
|
|
584
532
|
add("mailbox", "task", store.getWorkMailbox({ kind: "task", taskId }));
|
|
585
533
|
for (const role of store.listRoles(taskId)) {
|
|
586
534
|
add("mailbox", `role:${role.name}`, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }));
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
2
|
import { TASK_SEARCH_KINDS } from "../storage/taskSearch.js";
|
|
3
3
|
import { contextContentDigest } from "./contextSnapshot.js";
|
|
4
|
-
import {
|
|
5
|
-
import { readTaskContextResource } from "./taskContext.js";
|
|
4
|
+
import { readTaskContextResource, resolveContextReader } from "./taskContext.js";
|
|
6
5
|
const PAGE_BYTES = 32 * 1024;
|
|
7
6
|
/** Human-readable field pages, using the same source authorization and version
|
|
8
7
|
* checks as Context. Offsets are Unicode code points, like search snippets. */
|
|
@@ -67,11 +66,8 @@ export function searchTaskBodies(store, options, environment = {}) {
|
|
|
67
66
|
...(options.kind === undefined ? [] : ["--kind", options.kind]),
|
|
68
67
|
...(options.cursor === undefined ? [] : ["--cursor", options.cursor])]);
|
|
69
68
|
return store.readTransaction(reader => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw usageError("Search target is outside the caller's Task.");
|
|
73
|
-
}
|
|
74
|
-
const taskId = callerTask ?? valid.taskId;
|
|
69
|
+
resolveContextReader(reader, environment);
|
|
70
|
+
const taskId = valid.taskId;
|
|
75
71
|
const scope = contextContentDigest({ taskId: taskId ?? null, query: valid.query, kind: valid.kind ?? null });
|
|
76
72
|
const after = valid.cursor === undefined ? undefined : decodeCursor(valid.cursor, scope);
|
|
77
73
|
const rows = reader.queryTaskSearch({ ...valid, taskId, after, limit: valid.limit + 1 });
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -14,14 +14,14 @@ import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurati
|
|
|
14
14
|
import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
|
|
15
15
|
import { discoveryCommandTree } from "./cli/commandDiscovery.js";
|
|
16
16
|
import { callerDiscoveryAudience } from "./cli/callerDiscovery.js";
|
|
17
|
-
import {
|
|
17
|
+
import { assertTaskReadable } from "./context/taskContext.js";
|
|
18
18
|
import { renderCompletion } from "./cli/completion.js";
|
|
19
19
|
import { runCompletionWizard } from "./cli/completionWizard.js";
|
|
20
20
|
import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
|
|
21
21
|
import { renderCommandHelp } from "./cli/helpRenderer.js";
|
|
22
22
|
import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
|
|
23
23
|
import { routeInvocation } from "./cli/invocationRouter.js";
|
|
24
|
-
import { operatorOfflineCommand,
|
|
24
|
+
import { operatorOfflineCommand, isTaskReadCommand } from "./cli/managedDiagnostics.js";
|
|
25
25
|
import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
|
|
26
26
|
import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
|
|
27
27
|
import { assertJobAgentCommand } from "./job/agentRoleScope.js";
|
|
@@ -1094,7 +1094,7 @@ export async function main() {
|
|
|
1094
1094
|
if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
|
|
1095
1095
|
throw usageError(usage);
|
|
1096
1096
|
}
|
|
1097
|
-
if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
|
|
1097
|
+
if (action === "save" && process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
|
|
1098
1098
|
throw usageError("Artifact is outside the managed Task scope.");
|
|
1099
1099
|
}
|
|
1100
1100
|
if (store.getTask(taskId) === null)
|
|
@@ -2065,12 +2065,7 @@ async function preflightManagedTaskControlPlane() {
|
|
|
2065
2065
|
&& ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
|
|
2066
2066
|
const home = resolveYuiHome(process.env);
|
|
2067
2067
|
assertManagedSessionManifest(home, "task");
|
|
2068
|
-
const
|
|
2069
|
-
const diagnostic = diagnosticTarget !== undefined
|
|
2070
|
-
&& diagnosticTarget === process.env.YUI_TASK_ID;
|
|
2071
|
-
if (diagnosticTarget !== undefined && !diagnostic) {
|
|
2072
|
-
throw usageError("Diagnostics must remain within this Session's Task.");
|
|
2073
|
-
}
|
|
2068
|
+
const diagnostic = isTaskReadCommand(args);
|
|
2074
2069
|
// One gate for every managed command: the current CLI, Home, and Controller
|
|
2075
2070
|
// must agree. Internal callbacks must still be able to append their immutable
|
|
2076
2071
|
// fact while the Controller is offline.
|
|
@@ -2087,13 +2082,6 @@ async function preflightManagedTaskControlPlane() {
|
|
|
2087
2082
|
: diagnostic
|
|
2088
2083
|
? resolveManagedTaskReader(verifiedStore, process.env)
|
|
2089
2084
|
: requireManagedTaskCaller(verifiedStore, process.env);
|
|
2090
|
-
if (diagnostic && runtime?.roleName !== "leader"
|
|
2091
|
-
&& !(args[1] === "show"
|
|
2092
|
-
|| (args[1] === "role" && args[2] === "session" && args[3] === "inspect"
|
|
2093
|
-
&& args[5] === runtime?.roleName))) {
|
|
2094
|
-
// Non-Leaders keep their Assignment-scoped read authorization.
|
|
2095
|
-
requireManagedTaskCaller(verifiedStore, process.env);
|
|
2096
|
-
}
|
|
2097
2085
|
const request = taskFinalReviewInvocation.request;
|
|
2098
2086
|
if (request !== undefined && diagnostic)
|
|
2099
2087
|
requireManagedTaskCaller(verifiedStore, process.env);
|
|
@@ -3079,7 +3067,7 @@ function completionSelectionPorts(home) {
|
|
|
3079
3067
|
if (process.env.YUI_SESSION_SCOPE !== "task")
|
|
3080
3068
|
return ports;
|
|
3081
3069
|
const taskId = process.env.YUI_TASK_ID;
|
|
3082
|
-
|
|
3070
|
+
assertTaskReadable(store, taskId, process.env);
|
|
3083
3071
|
const families = {
|
|
3084
3072
|
"task.list": "task", "project.list": "project-policy", "task.role.list": "role",
|
|
3085
3073
|
"task.work.list": "work-item", "task.message.list": "task-message", "task.input.list": "input-request",
|
|
@@ -3103,7 +3091,7 @@ function completionSelectionPorts(home) {
|
|
|
3103
3091
|
return false;
|
|
3104
3092
|
if (method === "project.list" && !store.getTask(taskId)?.projectBindings.some(binding => binding.projectId === id))
|
|
3105
3093
|
return false;
|
|
3106
|
-
return
|
|
3094
|
+
return true;
|
|
3107
3095
|
});
|
|
3108
3096
|
} };
|
|
3109
3097
|
}
|
|
@@ -2114,8 +2114,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2114
2114
|
&& existing.nativeSessionId !== input.session.nativeSessionId
|
|
2115
2115
|
&& existing.status === "active";
|
|
2116
2116
|
if (!defersConversationReplacement && (existing?.nativeSessionId !== input.session.nativeSessionId || existing.status !== "active")) {
|
|
2117
|
+
// Preparation reactivates the fixed native identity, not its name.
|
|
2118
|
+
// Preserve the stored title even for an ended Session; only the
|
|
2119
|
+
// naming receipt may advance it on resume.
|
|
2120
|
+
const { title, ...preparedSession } = input.session;
|
|
2117
2121
|
saveTaskSession(store, role, {
|
|
2118
|
-
...
|
|
2122
|
+
...preparedSession,
|
|
2123
|
+
...(existing?.nativeSessionId !== input.session.nativeSessionId && title !== undefined
|
|
2124
|
+
? { title } : {}),
|
|
2119
2125
|
nativeSessionId: input.session.nativeSessionId
|
|
2120
2126
|
}, "active", input.now);
|
|
2121
2127
|
}
|
|
@@ -2180,7 +2186,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2180
2186
|
recordLaunchedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
|
|
2181
2187
|
return this.store.transaction((store) => {
|
|
2182
2188
|
assertCurrent();
|
|
2183
|
-
|
|
2189
|
+
let session = input.owner.scope === "task"
|
|
2184
2190
|
? recordTaskRuntimeNativeSession(store, {
|
|
2185
2191
|
taskId: input.owner.taskId,
|
|
2186
2192
|
roleName: input.owner.roleName,
|
|
@@ -2196,6 +2202,20 @@ export class FileSchedulerStoreAdapter {
|
|
|
2196
2202
|
nativeSessionId: input.nativeSessionId,
|
|
2197
2203
|
effective: input.effective
|
|
2198
2204
|
}, now);
|
|
2205
|
+
if (input.generatedTitle !== undefined && (session.title === undefined
|
|
2206
|
+
|| session.title === session.generatedTitle || session.title === input.generatedTitle)) {
|
|
2207
|
+
const set = input.owner.scope === "task"
|
|
2208
|
+
? store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName)
|
|
2209
|
+
: store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
2210
|
+
const titled = recordRoleAgentSession(set, {
|
|
2211
|
+
...session, title: input.generatedTitle, generatedTitle: input.generatedTitle
|
|
2212
|
+
}, now);
|
|
2213
|
+
if (titled.owner.scope === "task")
|
|
2214
|
+
store.saveTaskRoleSessionSet(titled);
|
|
2215
|
+
else
|
|
2216
|
+
store.saveGlobalRoleSessionSet(titled);
|
|
2217
|
+
session = titled.sessions[input.agentId];
|
|
2218
|
+
}
|
|
2199
2219
|
if (input.owner.scope === "global") {
|
|
2200
2220
|
let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
2201
2221
|
let binding = sessions.providerBinding;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveJobCaller } from "../task/taskAuthority.js";
|
|
1
|
+
import { resolveJobCaller, resolveJobReadCaller } from "../task/taskAuthority.js";
|
|
2
2
|
import { callFileTaskController } from "./clientRuntime.js";
|
|
3
3
|
export async function startDurableJob(home, params, clientOptions = {}) {
|
|
4
4
|
// rr6/f2: Route through callFileTaskController so an Integration-issued
|
|
@@ -89,7 +89,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
|
|
|
89
89
|
return job;
|
|
90
90
|
},
|
|
91
91
|
async getJob(taskId, jobId) {
|
|
92
|
-
return getDurableJob(home, taskId, jobId,
|
|
92
|
+
return getDurableJob(home, taskId, jobId, resolveJobReadCaller(clientOptions.environment, taskId), clientOptions);
|
|
93
93
|
},
|
|
94
94
|
async cancelJob(taskId, jobId) {
|
|
95
95
|
// rr8/rr12: Bind the cancel request to the caller's managed identity.
|
|
@@ -18,7 +18,7 @@ import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
|
18
18
|
import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
|
|
19
19
|
import { CliError } from "../errors/cliError.js";
|
|
20
20
|
import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
|
|
21
|
-
import {
|
|
21
|
+
import { assertTaskReadable } from "../context/taskContext.js";
|
|
22
22
|
import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
|
|
23
23
|
import { validateHttpPreview } from "../job/httpPreview.js";
|
|
24
24
|
import { definitionFromTaskRun } from "../job/jobDefinition.js";
|
|
@@ -134,7 +134,7 @@ export function createDurableJobControl(store) {
|
|
|
134
134
|
if (caller === undefined)
|
|
135
135
|
throw jobControlError("UNAUTHORIZED", "Job reads require an explicit caller.");
|
|
136
136
|
try {
|
|
137
|
-
|
|
137
|
+
assertTaskReadable(store, taskId, caller.scope === "user" ? {} : {
|
|
138
138
|
YUI_SESSION_SCOPE: caller.scope, YUI_TASK_ID: caller.taskId, YUI_ROLE: caller.role,
|
|
139
139
|
YUI_AGENT_ID: caller.agentId, YUI_ADAPTER_ID: caller.adapterId, YUI_NATIVE_SESSION_ID: caller.nativeSessionId
|
|
140
140
|
});
|
|
@@ -137,7 +137,7 @@ export function authorizeRunRead(store, run, caller) {
|
|
|
137
137
|
throw new Error("Task delegation is not an Assignment grant.");
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
-
//
|
|
140
|
+
// Ordinary process-Run evidence does not transfer its execution Assignment.
|
|
141
141
|
if ("taskId" in run)
|
|
142
142
|
createDurableJobControl(store).getJob(run.taskId, run.id, caller);
|
|
143
143
|
}
|
|
@@ -168,6 +168,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
|
|
|
168
168
|
const same = runs.find(run => run.trigger.key === trigger.key && run.trigger.source === trigger.source);
|
|
169
169
|
if (same !== undefined) {
|
|
170
170
|
authorizeRunRead(tx, same, caller);
|
|
171
|
+
if ("taskId" in same && !same.delegation)
|
|
172
|
+
requireJobAssignment(tx, same, caller);
|
|
171
173
|
if (same.definitionRevision !== job.revision || same.retryOf !== retryOf) {
|
|
172
174
|
throw new Error(`JobRun trigger conflicts with its original snapshot: ${same.id}.`);
|
|
173
175
|
}
|
|
@@ -178,6 +180,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
|
|
|
178
180
|
|| run.status === "unknown-needs-attention");
|
|
179
181
|
if (occupied !== undefined) {
|
|
180
182
|
authorizeRunRead(tx, occupied, caller);
|
|
183
|
+
if ("taskId" in occupied && !occupied.delegation)
|
|
184
|
+
requireJobAssignment(tx, occupied, caller);
|
|
181
185
|
return { run: occupied, created: false, reason: "overlap" };
|
|
182
186
|
}
|
|
183
187
|
if (retryOf !== undefined && !runs.some(run => run.id === retryOf
|
|
@@ -69,6 +69,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
let preflightObserved = beforeHostStart === undefined;
|
|
72
|
+
let generatedTitle;
|
|
72
73
|
const observePreflight = (preflight) => {
|
|
73
74
|
if (preflightObserved || !sameOwner(preflight.owner, request.owner)
|
|
74
75
|
|| preflight.runId !== request.runId
|
|
@@ -80,6 +81,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
80
81
|
}
|
|
81
82
|
assertLaunchCurrent();
|
|
82
83
|
preflightObserved = true;
|
|
84
|
+
generatedTitle = preflight.generatedTitle;
|
|
83
85
|
beforeHostStart?.(preflight);
|
|
84
86
|
};
|
|
85
87
|
const shared = {
|
|
@@ -94,7 +96,9 @@ export class RuntimeLaunchCoordinator {
|
|
|
94
96
|
};
|
|
95
97
|
// Even without a startup-facts consumer, recheck after asynchronous Host
|
|
96
98
|
// validation and immediately before creating the process.
|
|
97
|
-
const callback = beforeHostStart === undefined
|
|
99
|
+
const callback = beforeHostStart === undefined
|
|
100
|
+
? (preflight) => { assertLaunchCurrent(); generatedTitle = preflight.generatedTitle; }
|
|
101
|
+
: observePreflight;
|
|
98
102
|
if (request.mode === "resume" && !request.nativeSessionId) {
|
|
99
103
|
throw new Error("Restoring a Session requires its native session id.");
|
|
100
104
|
}
|
|
@@ -109,11 +113,18 @@ export class RuntimeLaunchCoordinator {
|
|
|
109
113
|
throw new Error("Session host returned facts inconsistent with the requested launch.");
|
|
110
114
|
}
|
|
111
115
|
if (binding.nativeSessionId !== undefined) {
|
|
116
|
+
// A live Host may reuse its Provider without applying the new plan.
|
|
117
|
+
// Codex reports the actual naming effect; other adapters can only name
|
|
118
|
+
// at creation and must not overwrite native user names on resume.
|
|
119
|
+
const recordedTitle = request.adapterId === "codex"
|
|
120
|
+
? binding.appliedGeneratedTitle
|
|
121
|
+
: request.mode === "new" ? generatedTitle : undefined;
|
|
112
122
|
this.sessions.recordLaunchedRuntimeNativeSession({
|
|
113
123
|
owner: request.owner,
|
|
114
124
|
agentId: request.agentId,
|
|
115
125
|
adapterId: request.adapterId,
|
|
116
126
|
nativeSessionId: binding.nativeSessionId,
|
|
127
|
+
...(recordedTitle === undefined ? {} : { generatedTitle: recordedTitle }),
|
|
117
128
|
effective
|
|
118
129
|
}, assertLaunchCurrent, this.options.now?.() ?? new Date());
|
|
119
130
|
}
|
|
@@ -111,6 +111,8 @@ export function recordRoleAgentSession(set, input, now) {
|
|
|
111
111
|
adapterId,
|
|
112
112
|
nativeSessionId,
|
|
113
113
|
...optionalSessionText("title", input.title ?? continuing?.title),
|
|
114
|
+
...((input.generatedTitle ?? continuing?.generatedTitle) === undefined ? {}
|
|
115
|
+
: { generatedTitle: input.generatedTitle ?? continuing?.generatedTitle }),
|
|
114
116
|
...optionalSessionText("preview", input.preview ?? continuing?.preview),
|
|
115
117
|
policy: input.policy,
|
|
116
118
|
effective: continuing?.effective ?? effective,
|
|
@@ -567,6 +569,11 @@ export function validateRoleAgentSession(session, expectedAgentId = session.agen
|
|
|
567
569
|
throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
|
|
568
570
|
}
|
|
569
571
|
requireText(session.nativeSessionId, "Native session id");
|
|
572
|
+
if (session.generatedTitle !== undefined
|
|
573
|
+
&& (typeof session.generatedTitle !== "string" || session.generatedTitle.length > 80
|
|
574
|
+
|| optionalSessionText("title", session.generatedTitle).title !== session.generatedTitle)) {
|
|
575
|
+
throw new Error("Role Agent generated title is invalid.");
|
|
576
|
+
}
|
|
570
577
|
if (session.title !== undefined
|
|
571
578
|
&& optionalSessionText("title", session.title).title !== session.title) {
|
|
572
579
|
throw new Error("Role Agent session title is invalid.");
|
|
@@ -17,7 +17,7 @@ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
|
17
17
|
import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
|
|
18
18
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
19
19
|
import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
|
|
20
|
-
import {
|
|
20
|
+
import { globalRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
21
21
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
22
22
|
import { planningRuntimeCwd } from "../storage/homeLayout.js";
|
|
23
23
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
@@ -220,7 +220,11 @@ export class FileRoleLaunchPlanner {
|
|
|
220
220
|
// attempt was rejected, cancelled, or completed.
|
|
221
221
|
assertProviderConversationReplaceable(sessionSet.providerBinding);
|
|
222
222
|
}
|
|
223
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name
|
|
223
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name, {
|
|
224
|
+
executionAuthority: (input.mode === "resume" ? existing?.effective : effective)?.executionAuthority,
|
|
225
|
+
instanceCode: role.instanceCode,
|
|
226
|
+
generatedTitle: existing?.generatedTitle
|
|
227
|
+
}), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
|
|
224
228
|
purpose
|
|
225
229
|
});
|
|
226
230
|
}
|
|
@@ -249,9 +253,12 @@ export class FileRoleLaunchPlanner {
|
|
|
249
253
|
if (input.mode === "new" && sessionSet?.providerBinding != null) {
|
|
250
254
|
assertProviderConversationReplaceable(sessionSet.providerBinding);
|
|
251
255
|
}
|
|
252
|
-
return this.#compile(role, input, { scope: "global" }, input.mode === "new"
|
|
253
|
-
?
|
|
254
|
-
:
|
|
256
|
+
return this.#compile(role, input, { scope: "global" }, input.mode === "new"
|
|
257
|
+
? globalRoleSessionTitle(role.name, this.#now(), this.store.getConfig().timeZone, role.instanceCode)
|
|
258
|
+
: existing?.generatedTitle !== undefined && existing.title === existing.generatedTitle
|
|
259
|
+
? globalRoleSessionTitle(role.name, new Date(existing.createdAt), this.store.getConfig().timeZone, role.instanceCode)
|
|
260
|
+
?? `Yui ${role.name}`
|
|
261
|
+
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
|
|
255
262
|
}
|
|
256
263
|
#compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
|
|
257
264
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
@@ -385,7 +392,9 @@ export class FileRoleLaunchPlanner {
|
|
|
385
392
|
agent,
|
|
386
393
|
config: effectiveConfig,
|
|
387
394
|
workspace: agentWorkspace,
|
|
388
|
-
|
|
395
|
+
// CLI --name has no ownership check on resume. Only the Codex native
|
|
396
|
+
// metadata path below may rename an existing conversation.
|
|
397
|
+
...(sessionTitle === undefined || postCreateOperatorTitle || input.mode === "resume"
|
|
389
398
|
? {}
|
|
390
399
|
: { sessionTitle }),
|
|
391
400
|
...sessionContext
|
|
@@ -510,6 +519,9 @@ export class FileRoleLaunchPlanner {
|
|
|
510
519
|
mode: "resume",
|
|
511
520
|
nativeSessionId: requireText(providerNativeSessionId, "Managed Provider restore native session id"),
|
|
512
521
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
522
|
+
...(existingSession?.generatedTitle !== undefined
|
|
523
|
+
&& existingSession.title === existingSession.generatedTitle
|
|
524
|
+
? { previousSessionTitle: existingSession.generatedTitle } : {}),
|
|
513
525
|
...(managedCompiled.codexThread === undefined
|
|
514
526
|
? {}
|
|
515
527
|
: { codexThread: managedCompiled.codexThread }),
|
|
@@ -594,6 +606,10 @@ export class FileRoleLaunchPlanner {
|
|
|
594
606
|
},
|
|
595
607
|
launch: ordinaryConversationLaunch,
|
|
596
608
|
session,
|
|
609
|
+
...(sessionTitle !== undefined && (input.mode === "new"
|
|
610
|
+
|| existingSession?.title === undefined
|
|
611
|
+
|| existingSession.title === existingSession.generatedTitle)
|
|
612
|
+
? { generatedTitle: sessionTitle } : {}),
|
|
597
613
|
...(sessionTitle === undefined ? {} : { sessionTitle })
|
|
598
614
|
};
|
|
599
615
|
}
|