@awak-app/simy-cli 0.7.17 → 0.7.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli-contract.js +3 -1
- package/src/desktop-host.js +31 -4
- package/src/durable-local-task-steps-contract.js +19 -0
- package/src/durable-local-task-worker.js +160 -24
- package/src/provider-delegation.js +11 -1
- package/src/session-supervision/claude-sessions.js +5 -3
- package/src/session-supervision/codex-app-server.js +1 -0
- package/src/session-supervision/discovery-window.js +1 -1
- package/src/session-supervision/index.js +3 -3
- package/src/session-supervision/remote-hosts.js +1 -1
- package/src/team-threads.js +37 -16
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
LOCAL_TASK_STEP_CONTEXT_SCHEMA,
|
|
11
11
|
LOCAL_TASK_STEP_SCHEMA,
|
|
12
12
|
MAX_LOCAL_TASK_PLAN_STEPS,
|
|
13
|
+
WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY,
|
|
13
14
|
} from "./durable-local-task-steps-contract.js";
|
|
14
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
16
|
|
|
16
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.19";
|
|
17
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
19
|
|
|
19
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -43,6 +44,7 @@ export function withCliContract(
|
|
|
43
44
|
plan_schema_version: LOCAL_TASK_PLAN_SCHEMA,
|
|
44
45
|
step_schema_version: LOCAL_TASK_STEP_SCHEMA,
|
|
45
46
|
context_schema_version: LOCAL_TASK_STEP_CONTEXT_SCHEMA,
|
|
47
|
+
workflow_artifact_delivery: WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY,
|
|
46
48
|
max_steps: MAX_LOCAL_TASK_PLAN_STEPS,
|
|
47
49
|
},
|
|
48
50
|
execution_capabilities: buildExecutionCapabilityContract({
|
package/src/desktop-host.js
CHANGED
|
@@ -773,7 +773,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
773
773
|
for (const session of status.sessionSupervision.sessions) {
|
|
774
774
|
const key = teamThreadKey(agent.getTeamThreadsScope?.() || {}, session);
|
|
775
775
|
const own = status.teamThreads?.own.find(item => item.threadKey === key);
|
|
776
|
-
session.teamSharing = { enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true };
|
|
776
|
+
session.teamSharing = { threadKey: key, enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true, confirmationRequired: status.teamThreads?.confirmationRequired.includes(key) === true };
|
|
777
777
|
}
|
|
778
778
|
const latestAccess = await accessSnapshot();
|
|
779
779
|
if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks ||
|
|
@@ -1138,9 +1138,20 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1138
1138
|
else if(action === 'team_thread_open') ok = await teamThreads.open({memberId:command.memberId,threadKey:command.threadKey});
|
|
1139
1139
|
else {
|
|
1140
1140
|
if (command.organizationId !== undefined && command.organizationId !== access.organizationId) throw new Error('Sharing organization changed. Confirm again.');
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1141
|
+
// Publication reads this host's provider context. Never reinterpret
|
|
1142
|
+
// a remote or ambiguous identity as a same-ID local conversation.
|
|
1143
|
+
if ((command.hostId && command.hostId !== 'local') || (!command.hostId && remoteHosts?.hasSession(command.session))) {
|
|
1144
|
+
throw new Error('Team sharing requires an explicitly selected local conversation.');
|
|
1145
|
+
}
|
|
1146
|
+
const selected = sessionStatus.sessions.find(item => (!item.hostId || item.hostId === 'local') && item.provider === command.provider && item.session === command.session);
|
|
1147
|
+
if (command.enabled === false) {
|
|
1148
|
+
// Revocation uses authenticated own rows, never provider context.
|
|
1149
|
+
const key = command.threadKey ?? teamThreadKey(agent.getTeamThreadsScope(), { provider: command.provider, session: command.session });
|
|
1150
|
+
ok = await teamThreads.stopSharing(key);
|
|
1151
|
+
} else {
|
|
1152
|
+
if(!selected || command.threadKey) throw new Error('Refresh and select your conversation again.');
|
|
1153
|
+
ok = await teamThreads.setSharing(selected,command.enabled);
|
|
1154
|
+
}
|
|
1144
1155
|
}
|
|
1145
1156
|
emit({type:'result',id:command.id,ok:ok===true}); void publish().catch(() => {}); return;
|
|
1146
1157
|
}
|
|
@@ -1457,6 +1468,22 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1457
1468
|
await authorizeWork(commandScope);
|
|
1458
1469
|
const result = await deadlines.decide(context, { ...command, requestId: command.requestId || command.decisionId });
|
|
1459
1470
|
if (result.status === 'conflict') throw Object.assign(new Error('Deadline changed; refresh before deciding.'), { code: 'conflict' });
|
|
1471
|
+
} else if (action === "session_set_pinned") {
|
|
1472
|
+
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether to pin this conversation.');
|
|
1473
|
+
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1474
|
+
await authorizeWork(commandScope);
|
|
1475
|
+
const pinScope = command.pinScope ?? 'legacy';
|
|
1476
|
+
if (typeof pinScope !== 'string' || !pinScope || pinScope.length > 512) throw new Error('Invalid pin scope.');
|
|
1477
|
+
await updateRecord(deliveryRoot, command.provider, command.session, previous => {
|
|
1478
|
+
const scopes = new Set(Array.isArray(previous?.listPinScopes) ? previous.listPinScopes : previous?.listPinned ? ['legacy'] : []);
|
|
1479
|
+
if (command.enabled) scopes.add(pinScope); else scopes.delete(pinScope);
|
|
1480
|
+
return {
|
|
1481
|
+
...(previous || { schemaVersion: 1, provider: command.provider, session: command.session, cwd: candidate.cwd, observedAt: Date.now() }),
|
|
1482
|
+
listPinned: scopes.size > 0, listPinScopes: [...scopes],
|
|
1483
|
+
...(command.enabled ? { listMetadata: { title: candidate.title || '', createdAt: candidate.createdAt ?? null, lastActivityAt: candidate.lastActivityAt ?? null } } : {}),
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
refreshSessions(true);
|
|
1460
1487
|
} else if (action === "session_archive" || action === "session_restore") {
|
|
1461
1488
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1462
1489
|
await authorizeWork(commandScope);
|
|
@@ -20,6 +20,11 @@ export const LOCAL_TASK_PLAN_ORCHESTRATION_SCHEMA =
|
|
|
20
20
|
export const LOCAL_TASK_PLAN_SCHEMA = "local_task_plan.v1";
|
|
21
21
|
export const LOCAL_TASK_STEP_SCHEMA = "local_task_step.v1";
|
|
22
22
|
export const LOCAL_TASK_STEP_CONTEXT_SCHEMA = "local_task_step_context.v1";
|
|
23
|
+
// This optional field stays on the existing closed claim schema. Backend only
|
|
24
|
+
// emits it to devices that advertise the matching capability, so older strict
|
|
25
|
+
// clients never receive a field they cannot parse.
|
|
26
|
+
export const WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY =
|
|
27
|
+
"workflow_local_task_artifact.v1";
|
|
23
28
|
export const MAX_LOCAL_TASK_PLAN_STEPS = 8;
|
|
24
29
|
|
|
25
30
|
const UUID_PATTERN =
|
|
@@ -254,6 +259,9 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
254
259
|
"lease_expires_at",
|
|
255
260
|
"claim_replayed",
|
|
256
261
|
...(Object.hasOwn(claim, "model") ? ["model"] : []),
|
|
262
|
+
...(Object.hasOwn(claim, "workflow_artifact_delivery")
|
|
263
|
+
? ["workflow_artifact_delivery"]
|
|
264
|
+
: []),
|
|
257
265
|
]),
|
|
258
266
|
"claim",
|
|
259
267
|
);
|
|
@@ -264,6 +272,15 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
264
272
|
fail("Unsupported durable Local Task step disposition.");
|
|
265
273
|
}
|
|
266
274
|
const model = claim.model == null ? null : requiredString(claim.model, "model", 160);
|
|
275
|
+
const workflowArtifactDelivery = claim.workflow_artifact_delivery === undefined
|
|
276
|
+
? false
|
|
277
|
+
: claim.workflow_artifact_delivery === WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY;
|
|
278
|
+
if (
|
|
279
|
+
claim.workflow_artifact_delivery !== undefined &&
|
|
280
|
+
!workflowArtifactDelivery
|
|
281
|
+
) {
|
|
282
|
+
fail("Unsupported durable Local Task WorkFlow artifact delivery contract.");
|
|
283
|
+
}
|
|
267
284
|
const turnId = requiredUuid(claim.turn_id, "turn_id");
|
|
268
285
|
const plan = normalizePlan(claim.plan);
|
|
269
286
|
const currentStepId = requiredStepId(claim.current_step_id, "current_step_id");
|
|
@@ -309,6 +326,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
309
326
|
leaseEpoch: null,
|
|
310
327
|
leaseExpiresAtMs: null,
|
|
311
328
|
claimReplayed: false,
|
|
329
|
+
workflowArtifactDelivery,
|
|
312
330
|
};
|
|
313
331
|
}
|
|
314
332
|
|
|
@@ -366,6 +384,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
366
384
|
leaseEpoch,
|
|
367
385
|
leaseExpiresAtMs,
|
|
368
386
|
claimReplayed: claim.claim_replayed,
|
|
387
|
+
workflowArtifactDelivery,
|
|
369
388
|
remainingProviderTokens: remainingTokens,
|
|
370
389
|
remainingTimeoutMs: remainingMs,
|
|
371
390
|
};
|
|
@@ -361,6 +361,19 @@ export function createDurableLocalTaskApi({
|
|
|
361
361
|
{ ambiguousOnTransport: true },
|
|
362
362
|
);
|
|
363
363
|
},
|
|
364
|
+
async prepareWorkflowArtifact({ turnId, workerId, leaseEpoch, localArtifactId, name, mimeType, sizeBytes, sha256 }) {
|
|
365
|
+
return request(`local-tasks/${encodeURIComponent(turnId)}/workflow-artifacts/prepare`, {
|
|
366
|
+
turn_id: turnId, worker_id: workerId, lease_epoch: leaseEpoch,
|
|
367
|
+
local_artifact_id: localArtifactId, name, mime_type: mimeType,
|
|
368
|
+
size_bytes: sizeBytes, sha256,
|
|
369
|
+
}, { ambiguousOnTransport: true });
|
|
370
|
+
},
|
|
371
|
+
async commitWorkflowArtifact({ uploadId, turnId, workerId, leaseEpoch, localArtifactId, sha256, sizeBytes }) {
|
|
372
|
+
return request(`local-tasks/${encodeURIComponent(turnId)}/workflow-artifacts/commit`, {
|
|
373
|
+
upload_id: uploadId, turn_id: turnId, worker_id: workerId, lease_epoch: leaseEpoch,
|
|
374
|
+
local_artifact_id: localArtifactId, sha256, size_bytes: sizeBytes,
|
|
375
|
+
}, { ambiguousOnTransport: true });
|
|
376
|
+
},
|
|
364
377
|
async heartbeatStep(input) {
|
|
365
378
|
const contract = stepContract.heartbeat(input);
|
|
366
379
|
const payload = await request(
|
|
@@ -677,6 +690,7 @@ export function createDurableLocalTaskWorker({
|
|
|
677
690
|
persist = persistLocalTask,
|
|
678
691
|
stopTask = stopLocalTask,
|
|
679
692
|
materializeArtifacts = materializePipelineArtifacts,
|
|
693
|
+
materializeWorkflowArtifacts = materializeWorkflowLocalTaskArtifacts,
|
|
680
694
|
attachmentStore = null,
|
|
681
695
|
getDeviceId = () => null,
|
|
682
696
|
readPhotoSource = readLocalPhotoSource,
|
|
@@ -996,10 +1010,6 @@ export function createDurableLocalTaskWorker({
|
|
|
996
1010
|
if (!await checkLease()) return;
|
|
997
1011
|
await runHeartbeat();
|
|
998
1012
|
if (leaseLost) return;
|
|
999
|
-
if (heartbeatTimer) {
|
|
1000
|
-
clearIntervalImpl(heartbeatTimer);
|
|
1001
|
-
heartbeatTimer = null;
|
|
1002
|
-
}
|
|
1003
1013
|
const status = completed.status === "stopped"
|
|
1004
1014
|
? "cancelled"
|
|
1005
1015
|
: boundary === "unknown"
|
|
@@ -1017,25 +1027,59 @@ export function createDurableLocalTaskWorker({
|
|
|
1017
1027
|
status,
|
|
1018
1028
|
reportFailureCode,
|
|
1019
1029
|
);
|
|
1030
|
+
const workflowArtifactDelivery =
|
|
1031
|
+
normalized.workflowArtifactDelivery ||
|
|
1032
|
+
context.workflowArtifactDelivery === true;
|
|
1020
1033
|
let artifactRefs = [];
|
|
1021
1034
|
try {
|
|
1022
1035
|
artifactRefs = buildLocalTaskArtifactRefs(
|
|
1023
1036
|
completed.artifacts,
|
|
1024
1037
|
completed.id,
|
|
1025
1038
|
);
|
|
1039
|
+
// Durable step claims intentionally have a closed contract and do not
|
|
1040
|
+
// repeat the parent execution spec. Preserve the parent-scoped
|
|
1041
|
+
// WorkFlow delivery contract in the same worker context so each
|
|
1042
|
+
// step's verified bytes are materialized before its terminal receipt.
|
|
1043
|
+
if (
|
|
1044
|
+
reportStatus === "succeeded" &&
|
|
1045
|
+
workflowArtifactDelivery &&
|
|
1046
|
+
artifactRefs.length > 0
|
|
1047
|
+
) {
|
|
1048
|
+
artifactRefs = await materializeWorkflowArtifacts({
|
|
1049
|
+
api,
|
|
1050
|
+
turnId: normalized.turnId,
|
|
1051
|
+
task: completed,
|
|
1052
|
+
artifactRefs,
|
|
1053
|
+
workerId,
|
|
1054
|
+
leaseEpoch: normalized.leaseEpoch,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1026
1057
|
} catch (error) {
|
|
1027
1058
|
artifactRefs = [];
|
|
1028
1059
|
if (status !== "cancelled") {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1060
|
+
const contractFailure = error instanceof LocalTaskArtifactContractError;
|
|
1061
|
+
const ambiguousDelivery =
|
|
1062
|
+
workflowArtifactDelivery &&
|
|
1063
|
+
(error?.ambiguous === true || error?.status >= 500);
|
|
1064
|
+
reportStatus = status === "unknown" || ambiguousDelivery
|
|
1065
|
+
? "unknown"
|
|
1066
|
+
: "failed";
|
|
1067
|
+
reportFailureCode = contractFailure
|
|
1068
|
+
? "artifact_verification_failed"
|
|
1069
|
+
: "artifact_materialization_failed";
|
|
1070
|
+
reportSummary = workflowArtifactDelivery && ambiguousDelivery
|
|
1071
|
+
? workflowArtifactRecoverySummary()
|
|
1072
|
+
: reportFailureCode === "artifact_verification_failed"
|
|
1073
|
+
? "SIMY could not safely verify this durable step's output files."
|
|
1074
|
+
: "SIMY created the durable step output files locally but could not safely attach them to this WorkFlow run.";
|
|
1033
1075
|
completed.status =
|
|
1034
1076
|
reportStatus === "failed" ? "failed" : completed.status;
|
|
1035
1077
|
completed.reason_code = reportFailureCode;
|
|
1036
1078
|
completed.error = error.message;
|
|
1037
|
-
|
|
1038
|
-
|
|
1079
|
+
if (contractFailure) {
|
|
1080
|
+
completed.artifacts = [];
|
|
1081
|
+
if (completed.result) completed.result.artifacts = [];
|
|
1082
|
+
}
|
|
1039
1083
|
await persist(completed);
|
|
1040
1084
|
}
|
|
1041
1085
|
onError(
|
|
@@ -1450,17 +1494,16 @@ export function createDurableLocalTaskWorker({
|
|
|
1450
1494
|
deliveryStage = "artifacts";
|
|
1451
1495
|
if (
|
|
1452
1496
|
(reportStatus === "succeeded" || native) &&
|
|
1453
|
-
(normalized.pipelineContext || normalized.narrativeImageRequest || native) &&
|
|
1497
|
+
(normalized.pipelineContext || normalized.workflowArtifactDelivery || normalized.narrativeImageRequest || native) &&
|
|
1454
1498
|
artifactRefs.length > 0
|
|
1455
1499
|
) {
|
|
1456
|
-
artifactRefs =
|
|
1457
|
-
api,
|
|
1458
|
-
|
|
1459
|
-
task: completed,
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
});
|
|
1500
|
+
artifactRefs = normalized.workflowArtifactDelivery
|
|
1501
|
+
? await materializeWorkflowArtifacts({ api, turnId: normalized.turnId, task: completed,
|
|
1502
|
+
artifactRefs, workerId, leaseEpoch: normalized.leaseEpoch })
|
|
1503
|
+
: await materializeArtifacts({ api, turnId: normalized.turnId, task: completed, artifactRefs,
|
|
1504
|
+
...(native ? { preserveAmbiguity: true, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
|
|
1505
|
+
...(normalized.narrativeImageRequest ? { nativeImageReceipt: completed.native_image_receipt, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
|
|
1506
|
+
});
|
|
1464
1507
|
}
|
|
1465
1508
|
} catch (error) {
|
|
1466
1509
|
const contractFailure = error instanceof LocalTaskArtifactContractError;
|
|
@@ -1471,7 +1514,7 @@ export function createDurableLocalTaskWorker({
|
|
|
1471
1514
|
if (status !== "cancelled") {
|
|
1472
1515
|
// A gateway 5xx can hide a committed image. Never present that outcome
|
|
1473
1516
|
// as a clean failure that invites a second generation.
|
|
1474
|
-
reportStatus = status === "unknown" || ((normalized.narrativeImageRequest || native) &&
|
|
1517
|
+
reportStatus = status === "unknown" || ((normalized.narrativeImageRequest || native || normalized.workflowArtifactDelivery) &&
|
|
1475
1518
|
(error?.ambiguous === true || error?.status >= 500)) ? "unknown" : "failed";
|
|
1476
1519
|
reportFailureCode = normalized.narrativeImageRequest && error?.code === "narrative_image_receipt_missing"
|
|
1477
1520
|
? "narrative_image_receipt_missing"
|
|
@@ -1483,6 +1526,8 @@ export function createDurableLocalTaskWorker({
|
|
|
1483
1526
|
reportSummary =
|
|
1484
1527
|
native ? recoverySummary(reportStatus, reportFailureCode) : normalized.narrativeImageRequest
|
|
1485
1528
|
? "SIMY could not confirm the generated image delivery. Keep the local output for verification; do not regenerate it."
|
|
1529
|
+
: normalized.workflowArtifactDelivery && reportStatus === "unknown"
|
|
1530
|
+
? workflowArtifactRecoverySummary()
|
|
1486
1531
|
: deliveryStage === "destination"
|
|
1487
1532
|
? "SIMY could not confirm delivery to the selected local folder. Existing files and local outputs were preserved."
|
|
1488
1533
|
: contractFailure
|
|
@@ -1507,7 +1552,7 @@ export function createDurableLocalTaskWorker({
|
|
|
1507
1552
|
reportFailureCode,
|
|
1508
1553
|
)
|
|
1509
1554
|
: workerError(
|
|
1510
|
-
normalized.narrativeImageRequest
|
|
1555
|
+
normalized.narrativeImageRequest || (normalized.workflowArtifactDelivery && reportStatus === "unknown")
|
|
1511
1556
|
? reportSummary
|
|
1512
1557
|
: `SIMY could not persist the Local Task output files for this Pipeline run. ${errorMessage(error)} Keep SIMY CLI running, check the connection, and start a new Pipeline run.`,
|
|
1513
1558
|
reportFailureCode,
|
|
@@ -1787,11 +1832,18 @@ export function createDurableLocalTaskWorker({
|
|
|
1787
1832
|
releaseWork();
|
|
1788
1833
|
return false;
|
|
1789
1834
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1835
|
+
// An orchestration envelope only authorizes the CLI to submit a
|
|
1836
|
+
// controlled plan. It does not dispatch a Provider. Its follow-up
|
|
1837
|
+
// may be the existing single-provider native claim, but must never
|
|
1838
|
+
// become a durable step: executeStepClaim has no native delegation
|
|
1839
|
+
// receipt contract.
|
|
1840
|
+
const orchestrationCarriesNativeDelegation = isLocalTaskPlanOrchestration(claim) &&
|
|
1841
|
+
Boolean(claim.execution_spec?.delegation || claim.execution_spec?.delegation_target || claim.execution_spec?.native_identity);
|
|
1793
1842
|
if (isLocalTaskPlanOrchestration(claim)) {
|
|
1794
1843
|
const orchestration = claim;
|
|
1844
|
+
context.workflowArtifactDelivery =
|
|
1845
|
+
orchestration.execution_spec?.workflow_artifact_delivery ===
|
|
1846
|
+
"workflow_local_task_artifact.v1";
|
|
1795
1847
|
const controlledPlan = buildControlledLocalTaskPlan(orchestration);
|
|
1796
1848
|
await prepareDurableStepAttachments({
|
|
1797
1849
|
orchestration,
|
|
@@ -1820,6 +1872,9 @@ export function createDurableLocalTaskWorker({
|
|
|
1820
1872
|
"local_task_plan_claim_mismatch",
|
|
1821
1873
|
);
|
|
1822
1874
|
}
|
|
1875
|
+
if (orchestrationCarriesNativeDelegation && isDurableLocalTaskStepClaim(claim)) {
|
|
1876
|
+
throw workerError('Native delegation requires the admitted single provider turn.', 'native_delegation_execution_mode_unsupported');
|
|
1877
|
+
}
|
|
1823
1878
|
}
|
|
1824
1879
|
if (requestedTurnId && claim.turn_id !== requestedTurnId) {
|
|
1825
1880
|
throw workerError(
|
|
@@ -2042,6 +2097,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
|
|
|
2042
2097
|
throw workerError("Unsupported narrative image execution scope.", "invalid_narrative_image_execution_scope");
|
|
2043
2098
|
}
|
|
2044
2099
|
const pipelineContext = normalizePipelineContext(spec);
|
|
2100
|
+
const workflowArtifactDelivery = spec.workflow_artifact_delivery === "workflow_local_task_artifact.v1";
|
|
2045
2101
|
const scenarioPack = normalizeLocalTaskScenarioPack(spec.scenario_pack);
|
|
2046
2102
|
return {
|
|
2047
2103
|
turnId,
|
|
@@ -2052,6 +2108,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
|
|
|
2052
2108
|
uploadedRefs,
|
|
2053
2109
|
spec,
|
|
2054
2110
|
pipelineContext,
|
|
2111
|
+
workflowArtifactDelivery,
|
|
2055
2112
|
scenarioPack,
|
|
2056
2113
|
narrativeImageRequest,
|
|
2057
2114
|
};
|
|
@@ -2724,6 +2781,61 @@ export async function materializePipelineArtifacts({
|
|
|
2724
2781
|
return materialized;
|
|
2725
2782
|
}
|
|
2726
2783
|
|
|
2784
|
+
export async function materializeWorkflowLocalTaskArtifacts({
|
|
2785
|
+
api, turnId, task, artifactRefs, workerId, leaseEpoch,
|
|
2786
|
+
}) {
|
|
2787
|
+
if (
|
|
2788
|
+
!api || typeof api.prepareWorkflowArtifact !== "function" ||
|
|
2789
|
+
typeof api.uploadArtifact !== "function" || typeof api.commitWorkflowArtifact !== "function"
|
|
2790
|
+
) {
|
|
2791
|
+
throw workerError(
|
|
2792
|
+
"This SIMY CLI cannot persist WorkFlow output files. Update SIMY CLI and retry the WorkFlow run.",
|
|
2793
|
+
"artifact_materialization_unsupported",
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
const materialized = [];
|
|
2797
|
+
for (const ref of artifactRefs) {
|
|
2798
|
+
const artifact = await localTaskArtifact(task, ref.id);
|
|
2799
|
+
if (!artifact) throw workerError("The WorkFlow output changed before it could be attached.", "artifact_materialization_source_changed");
|
|
2800
|
+
const bytes = await readFile(artifact.local_path);
|
|
2801
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
2802
|
+
if (bytes.byteLength !== ref.size_bytes || digest !== ref.sha256.toLowerCase()) {
|
|
2803
|
+
throw workerError("The WorkFlow output changed before it could be attached.", "artifact_materialization_source_changed");
|
|
2804
|
+
}
|
|
2805
|
+
const prepared = await retryAmbiguousArtifactStep(() => api.prepareWorkflowArtifact({
|
|
2806
|
+
turnId, workerId, leaseEpoch, localArtifactId: ref.id, name: ref.name,
|
|
2807
|
+
mimeType: ref.mime_type, sizeBytes: ref.size_bytes, sha256: ref.sha256.toLowerCase(),
|
|
2808
|
+
}), { preserveAmbiguity: true });
|
|
2809
|
+
validatePreparedArtifact(prepared);
|
|
2810
|
+
await uploadWorkflowArtifactForCommitVerification(api, {
|
|
2811
|
+
uploadUrl: prepared.upload_url, method: prepared.method, headers: prepared.headers, bytes,
|
|
2812
|
+
});
|
|
2813
|
+
const committed = await retryAmbiguousArtifactStep(() => api.commitWorkflowArtifact({
|
|
2814
|
+
uploadId: prepared.upload_id, turnId, workerId, leaseEpoch, localArtifactId: ref.id,
|
|
2815
|
+
sha256: ref.sha256.toLowerCase(), sizeBytes: ref.size_bytes,
|
|
2816
|
+
}), { preserveAmbiguity: true });
|
|
2817
|
+
materialized.push(validateWorkflowCommittedArtifact(committed, ref));
|
|
2818
|
+
}
|
|
2819
|
+
return materialized;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
async function uploadWorkflowArtifactForCommitVerification(api, input) {
|
|
2823
|
+
try {
|
|
2824
|
+
await retryAmbiguousArtifactStep(
|
|
2825
|
+
() => api.uploadArtifact(input),
|
|
2826
|
+
{ preserveAmbiguity: true },
|
|
2827
|
+
);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
// An immutable signed S3 PUT may have stored the exact bytes before its
|
|
2830
|
+
// response was lost. A later precondition failure only permits the
|
|
2831
|
+
// lease-fenced commit to verify that possibility; it is never publish
|
|
2832
|
+
// confirmation by itself.
|
|
2833
|
+
if (!(error instanceof DurableLocalTaskApiError) || error.status !== 412) {
|
|
2834
|
+
throw error;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2727
2839
|
async function retryAmbiguousArtifactStep(operation, { preserveAmbiguity = false } = {}) {
|
|
2728
2840
|
let lastError = null;
|
|
2729
2841
|
let ambiguous = false;
|
|
@@ -2791,6 +2903,26 @@ function validateCommittedArtifact(value, expected) {
|
|
|
2791
2903
|
};
|
|
2792
2904
|
}
|
|
2793
2905
|
|
|
2906
|
+
function validateWorkflowCommittedArtifact(value, expected) {
|
|
2907
|
+
const artifact = value?.workflow_artifact;
|
|
2908
|
+
if (
|
|
2909
|
+
!artifact || typeof artifact !== "object" || !isUuid(String(artifact.artifact_id || "")) ||
|
|
2910
|
+
artifact.name !== expected.name || artifact.mime_type !== expected.mime_type ||
|
|
2911
|
+
artifact.size_bytes !== expected.size_bytes ||
|
|
2912
|
+
String(artifact.sha256 || "").toLowerCase() !== expected.sha256.toLowerCase()
|
|
2913
|
+
) {
|
|
2914
|
+
throw workerError(
|
|
2915
|
+
"SIMY could not confirm the persisted WorkFlow output file.",
|
|
2916
|
+
"artifact_materialization_invalid_response",
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
id: expected.id, local_task_id: expected.local_task_id, name: expected.name,
|
|
2921
|
+
relative_path: expected.relative_path, mime_type: expected.mime_type,
|
|
2922
|
+
size_bytes: expected.size_bytes, sha256: expected.sha256.toLowerCase(), verified: true,
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2794
2926
|
function trustedArtifactUploadUrl(value) {
|
|
2795
2927
|
let target;
|
|
2796
2928
|
try {
|
|
@@ -2938,6 +3070,10 @@ function recoverySummary(status, failureCode) {
|
|
|
2938
3070
|
return `The Local Task failed (${failureCode || "unknown error"}).`;
|
|
2939
3071
|
}
|
|
2940
3072
|
|
|
3073
|
+
function workflowArtifactRecoverySummary() {
|
|
3074
|
+
return "SIMY could not confirm WorkFlow output delivery. Existing local output files were preserved. Keep SIMY CLI running while delivery is reconciled; do not rerun this WorkFlow.";
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2941
3077
|
function durableResultSummary(completed, status, failureCode) {
|
|
2942
3078
|
// Process exit details such as signal-derived code 130 are useful only in
|
|
2943
3079
|
// the local technical record. A user-requested stop has one stable public
|
|
@@ -156,7 +156,17 @@ async function runCodexDelegation({ selection, instruction, getInstruction, repo
|
|
|
156
156
|
if (!accountMatches(account.account?.email)) throw fail('provider_account_mismatch');
|
|
157
157
|
const metadata = await readCodexSession(client, selection.session_id, { signal });
|
|
158
158
|
if (metadata.isRunning === true) throw fail('provider_session_busy');
|
|
159
|
-
|
|
159
|
+
if (metadata.model !== selection.model) throw fail('provider_session_mismatch');
|
|
160
|
+
// A fresh app-server process otherwise resolves a resumed read-only
|
|
161
|
+
// session against the local default sandbox. Reapply only the already
|
|
162
|
+
// bound, narrower read contract before inspecting it; this never grants
|
|
163
|
+
// workspace-write authority or changes the selected session identity.
|
|
164
|
+
const resumeParams = { threadId: selection.session_id, excludeTurns: true };
|
|
165
|
+
if (permissionMode === 'read_only') {
|
|
166
|
+
resumeParams.model = selection.model;
|
|
167
|
+
resumeParams.sandbox = 'read-only';
|
|
168
|
+
}
|
|
169
|
+
const configuration = await client.request('thread/resume', resumeParams);
|
|
160
170
|
if (configuration.thread?.id !== selection.session_id || configuration.model !== selection.model ||
|
|
161
171
|
!configuration.cwd || await realpath(configuration.cwd) !== await realpath(nativeIdentity ? metadata.cwd : repositoryPath) ||
|
|
162
172
|
workspaceMode === 'repository' && await realpath(configuration.cwd) !== await realpath(repositoryPath)) throw fail('provider_session_mismatch');
|
|
@@ -25,7 +25,7 @@ function rememberMetadata(path, value) {
|
|
|
25
25
|
function candidateMetadata(id, metadata, lastActivityAt, observedAt) {
|
|
26
26
|
return {
|
|
27
27
|
provider: 'claude', session: id, ...(metadata.parentSessionId ? { parentSession: { provider: 'claude', session: metadata.parentSessionId } } : {}), cwd: metadata.cwd, title: metadata.title,
|
|
28
|
-
source: 'claude_cli', lastActivityAt,
|
|
28
|
+
source: 'claude_cli', lastActivityAt, createdAt: metadata.createdAt ?? null,
|
|
29
29
|
observation: { source: 'claude_local_index', observedAt },
|
|
30
30
|
availability: 'unsupported', model: null,
|
|
31
31
|
capabilities: { canMonitor: true, canEnroll: false, canAutoContinue: false },
|
|
@@ -62,7 +62,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
62
62
|
const text = bytes.toString('utf8');
|
|
63
63
|
const complete = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
64
64
|
if (!complete) return null;
|
|
65
|
-
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false;
|
|
65
|
+
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false, createdAt = null;
|
|
66
66
|
for (const line of complete.split('\n').slice(0, MAX_LINES)) {
|
|
67
67
|
if (!line) continue;
|
|
68
68
|
let item;
|
|
@@ -73,6 +73,8 @@ function metadataFromPrefix(bytes, id) {
|
|
|
73
73
|
if (item.type === 'custom-title' && item.isSidechain !== true) { customTitle = sessionTitle(item.customTitle); continue; }
|
|
74
74
|
if (item.type !== 'user' && item.type !== 'assistant') continue;
|
|
75
75
|
if (item.isSidechain === true) return null;
|
|
76
|
+
const timestamp = typeof item.timestamp === 'string' ? Date.parse(item.timestamp) : item.timestamp;
|
|
77
|
+
if (Number.isFinite(timestamp) && timestamp > 0) createdAt = createdAt === null ? timestamp : Math.min(createdAt, timestamp);
|
|
76
78
|
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd)) continue;
|
|
77
79
|
if (!cwd) cwd = resolve(item.cwd);
|
|
78
80
|
// A conversation can cd into a child directory without changing identity.
|
|
@@ -80,7 +82,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
80
82
|
if (!title) title = sessionTitle(item.slug);
|
|
81
83
|
found = true;
|
|
82
84
|
}
|
|
83
|
-
return found ? { cwd, title: customTitle || title, parentSessionId } : null;
|
|
85
|
+
return found ? { cwd, title: customTitle || title, parentSessionId, createdAt } : null;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// /rename appends a custom-title envelope, often beyond the bounded prefix.
|
|
@@ -222,6 +222,7 @@ export function codexThreadMetadata(thread) {
|
|
|
222
222
|
title: typeof thread.name === 'string' && thread.name.trim() ? thread.name.slice(0, 200) : '',
|
|
223
223
|
source: typeof thread.source === 'string' ? thread.source : thread.source?.type || 'unknown',
|
|
224
224
|
lastActivityAt: Number.isFinite(thread.updatedAt) ? thread.updatedAt * 1000 : null,
|
|
225
|
+
createdAt: Number.isFinite(thread.createdAt) && thread.createdAt > 0 ? thread.createdAt * 1000 : null,
|
|
225
226
|
model: typeof thread.model === 'string' ? thread.model : null,
|
|
226
227
|
// Preserve native wait flags for presentation without changing isRunning,
|
|
227
228
|
// which also protects the provider writer lock. Unknown flags fail closed.
|
|
@@ -4,7 +4,7 @@ export const RECENT_SESSION_LIMIT = 200;
|
|
|
4
4
|
|
|
5
5
|
// Explicit tracking and unsettled work remain visible outside recent history.
|
|
6
6
|
// Completed, untracked records are history, not an unlimited extra catalog.
|
|
7
|
-
export const retainedSession = record => Boolean(record?.autoContinue || record?.monitor || record?.tracked ||
|
|
7
|
+
export const retainedSession = record => Boolean(record?.listPinned || record?.autoContinue || record?.monitor || record?.tracked ||
|
|
8
8
|
record?.run && !TERMINAL.has(record.run.state));
|
|
9
9
|
|
|
10
10
|
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false } = {}) {
|
|
@@ -231,8 +231,8 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
231
231
|
// A hook observation can belong to an AI-created worker. Only explicit
|
|
232
232
|
// supervision may restore a conversation absent from provider discovery.
|
|
233
233
|
if (!byKey.has(key) && (retainedSession(record) || includeArchived && record.listHidden === true || requiredAncestors.has(key) && (record.observedAt || record.admission?.validatedAt))) byKey.set(key, {
|
|
234
|
-
provider: record.provider, session: record.session, cwd: record.cwd, title: '', archived, source: record.admission?.source || 'hook',
|
|
235
|
-
lastActivityAt: record.observedAt
|
|
234
|
+
provider: record.provider, session: record.session, cwd: record.cwd, title: record.listMetadata?.title || '', createdAt: record.listMetadata?.createdAt ?? null, archived, source: record.admission?.source || 'hook',
|
|
235
|
+
lastActivityAt: record.listMetadata?.lastActivityAt ?? record.observedAt ?? record.admission?.validatedAt ?? record.run?.updatedAt ?? null,
|
|
236
236
|
observation: { source: record.admission?.source || `${record.provider}_hook`, observedAt: record.observedAt || record.admission?.validatedAt || null },
|
|
237
237
|
availability: 'unknown', model: record.admission?.model || null,
|
|
238
238
|
});
|
|
@@ -327,7 +327,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
327
327
|
autoContinueEpoch: record?.autoContinue === true ? record.autoContinueEpoch : null,
|
|
328
328
|
autoContinueAvailability: availability,
|
|
329
329
|
...autoContinueState(record, isRunning, availability),
|
|
330
|
-
lastActivityAt: item.lastActivityAt, observation, availability: item.availability,
|
|
330
|
+
lastActivityAt: item.lastActivityAt, createdAt: item.createdAt ?? null, observation, availability: item.availability,
|
|
331
331
|
taskId: run?.managed?.taskId || record?.associationTaskId || null,
|
|
332
332
|
binding: { mode, taskId: run?.managed?.taskId || record?.associationTaskId || null },
|
|
333
333
|
originalSessionTarget: originalSessionTarget(item.provider, item.session),
|
|
@@ -13,7 +13,7 @@ export const remoteHostId = alias => `remote-ssh-discovered:${alias}`;
|
|
|
13
13
|
const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
14
14
|
const bootstrap = 'import { realpathSync } from "node:fs"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; const entry = join(dirname(realpathSync(process.argv[1])), "session-supervision", "remote-entry.js"); await import(pathToFileURL(entry).href);';
|
|
15
15
|
export const REMOTE_HOST_COMMAND = `exec sh -lc ${shellQuote(`exec env SIMY_REMOTE_CHILD=1 SIMY_AUTO_UPDATE=0 node --input-type=module -e ${shellQuote(bootstrap)} "$(command -v simy)"`)}`;
|
|
16
|
-
const allowedActions = new Set(['refresh', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
|
|
16
|
+
const allowedActions = new Set(['refresh', 'session_set_pinned', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
|
|
17
17
|
'session_monitor', 'session_unmonitor', 'session_enroll', 'session_set_auto_continue', 'session_analyze', 'session_continue', 'session_answer',
|
|
18
18
|
'session_deadline_decide', 'session_action_refresh', 'session_action_select', 'session_action_unlink',
|
|
19
19
|
'delivery_pause', 'delivery_stop', 'delivery_reject', 'delivery_resume', 'delivery_replan', 'delivery_approve',
|
package/src/team-threads.js
CHANGED
|
@@ -113,6 +113,7 @@ export function createTeamThreads({
|
|
|
113
113
|
selected = null,
|
|
114
114
|
memberFilter = null,
|
|
115
115
|
pending = new Map(),
|
|
116
|
+
publicationGrants = new Map(),
|
|
116
117
|
storageFailed = false,
|
|
117
118
|
lastAllowed = false;
|
|
118
119
|
function scope() {
|
|
@@ -122,11 +123,12 @@ export function createTeamThreads({
|
|
|
122
123
|
generation++;
|
|
123
124
|
const sameIdentity = scopeKey === current.key;
|
|
124
125
|
scopeKey = current.key;
|
|
125
|
-
const own = sameIdentity ? value.own : [];
|
|
126
|
+
const own = sameIdentity && current.authenticated ? value.own : [];
|
|
126
127
|
value = { ...empty(), own };
|
|
127
128
|
selected = null;
|
|
128
129
|
memberFilter = null;
|
|
129
130
|
if (!sameIdentity) pending = new Map();
|
|
131
|
+
publicationGrants.clear();
|
|
130
132
|
lastPoll = -Infinity;
|
|
131
133
|
}
|
|
132
134
|
lastAllowed = current.allowed;
|
|
@@ -135,7 +137,7 @@ export function createTeamThreads({
|
|
|
135
137
|
}
|
|
136
138
|
const snapshot = () => {
|
|
137
139
|
scope();
|
|
138
|
-
return structuredClone({ ...value, pendingStops: [...pending.keys()] });
|
|
140
|
+
return structuredClone({ ...value, pendingStops: [...pending.keys()], confirmationRequired: value.own.filter(row => row.enabled && publicationGrants.get(row.threadKey) !== row.version).map(row => row.threadKey) });
|
|
139
141
|
};
|
|
140
142
|
const fenceKey = (current, key) => digest(`${current.fenceKey}:${key}`);
|
|
141
143
|
async function run(fn, allowOff = false) {
|
|
@@ -272,6 +274,11 @@ export function createTeamThreads({
|
|
|
272
274
|
incomplete: result.incomplete,
|
|
273
275
|
};
|
|
274
276
|
}
|
|
277
|
+
function sourceObservedAt(candidate) {
|
|
278
|
+
const observed = candidate.observation?.observedAt;
|
|
279
|
+
if (!Number.isFinite(observed) || observed <= 0 || observed > now()) invalid();
|
|
280
|
+
return new Date(observed).toISOString();
|
|
281
|
+
}
|
|
275
282
|
async function payload(candidate, signal) {
|
|
276
283
|
const result = await readContext(candidate, { signal });
|
|
277
284
|
if (!Array.isArray(result?.messages) || !result.messages.length) invalid();
|
|
@@ -295,7 +302,7 @@ export function createTeamThreads({
|
|
|
295
302
|
240,
|
|
296
303
|
),
|
|
297
304
|
state: running ? "running" : "unknown",
|
|
298
|
-
observedAt:
|
|
305
|
+
observedAt: sourceObservedAt(candidate),
|
|
299
306
|
messages,
|
|
300
307
|
truncated: result.truncated === true,
|
|
301
308
|
incomplete: result.incomplete === true,
|
|
@@ -322,13 +329,18 @@ export function createTeamThreads({
|
|
|
322
329
|
const fence = await fenceStore.read(fenceKey(current, item.threadKey));
|
|
323
330
|
if (!valid()) return;
|
|
324
331
|
if (fence) {
|
|
325
|
-
|
|
326
|
-
|
|
332
|
+
// A lost OFF response (or intervening version) must reconcile against
|
|
333
|
+
// authenticated current state, rather than retry a stale CAS forever.
|
|
334
|
+
pending.set(item.threadKey, item.version);
|
|
335
|
+
if (item.enabled) await stop(send, current, item.threadKey, item.version);
|
|
336
|
+
else await clearStop(current, item.threadKey);
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
if (publish && !storageFailed)
|
|
330
340
|
for (const item of [...value.own]) {
|
|
331
|
-
|
|
341
|
+
// Server consent alone cannot re-arm publication after a process restart:
|
|
342
|
+
// an OFF may have failed both durable storage and the network.
|
|
343
|
+
if (!item.enabled || pending.has(item.threadKey) || publicationGrants.get(item.threadKey) !== item.version) continue;
|
|
332
344
|
const candidate = candidates.find(
|
|
333
345
|
(c) => teamThreadKey(current, c) === item.threadKey,
|
|
334
346
|
);
|
|
@@ -384,9 +396,23 @@ export function createTeamThreads({
|
|
|
384
396
|
if (valid()) onChange();
|
|
385
397
|
});
|
|
386
398
|
},
|
|
387
|
-
async
|
|
399
|
+
async stopSharing(threadKey) {
|
|
400
|
+
scope();
|
|
401
|
+
if (!keyPattern.test(threadKey) || !value.own.some(row => row.threadKey === threadKey)) return false;
|
|
402
|
+
return setSharing(null, false, threadKey);
|
|
403
|
+
},
|
|
404
|
+
setSharing,
|
|
405
|
+
close() {
|
|
406
|
+
closed = true;
|
|
407
|
+
controller?.abort();
|
|
408
|
+
generation++;
|
|
409
|
+
publicationGrants.clear();
|
|
410
|
+
value = empty();
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
async function setSharing(candidate, enabled, existingKey) {
|
|
388
414
|
const current = scope(),
|
|
389
|
-
key = teamThreadKey(current, candidate),
|
|
415
|
+
key = existingKey ?? teamThreadKey(current, candidate),
|
|
390
416
|
item = value.own.find((row) => row.threadKey === key),
|
|
391
417
|
version = item?.version ?? 0;
|
|
392
418
|
if (
|
|
@@ -408,6 +434,7 @@ export function createTeamThreads({
|
|
|
408
434
|
storageFailed = false;
|
|
409
435
|
}
|
|
410
436
|
if (!enabled) {
|
|
437
|
+
publicationGrants.delete(key);
|
|
411
438
|
pending.set(key, version);
|
|
412
439
|
controller?.abort();
|
|
413
440
|
generation++;
|
|
@@ -442,16 +469,10 @@ export function createTeamThreads({
|
|
|
442
469
|
receipt.version !== version + 1
|
|
443
470
|
)
|
|
444
471
|
invalid();
|
|
472
|
+
if (valid() && !pending.has(key)) publicationGrants.set(key, receipt.version);
|
|
445
473
|
}
|
|
446
474
|
if (current.allowed) await context(send);
|
|
447
475
|
if (valid()) onChange();
|
|
448
476
|
}, !enabled);
|
|
449
|
-
|
|
450
|
-
close() {
|
|
451
|
-
closed = true;
|
|
452
|
-
controller?.abort();
|
|
453
|
-
generation++;
|
|
454
|
-
value = empty();
|
|
455
|
-
},
|
|
456
|
-
};
|
|
477
|
+
}
|
|
457
478
|
}
|