gestalt-mobile 0.25.2 → 0.25.4
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/client/assets/{index-BkpRh3eb.js → index-CFw8G97O.js} +18 -18
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +52 -62
- package/dist/server/server/composition.js +63 -4
- package/dist/server/server/features/agent-activity/model.js +27 -0
- package/dist/server/server/features/agent-activity/registry.js +24 -0
- package/dist/server/server/features/autopilot/application/policy.js +14 -12
- package/dist/server/server/features/autopilot/application/service.js +339 -17
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +2 -0
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +255 -0
- package/dist/server/server/platform/codex/session-runtime.js +204 -3
- package/dist/server/server/platform/persistence/migrate.js +7 -2
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +21 -3
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +5 -0
- package/dist/server/shared/contracts/autopilot-audit.js +12 -0
- package/dist/server/shared/org-plan-position.js +12 -2
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { orgPlanAgentDisplayName } from '../../../../shared/org-plan-position.js';
|
|
7
|
+
export const blockingResumeConditions = {
|
|
8
|
+
planChange: 'planRevision',
|
|
9
|
+
hardBlock: 'externalStateChanged',
|
|
10
|
+
missingDependency: 'dependencyInstalled',
|
|
11
|
+
permissionRequired: 'permissionGranted',
|
|
12
|
+
externalState: 'externalStateChanged',
|
|
13
|
+
materialAmbiguity: 'userGuidance',
|
|
14
|
+
};
|
|
15
|
+
export function validStructuredBlock(value) {
|
|
16
|
+
return Boolean(value && blockingResumeConditions[value.reason] === value.resumeCondition);
|
|
17
|
+
}
|
|
18
|
+
export function parsePersistedSupervisedLifecycle(value) {
|
|
19
|
+
const root = record(value);
|
|
20
|
+
if (!root)
|
|
21
|
+
return undefined;
|
|
22
|
+
const blockingValue = record(root.blocking);
|
|
23
|
+
const blocking = blockingValue ? parseStructuredBlock(blockingValue) : undefined;
|
|
24
|
+
if (blockingValue && !blocking)
|
|
25
|
+
return undefined;
|
|
26
|
+
const executorValue = record(root.executor);
|
|
27
|
+
if (!executorValue)
|
|
28
|
+
return blocking ? { blocking } : {};
|
|
29
|
+
const processesValue = executorValue.ownedProcesses;
|
|
30
|
+
if (!Array.isArray(processesValue) || processesValue.length > 64)
|
|
31
|
+
return undefined;
|
|
32
|
+
const ownedProcesses = processesValue.flatMap((candidate) => {
|
|
33
|
+
const process = record(candidate);
|
|
34
|
+
if (!process)
|
|
35
|
+
return [];
|
|
36
|
+
const processId = boundedText(process.processId);
|
|
37
|
+
const itemId = boundedText(process.itemId);
|
|
38
|
+
const ownerThreadId = boundedText(process.ownerThreadId);
|
|
39
|
+
const ownerTaskPath = boundedText(process.ownerTaskPath);
|
|
40
|
+
const ownership = stringValue(process.ownership, ['executor', 'supervisor']);
|
|
41
|
+
const state = stringValue(process.state, [
|
|
42
|
+
'running',
|
|
43
|
+
'detached-active',
|
|
44
|
+
'exited-awaiting-result',
|
|
45
|
+
'result-consumed',
|
|
46
|
+
'terminated-for-budget',
|
|
47
|
+
]);
|
|
48
|
+
const observedAt = boundedText(process.observedAt);
|
|
49
|
+
const elapsedMs = nonNegativeNumber(process.elapsedMs);
|
|
50
|
+
const cpuPercent = nullableNonNegativeNumber(process.cpuPercent);
|
|
51
|
+
const rssBytes = nullableNonNegativeNumber(process.rssBytes);
|
|
52
|
+
if (!processId ||
|
|
53
|
+
!itemId ||
|
|
54
|
+
!ownerThreadId ||
|
|
55
|
+
!ownerTaskPath ||
|
|
56
|
+
!ownership ||
|
|
57
|
+
!state ||
|
|
58
|
+
!observedAt ||
|
|
59
|
+
elapsedMs === null ||
|
|
60
|
+
cpuPercent === undefined ||
|
|
61
|
+
rssBytes === undefined)
|
|
62
|
+
return [];
|
|
63
|
+
const osPid = nonNegativeInteger(process.osPid);
|
|
64
|
+
const exitStatus = integer(process.exitStatus);
|
|
65
|
+
const resultArtifact = boundedText(process.resultArtifact);
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
processId,
|
|
69
|
+
itemId,
|
|
70
|
+
ownerThreadId,
|
|
71
|
+
ownerTaskPath,
|
|
72
|
+
ownership,
|
|
73
|
+
state,
|
|
74
|
+
observedAt,
|
|
75
|
+
elapsedMs,
|
|
76
|
+
cpuPercent,
|
|
77
|
+
rssBytes,
|
|
78
|
+
...(osPid === null ? {} : { osPid }),
|
|
79
|
+
...(exitStatus === null ? {} : { exitStatus }),
|
|
80
|
+
...(resultArtifact ? { resultArtifact } : {}),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
});
|
|
84
|
+
if (ownedProcesses.length !== processesValue.length)
|
|
85
|
+
return undefined;
|
|
86
|
+
const canonicalPosition = boundedText(executorValue.canonicalPosition);
|
|
87
|
+
const canonicalTaskName = boundedText(executorValue.canonicalTaskName);
|
|
88
|
+
const taskPath = boundedText(executorValue.taskPath);
|
|
89
|
+
const threadId = boundedText(executorValue.threadId);
|
|
90
|
+
const l1State = stringValue(executorValue.l1State, ['TODO', 'WIP', 'DONE']);
|
|
91
|
+
const l2State = stringValue(executorValue.l2State, ['TODO', 'WIP', 'DONE']);
|
|
92
|
+
const lastActivityAt = boundedText(executorValue.lastActivityAt);
|
|
93
|
+
const outcome = stringValue(executorValue.outcome, [
|
|
94
|
+
'objective_complete',
|
|
95
|
+
'partial',
|
|
96
|
+
'blocked',
|
|
97
|
+
'cancelled',
|
|
98
|
+
'failed',
|
|
99
|
+
]);
|
|
100
|
+
const continuationGeneration = nonNegativeInteger(executorValue.continuationGeneration);
|
|
101
|
+
const continuationCount = nonNegativeInteger(executorValue.continuationCount);
|
|
102
|
+
if (!canonicalPosition ||
|
|
103
|
+
!canonicalTaskName ||
|
|
104
|
+
!taskPath ||
|
|
105
|
+
!threadId ||
|
|
106
|
+
!l1State ||
|
|
107
|
+
!lastActivityAt ||
|
|
108
|
+
!outcome ||
|
|
109
|
+
continuationGeneration === null ||
|
|
110
|
+
continuationCount === null)
|
|
111
|
+
return undefined;
|
|
112
|
+
const executorBlockingValue = record(executorValue.blocking);
|
|
113
|
+
const executorBlocking = executorBlockingValue
|
|
114
|
+
? parseStructuredBlock(executorBlockingValue)
|
|
115
|
+
: undefined;
|
|
116
|
+
if (executorBlockingValue && !executorBlocking)
|
|
117
|
+
return undefined;
|
|
118
|
+
return {
|
|
119
|
+
executor: {
|
|
120
|
+
canonicalPosition,
|
|
121
|
+
canonicalTaskName,
|
|
122
|
+
taskPath,
|
|
123
|
+
threadId,
|
|
124
|
+
l1State,
|
|
125
|
+
...(l2State ? { l2State } : {}),
|
|
126
|
+
lastActivityAt,
|
|
127
|
+
ownedProcesses,
|
|
128
|
+
outcome,
|
|
129
|
+
...(executorBlocking ? { blocking: executorBlocking } : {}),
|
|
130
|
+
continuationGeneration,
|
|
131
|
+
continuationCount,
|
|
132
|
+
},
|
|
133
|
+
...(blocking ? { blocking } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function classifyExecutorOutcome(input) {
|
|
137
|
+
if (input.objectiveComplete)
|
|
138
|
+
return { outcome: 'objective_complete' };
|
|
139
|
+
if (input.reportedOutcome === 'cancelled' || input.reportedOutcome === 'failed')
|
|
140
|
+
return { outcome: input.reportedOutcome };
|
|
141
|
+
const blocking = input.blockingReason && input.resumeCondition
|
|
142
|
+
? { reason: input.blockingReason, resumeCondition: input.resumeCondition }
|
|
143
|
+
: undefined;
|
|
144
|
+
if (input.reportedOutcome === 'blocked' && validStructuredBlock(blocking))
|
|
145
|
+
return { outcome: 'blocked', blocking };
|
|
146
|
+
// A turn ending, a checkpoint, or free-form language about time/context is
|
|
147
|
+
// not objective state. Incomplete Org state remains mechanically partial.
|
|
148
|
+
return { outcome: 'partial' };
|
|
149
|
+
}
|
|
150
|
+
export function decideSupervisedLifecycle(input) {
|
|
151
|
+
if (input.explicitlyPausedOrCancelled || executionComplete(input.plan))
|
|
152
|
+
return { finalAllowed: true, action: { kind: 'allowFinal' } };
|
|
153
|
+
if (validStructuredBlock(input.attention))
|
|
154
|
+
return {
|
|
155
|
+
finalAllowed: true,
|
|
156
|
+
action: { kind: 'invokeAttention', ...input.attention },
|
|
157
|
+
};
|
|
158
|
+
const processes = input.executor?.ownedProcesses ?? [];
|
|
159
|
+
const overBudget = processes.find((process) => (process.state === 'running' || process.state === 'detached-active') &&
|
|
160
|
+
(process.elapsedMs > input.policy.processMaxElapsedMs ||
|
|
161
|
+
(process.rssBytes !== null && process.rssBytes > input.policy.processMaxRssBytes)));
|
|
162
|
+
if (overBudget)
|
|
163
|
+
return {
|
|
164
|
+
finalAllowed: false,
|
|
165
|
+
action: {
|
|
166
|
+
kind: 'terminateProcess',
|
|
167
|
+
threadId: overBudget.ownerThreadId,
|
|
168
|
+
processId: overBudget.processId,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
const exited = processes.find((process) => process.state === 'exited-awaiting-result' && process.resultArtifact);
|
|
172
|
+
if (exited)
|
|
173
|
+
return {
|
|
174
|
+
finalAllowed: false,
|
|
175
|
+
action: {
|
|
176
|
+
kind: 'consumeProcessResult',
|
|
177
|
+
threadId: exited.ownerThreadId,
|
|
178
|
+
processId: exited.processId,
|
|
179
|
+
resultArtifact: exited.resultArtifact,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const active = processes.find((process) => process.state === 'running' || process.state === 'detached-active');
|
|
183
|
+
if (active)
|
|
184
|
+
return {
|
|
185
|
+
finalAllowed: false,
|
|
186
|
+
action: {
|
|
187
|
+
kind: 'monitorProcess',
|
|
188
|
+
process: { ...active, ownership: 'supervisor', state: 'detached-active' },
|
|
189
|
+
pollAfterMs: input.policy.processPollMs,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
if (input.event === 'waitTimeout' && !input.executor)
|
|
193
|
+
return { finalAllowed: false, action: { kind: 'reinspect' } };
|
|
194
|
+
if (!input.executor || input.executor.l1State === 'DONE')
|
|
195
|
+
return { finalAllowed: false, action: { kind: 'continueSupervisor' } };
|
|
196
|
+
const exponent = Math.min(30, Math.max(0, input.executor.continuationCount));
|
|
197
|
+
const delayMs = Math.min(input.policy.continuationMaxDelayMs, input.policy.continuationBaseDelayMs * 2 ** exponent);
|
|
198
|
+
return {
|
|
199
|
+
finalAllowed: false,
|
|
200
|
+
action: {
|
|
201
|
+
kind: 'resumeExecutor',
|
|
202
|
+
threadId: input.executor.threadId,
|
|
203
|
+
generation: input.executor.continuationGeneration + 1,
|
|
204
|
+
delayMs,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function executorIdentity(canonicalTaskName, generation) {
|
|
209
|
+
if (!/^l[1-9]\d*(?:_[1-9]\d*)?$/.test(canonicalTaskName) ||
|
|
210
|
+
!Number.isSafeInteger(generation) ||
|
|
211
|
+
generation < 1)
|
|
212
|
+
throw new Error('INVALID_EXECUTOR_IDENTITY');
|
|
213
|
+
return {
|
|
214
|
+
canonicalTaskName,
|
|
215
|
+
canonicalPosition: orgPlanAgentDisplayName(canonicalTaskName),
|
|
216
|
+
generation,
|
|
217
|
+
taskName: generation === 1 ? canonicalTaskName : `${canonicalTaskName}_g${generation}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function executionComplete(plan) {
|
|
221
|
+
return (plan.executionComplete ??
|
|
222
|
+
plan.steps.every((step) => step.state === 'DONE' &&
|
|
223
|
+
step.reviewStatus === 'REVIEWED' &&
|
|
224
|
+
step.children.every((child) => child.state === 'DONE')));
|
|
225
|
+
}
|
|
226
|
+
function record(value) {
|
|
227
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
228
|
+
? value
|
|
229
|
+
: undefined;
|
|
230
|
+
}
|
|
231
|
+
function parseStructuredBlock(value) {
|
|
232
|
+
const reason = stringValue(value.reason, Object.keys(blockingResumeConditions));
|
|
233
|
+
const resumeCondition = stringValue(value.resumeCondition, Object.values(blockingResumeConditions));
|
|
234
|
+
const blocking = reason && resumeCondition ? { reason, resumeCondition } : undefined;
|
|
235
|
+
return validStructuredBlock(blocking) ? blocking : undefined;
|
|
236
|
+
}
|
|
237
|
+
function boundedText(value) {
|
|
238
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 512 ? value : undefined;
|
|
239
|
+
}
|
|
240
|
+
function stringValue(value, values) {
|
|
241
|
+
return typeof value === 'string' && values.includes(value) ? value : undefined;
|
|
242
|
+
}
|
|
243
|
+
function nonNegativeNumber(value) {
|
|
244
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
245
|
+
}
|
|
246
|
+
function nullableNonNegativeNumber(value) {
|
|
247
|
+
return value === null ? null : (nonNegativeNumber(value) ?? undefined);
|
|
248
|
+
}
|
|
249
|
+
function integer(value) {
|
|
250
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
|
|
251
|
+
}
|
|
252
|
+
function nonNegativeInteger(value) {
|
|
253
|
+
const parsed = integer(value);
|
|
254
|
+
return parsed !== null && parsed >= 0 ? parsed : null;
|
|
255
|
+
}
|
|
@@ -29,6 +29,8 @@ class SessionResource {
|
|
|
29
29
|
writtenThreadName;
|
|
30
30
|
capabilities = new Map();
|
|
31
31
|
spawnedAgentModels = new Map();
|
|
32
|
+
attemptedAgentModelRecovery = new Set();
|
|
33
|
+
ownedChildProcesses = new Map();
|
|
32
34
|
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
33
35
|
this.sessionId = sessionId;
|
|
34
36
|
this.process = process;
|
|
@@ -182,6 +184,17 @@ export class CodexSessionRuntime {
|
|
|
182
184
|
}));
|
|
183
185
|
return RelaySession.rehydrate(session).startTurn(result, now).snapshot;
|
|
184
186
|
}
|
|
187
|
+
async startExecutorTurn(session, childThreadId, text, clientUserMessageId) {
|
|
188
|
+
const resource = this.sessions.get(session.id);
|
|
189
|
+
if (!resource)
|
|
190
|
+
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
191
|
+
return decodeTurnStart(await resource.process.rpc.request('turn/start', {
|
|
192
|
+
threadId: childThreadId,
|
|
193
|
+
input: [{ type: 'text', text, text_elements: [] }],
|
|
194
|
+
clientUserMessageId,
|
|
195
|
+
...(session.model ? { model: session.model } : {}),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
185
198
|
/** The sole authoritative in-process ownership probe. It never launches a child. */
|
|
186
199
|
ownsWriter(sessionId) {
|
|
187
200
|
return this.sessions.get(sessionId)?.active === true;
|
|
@@ -315,6 +328,9 @@ export class CodexSessionRuntime {
|
|
|
315
328
|
...(owned.spawnedAgentModels.get(value.id)
|
|
316
329
|
? { model: owned.spawnedAgentModels.get(value.id) }
|
|
317
330
|
: {}),
|
|
331
|
+
...(decodeAgentTaskPath(value.source)
|
|
332
|
+
? { taskPath: decodeAgentTaskPath(value.source) }
|
|
333
|
+
: {}),
|
|
318
334
|
},
|
|
319
335
|
];
|
|
320
336
|
}));
|
|
@@ -322,7 +338,7 @@ export class CodexSessionRuntime {
|
|
|
322
338
|
? response.nextCursor
|
|
323
339
|
: undefined;
|
|
324
340
|
if (!next)
|
|
325
|
-
return children;
|
|
341
|
+
return this.withResolvedChildModels(owned, children);
|
|
326
342
|
if (cursors.has(next) || children.length >= 64)
|
|
327
343
|
throw new Error('CODEX_CHILD_LIST_UNSUPPORTED');
|
|
328
344
|
cursors.add(next);
|
|
@@ -330,7 +346,159 @@ export class CodexSessionRuntime {
|
|
|
330
346
|
}
|
|
331
347
|
if (cursor)
|
|
332
348
|
throw new Error('CODEX_CHILD_LIST_UNSUPPORTED');
|
|
333
|
-
return children;
|
|
349
|
+
return this.withResolvedChildModels(owned, children);
|
|
350
|
+
}
|
|
351
|
+
async withResolvedChildModels(resource, children) {
|
|
352
|
+
for (const child of children) {
|
|
353
|
+
if (child.status !== 'notLoaded' ||
|
|
354
|
+
resource.spawnedAgentModels.has(child.id) ||
|
|
355
|
+
resource.attemptedAgentModelRecovery.has(child.id))
|
|
356
|
+
continue;
|
|
357
|
+
resource.attemptedAgentModelRecovery.add(child.id);
|
|
358
|
+
try {
|
|
359
|
+
const response = await resource.process.rpc.request('thread/resume', {
|
|
360
|
+
threadId: child.id,
|
|
361
|
+
});
|
|
362
|
+
const model = boundedString(asRecord(response)?.model, 256);
|
|
363
|
+
if (model)
|
|
364
|
+
resource.spawnedAgentModels.set(child.id, model);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// A live settings notification can still provide the model later.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return children.map((child) => {
|
|
371
|
+
const model = child.model ?? resource.spawnedAgentModels.get(child.id);
|
|
372
|
+
return model && model !== child.model ? { ...child, model } : child;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/** Reads only bounded process metadata; command text and output never enter lifecycle state. */
|
|
376
|
+
async inspectChildProcesses(session, child) {
|
|
377
|
+
const owned = this.sessions.get(session.id);
|
|
378
|
+
if (!owned)
|
|
379
|
+
return [];
|
|
380
|
+
const active = await this.listChildBackgroundTerminals(owned, child);
|
|
381
|
+
const activeIds = new Set(active.map((process) => process.processId));
|
|
382
|
+
for (const process of active)
|
|
383
|
+
owned.ownedChildProcesses.set(childProcessKey(child.id, process.processId), process);
|
|
384
|
+
const prior = [...owned.ownedChildProcesses.values()].filter((process) => process.ownerThreadId === child.id && !activeIds.has(process.processId));
|
|
385
|
+
if (prior.some((process) => process.state === 'running' || process.state === 'detached-active')) {
|
|
386
|
+
const results = await this.readChildProcessResults(owned, child.id);
|
|
387
|
+
for (const process of prior) {
|
|
388
|
+
if (process.state !== 'running' && process.state !== 'detached-active')
|
|
389
|
+
continue;
|
|
390
|
+
const result = results.get(process.itemId);
|
|
391
|
+
owned.ownedChildProcesses.set(childProcessKey(child.id, process.processId), {
|
|
392
|
+
...process,
|
|
393
|
+
state: 'exited-awaiting-result',
|
|
394
|
+
cpuPercent: 0,
|
|
395
|
+
rssBytes: 0,
|
|
396
|
+
...(result?.exitStatus === undefined ? {} : { exitStatus: result.exitStatus }),
|
|
397
|
+
resultArtifact: `${child.id}:${process.itemId}`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [...owned.ownedChildProcesses.values()].filter((process) => process.ownerThreadId === child.id &&
|
|
402
|
+
process.state !== 'result-consumed' &&
|
|
403
|
+
process.state !== 'terminated-for-budget');
|
|
404
|
+
}
|
|
405
|
+
consumeChildProcessResult(sessionId, childThreadId, processId) {
|
|
406
|
+
this.sessions
|
|
407
|
+
.get(sessionId)
|
|
408
|
+
?.ownedChildProcesses.delete(childProcessKey(childThreadId, processId));
|
|
409
|
+
}
|
|
410
|
+
async terminateChildProcess(session, childThreadId, processId) {
|
|
411
|
+
const owned = this.sessions.get(session.id);
|
|
412
|
+
if (!owned)
|
|
413
|
+
return false;
|
|
414
|
+
const result = await owned.process.rpc.request('thread/backgroundTerminals/terminate', {
|
|
415
|
+
threadId: childThreadId,
|
|
416
|
+
processId,
|
|
417
|
+
});
|
|
418
|
+
const terminated = asRecord(result)?.terminated === true;
|
|
419
|
+
if (terminated) {
|
|
420
|
+
const key = childProcessKey(childThreadId, processId);
|
|
421
|
+
const process = owned.ownedChildProcesses.get(key);
|
|
422
|
+
if (process)
|
|
423
|
+
owned.ownedChildProcesses.set(key, { ...process, state: 'terminated-for-budget' });
|
|
424
|
+
}
|
|
425
|
+
return terminated;
|
|
426
|
+
}
|
|
427
|
+
async listChildBackgroundTerminals(owned, child) {
|
|
428
|
+
const processes = [];
|
|
429
|
+
const cursors = new Set();
|
|
430
|
+
let cursor;
|
|
431
|
+
for (let page = 0; page < 4 && processes.length < 64; page += 1) {
|
|
432
|
+
const result = await owned.process.rpc.request('thread/backgroundTerminals/list', {
|
|
433
|
+
threadId: child.id,
|
|
434
|
+
limit: 64,
|
|
435
|
+
...(cursor ? { cursor } : {}),
|
|
436
|
+
});
|
|
437
|
+
const response = asRecord(result);
|
|
438
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
439
|
+
for (const candidate of data) {
|
|
440
|
+
const value = asRecord(candidate);
|
|
441
|
+
const itemId = boundedString(value?.itemId, 256);
|
|
442
|
+
const processId = boundedString(value?.processId, 256);
|
|
443
|
+
if (!itemId || !processId || processes.length >= 64)
|
|
444
|
+
continue;
|
|
445
|
+
const key = childProcessKey(child.id, processId);
|
|
446
|
+
const before = owned.ownedChildProcesses.get(key);
|
|
447
|
+
const observedAt = before?.observedAt ?? new Date().toISOString();
|
|
448
|
+
const rssKb = boundedNonNegativeNumber(value?.rssKb);
|
|
449
|
+
processes.push({
|
|
450
|
+
processId,
|
|
451
|
+
itemId,
|
|
452
|
+
ownerThreadId: child.id,
|
|
453
|
+
ownerTaskPath: child.taskPath ?? child.id,
|
|
454
|
+
ownership: before?.ownership ?? 'executor',
|
|
455
|
+
state: before?.state === 'detached-active' ? 'detached-active' : 'running',
|
|
456
|
+
observedAt,
|
|
457
|
+
elapsedMs: Math.max(0, Date.now() - Date.parse(observedAt)),
|
|
458
|
+
cpuPercent: boundedNonNegativeNumber(value?.cpuPercent),
|
|
459
|
+
rssBytes: rssKb === null ? null : Math.min(Number.MAX_SAFE_INTEGER, rssKb * 1024),
|
|
460
|
+
...(boundedNonNegativeInteger(value?.osPid) === null
|
|
461
|
+
? {}
|
|
462
|
+
: { osPid: boundedNonNegativeInteger(value?.osPid) }),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const next = boundedString(response?.nextCursor, 256);
|
|
466
|
+
if (!next)
|
|
467
|
+
return processes;
|
|
468
|
+
if (cursors.has(next) || processes.length >= 64)
|
|
469
|
+
throw new Error('CODEX_BACKGROUND_TERMINAL_LIST_UNSUPPORTED');
|
|
470
|
+
cursors.add(next);
|
|
471
|
+
cursor = next;
|
|
472
|
+
}
|
|
473
|
+
if (cursor)
|
|
474
|
+
throw new Error('CODEX_BACKGROUND_TERMINAL_LIST_UNSUPPORTED');
|
|
475
|
+
return processes;
|
|
476
|
+
}
|
|
477
|
+
async readChildProcessResults(owned, childThreadId) {
|
|
478
|
+
const result = await owned.process.rpc.request('thread/read', {
|
|
479
|
+
threadId: childThreadId,
|
|
480
|
+
includeTurns: true,
|
|
481
|
+
});
|
|
482
|
+
const decoded = new Map();
|
|
483
|
+
const turns = asRecord(asRecord(result)?.thread)?.turns;
|
|
484
|
+
if (!Array.isArray(turns) || turns.length > 10_000)
|
|
485
|
+
return decoded;
|
|
486
|
+
for (const turn of turns) {
|
|
487
|
+
const items = asRecord(turn)?.items;
|
|
488
|
+
if (!Array.isArray(items) || items.length > 10_000)
|
|
489
|
+
continue;
|
|
490
|
+
for (const candidate of items) {
|
|
491
|
+
const item = asRecord(candidate);
|
|
492
|
+
const itemId = boundedString(item?.id, 256);
|
|
493
|
+
if (item?.type !== 'commandExecution' ||
|
|
494
|
+
!itemId ||
|
|
495
|
+
!['completed', 'failed', 'declined'].includes(String(item.status)))
|
|
496
|
+
continue;
|
|
497
|
+
const exitStatus = boundedInteger(item.exitCode);
|
|
498
|
+
decoded.set(itemId, exitStatus === null ? {} : { exitStatus });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return decoded;
|
|
334
502
|
}
|
|
335
503
|
async readDetachedHistory(session) {
|
|
336
504
|
// A reader is intentionally not a SessionResource: it owns no subscriptions,
|
|
@@ -353,7 +521,6 @@ export class CodexSessionRuntime {
|
|
|
353
521
|
async decodeHistory(process, threadId, resource) {
|
|
354
522
|
const response = await process.rpc.request('thread/read', { threadId, includeTurns: true });
|
|
355
523
|
if (resource) {
|
|
356
|
-
resource.spawnedAgentModels.clear();
|
|
357
524
|
for (const [childId, model] of decodeSpawnedAgentModels(response))
|
|
358
525
|
resource.spawnedAgentModels.set(childId, model);
|
|
359
526
|
}
|
|
@@ -525,6 +692,11 @@ export class CodexSessionRuntime {
|
|
|
525
692
|
const notificationUnsubscribe = process.rpc.onNotification((notification) => {
|
|
526
693
|
if (!resource.active)
|
|
527
694
|
return;
|
|
695
|
+
const childModel = decodeThreadSettingsModel(notification);
|
|
696
|
+
if (childModel &&
|
|
697
|
+
(resource.spawnedAgentModels.has(childModel.threadId) ||
|
|
698
|
+
resource.spawnedAgentModels.size < 256))
|
|
699
|
+
resource.spawnedAgentModels.set(childModel.threadId, childModel.model);
|
|
528
700
|
const resolvedRequestId = resolvedServerRequestId(notification);
|
|
529
701
|
if (resolvedRequestId) {
|
|
530
702
|
const pending = resource.pendingRequests.get(resolvedRequestId);
|
|
@@ -622,6 +794,35 @@ function threadTokenUsage(value) {
|
|
|
622
794
|
function asRecord(value) {
|
|
623
795
|
return value && typeof value === 'object' ? value : undefined;
|
|
624
796
|
}
|
|
797
|
+
function decodeAgentTaskPath(value) {
|
|
798
|
+
const subagent = asRecord(asRecord(value)?.subagent);
|
|
799
|
+
const spawn = asRecord(subagent?.thread_spawn);
|
|
800
|
+
const path = boundedString(spawn?.agent_path, 256);
|
|
801
|
+
return path?.startsWith('/') && !path.includes('..') ? path : undefined;
|
|
802
|
+
}
|
|
803
|
+
/** Captures the resolved model that Codex applies after spawning a child thread. */
|
|
804
|
+
function decodeThreadSettingsModel(notification) {
|
|
805
|
+
if (notification.method !== 'thread/settings/updated')
|
|
806
|
+
return undefined;
|
|
807
|
+
const params = asRecord(notification.params);
|
|
808
|
+
const threadId = boundedString(params?.threadId, 256);
|
|
809
|
+
const model = boundedString(asRecord(params?.threadSettings)?.model, 256);
|
|
810
|
+
return threadId && model ? { threadId, model } : undefined;
|
|
811
|
+
}
|
|
812
|
+
function childProcessKey(childThreadId, processId) {
|
|
813
|
+
return `${childThreadId}:${processId}`;
|
|
814
|
+
}
|
|
815
|
+
function boundedNonNegativeNumber(value) {
|
|
816
|
+
const number = typeof value === 'bigint' ? Number(value) : value;
|
|
817
|
+
return typeof number === 'number' && Number.isFinite(number) && number >= 0 ? number : null;
|
|
818
|
+
}
|
|
819
|
+
function boundedInteger(value) {
|
|
820
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
|
|
821
|
+
}
|
|
822
|
+
function boundedNonNegativeInteger(value) {
|
|
823
|
+
const number = boundedInteger(value);
|
|
824
|
+
return number !== null && number >= 0 ? number : null;
|
|
825
|
+
}
|
|
625
826
|
function decodeThreadStart(value) {
|
|
626
827
|
const id = asRecord(asRecord(value)?.thread)?.id;
|
|
627
828
|
if (typeof id !== 'string' || !id || id.length > 256)
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
const schema = `CREATE TABLE IF NOT EXISTS relay_sessions (id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, workspace_path TEXT NOT NULL, profile TEXT NOT NULL, model TEXT, branch TEXT, sandbox TEXT, approval_policy TEXT, thread_id TEXT, state TEXT NOT NULL, desired_state TEXT NOT NULL, active_turn_id TEXT, protocol_version TEXT, failure_count INTEGER NOT NULL DEFAULT 0, effective_skill_selection_json TEXT, last_org_plan_json TEXT, next_sequence INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS pending_interactions (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, request_id TEXT NOT NULL, kind TEXT NOT NULL, payload_json TEXT NOT NULL, turn_id TEXT, requested_at TEXT, resolved_at TEXT, outcome TEXT, operation_key TEXT, resolution_state TEXT NOT NULL DEFAULT 'active', PRIMARY KEY (session_id, request_id)); CREATE TABLE IF NOT EXISTS session_events (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, sequence INTEGER NOT NULL, occurred_at TEXT NOT NULL, type TEXT NOT NULL, payload_json TEXT NOT NULL, autopilot_outbox_id INTEGER, PRIMARY KEY (session_id, sequence), UNIQUE(session_id, autopilot_outbox_id)); CREATE TABLE IF NOT EXISTS idempotency_results (scope TEXT NOT NULL, key TEXT NOT NULL, status_code INTEGER NOT NULL, body_json TEXT NOT NULL, PRIMARY KEY (scope, key)); CREATE TABLE IF NOT EXISTS autopilot_sessions (session_id TEXT PRIMARY KEY REFERENCES relay_sessions(id) ON DELETE CASCADE, state TEXT NOT NULL, requested_enabled INTEGER NOT NULL, plan_identity TEXT, plan_fingerprint TEXT, generation INTEGER NOT NULL, no_progress_count INTEGER NOT NULL, next_evaluation_at TEXT, last_control_id TEXT, stop_reason TEXT, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS autopilot_controls (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, control_id TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, failure_code TEXT, turn_id TEXT, PRIMARY KEY (session_id, control_id)); CREATE TABLE IF NOT EXISTS autopilot_outbox (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, type TEXT NOT NULL, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL);`;
|
|
6
|
+
const schema = `CREATE TABLE IF NOT EXISTS relay_sessions (id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, workspace_path TEXT NOT NULL, profile TEXT NOT NULL, model TEXT, branch TEXT, sandbox TEXT, approval_policy TEXT, thread_id TEXT, state TEXT NOT NULL, desired_state TEXT NOT NULL, active_turn_id TEXT, protocol_version TEXT, failure_count INTEGER NOT NULL DEFAULT 0, effective_skill_selection_json TEXT, last_org_plan_json TEXT, next_sequence INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS pending_interactions (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, request_id TEXT NOT NULL, kind TEXT NOT NULL, payload_json TEXT NOT NULL, turn_id TEXT, requested_at TEXT, resolved_at TEXT, outcome TEXT, operation_key TEXT, resolution_state TEXT NOT NULL DEFAULT 'active', PRIMARY KEY (session_id, request_id)); CREATE TABLE IF NOT EXISTS session_events (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, sequence INTEGER NOT NULL, occurred_at TEXT NOT NULL, type TEXT NOT NULL, payload_json TEXT NOT NULL, autopilot_outbox_id INTEGER, PRIMARY KEY (session_id, sequence), UNIQUE(session_id, autopilot_outbox_id)); CREATE TABLE IF NOT EXISTS idempotency_results (scope TEXT NOT NULL, key TEXT NOT NULL, status_code INTEGER NOT NULL, body_json TEXT NOT NULL, PRIMARY KEY (scope, key)); CREATE TABLE IF NOT EXISTS autopilot_sessions (session_id TEXT PRIMARY KEY REFERENCES relay_sessions(id) ON DELETE CASCADE, state TEXT NOT NULL, requested_enabled INTEGER NOT NULL, plan_identity TEXT, plan_fingerprint TEXT, generation INTEGER NOT NULL, no_progress_count INTEGER NOT NULL, next_evaluation_at TEXT, last_control_id TEXT, stop_reason TEXT, lifecycle_json TEXT, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS autopilot_controls (session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, control_id TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, failure_code TEXT, turn_id TEXT, PRIMARY KEY (session_id, control_id)); CREATE TABLE IF NOT EXISTS autopilot_outbox (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL REFERENCES relay_sessions(id) ON DELETE CASCADE, type TEXT NOT NULL, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL);`;
|
|
7
7
|
export function migrate(database) {
|
|
8
8
|
database.exec(schema);
|
|
9
|
-
database.exec("CREATE INDEX IF NOT EXISTS
|
|
9
|
+
database.exec("CREATE INDEX IF NOT EXISTS session_events_autopilot_audit_tail_v3 ON session_events(session_id, type, sequence DESC) WHERE type IN ('autopilot.continuation-scheduled','autopilot.control-issued','autopilot.turn-started','autopilot.turn-failed','autopilot.progress-reset','autopilot.final-rejected','autopilot.executor-resumed','autopilot.process-monitoring','autopilot.process-result-consumed','autopilot.process-terminated','autopilot.updated','org-plan.attention-required','org-plan.attention-resolved')");
|
|
10
10
|
const columns = database.prepare('PRAGMA table_info(relay_sessions)').all();
|
|
11
11
|
if (!columns.some((column) => column.name === 'effective_skill_selection_json'))
|
|
12
12
|
database.exec('ALTER TABLE relay_sessions ADD COLUMN effective_skill_selection_json TEXT');
|
|
@@ -22,6 +22,11 @@ export function migrate(database) {
|
|
|
22
22
|
database.exec('ALTER TABLE relay_sessions ADD COLUMN sandbox TEXT');
|
|
23
23
|
if (!columns.some((column) => column.name === 'approval_policy'))
|
|
24
24
|
database.exec('ALTER TABLE relay_sessions ADD COLUMN approval_policy TEXT');
|
|
25
|
+
const autopilotColumns = database
|
|
26
|
+
.prepare('PRAGMA table_info(autopilot_sessions)')
|
|
27
|
+
.all();
|
|
28
|
+
if (!autopilotColumns.some((column) => column.name === 'lifecycle_json'))
|
|
29
|
+
database.exec('ALTER TABLE autopilot_sessions ADD COLUMN lifecycle_json TEXT');
|
|
25
30
|
const interactionColumns = database
|
|
26
31
|
.prepare('PRAGMA table_info(pending_interactions)')
|
|
27
32
|
.all();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
+
import { parsePersistedSupervisedLifecycle } from '../../features/autopilot/domain/supervised-lifecycle.js';
|
|
6
7
|
export class SqliteAutopilotStore {
|
|
7
8
|
db;
|
|
8
9
|
constructor(db) {
|
|
@@ -14,6 +15,7 @@ export class SqliteAutopilotStore {
|
|
|
14
15
|
.get(sessionId);
|
|
15
16
|
if (!row)
|
|
16
17
|
return null;
|
|
18
|
+
const lifecycle = parseLifecycle(row.lifecycle_json);
|
|
17
19
|
if (!['disabled', 'monitoring', 'backoff', 'attentionRequired', 'completed'].includes(String(row.state)) ||
|
|
18
20
|
![
|
|
19
21
|
'manualDisabled',
|
|
@@ -34,7 +36,8 @@ export class SqliteAutopilotStore {
|
|
|
34
36
|
Number(row.generation) < 0 ||
|
|
35
37
|
!Number.isSafeInteger(Number(row.no_progress_count)) ||
|
|
36
38
|
Number(row.no_progress_count) < 0 ||
|
|
37
|
-
![0, 1].includes(Number(row.requested_enabled))
|
|
39
|
+
![0, 1].includes(Number(row.requested_enabled)) ||
|
|
40
|
+
lifecycle === null)
|
|
38
41
|
return null;
|
|
39
42
|
return {
|
|
40
43
|
sessionId: String(row.session_id),
|
|
@@ -47,13 +50,16 @@ export class SqliteAutopilotStore {
|
|
|
47
50
|
nextEvaluationAt: row.next_evaluation_at === null ? null : String(row.next_evaluation_at),
|
|
48
51
|
lastControlId: row.last_control_id === null ? null : String(row.last_control_id),
|
|
49
52
|
stopReason: row.stop_reason,
|
|
53
|
+
...lifecycle,
|
|
50
54
|
updatedAt: String(row.updated_at),
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
save(state) {
|
|
54
58
|
this.db
|
|
55
|
-
.prepare('INSERT INTO autopilot_sessions (session_id,state,requested_enabled,plan_identity,plan_fingerprint,generation,no_progress_count,next_evaluation_at,last_control_id,stop_reason,updated_at) VALUES (
|
|
56
|
-
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.
|
|
59
|
+
.prepare('INSERT INTO autopilot_sessions (session_id,state,requested_enabled,plan_identity,plan_fingerprint,generation,no_progress_count,next_evaluation_at,last_control_id,stop_reason,lifecycle_json,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET state=excluded.state,requested_enabled=excluded.requested_enabled,plan_identity=excluded.plan_identity,plan_fingerprint=excluded.plan_fingerprint,generation=excluded.generation,no_progress_count=excluded.no_progress_count,next_evaluation_at=excluded.next_evaluation_at,last_control_id=excluded.last_control_id,stop_reason=excluded.stop_reason,lifecycle_json=excluded.lifecycle_json,updated_at=excluded.updated_at')
|
|
60
|
+
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking
|
|
61
|
+
? JSON.stringify({ executor: state.executor, blocking: state.blocking })
|
|
62
|
+
: null, state.updatedAt);
|
|
57
63
|
}
|
|
58
64
|
findControl(sessionId, controlId) {
|
|
59
65
|
const row = this.db
|
|
@@ -158,3 +164,15 @@ export class SqliteAutopilotStore {
|
|
|
158
164
|
this.db.prepare('DELETE FROM autopilot_sessions WHERE session_id = ?').run(sessionId);
|
|
159
165
|
}
|
|
160
166
|
}
|
|
167
|
+
function parseLifecycle(value) {
|
|
168
|
+
if (value === null || value === undefined)
|
|
169
|
+
return {};
|
|
170
|
+
if (typeof value !== 'string' || value.length > 256_000)
|
|
171
|
+
return null;
|
|
172
|
+
try {
|
|
173
|
+
return parsePersistedSupervisedLifecycle(JSON.parse(value)) ?? null;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
export const autopilotAuditEventTypes = [
|
|
7
7
|
'autopilot.turn-started',
|
|
8
8
|
'autopilot.turn-failed',
|
|
9
|
+
'autopilot.final-rejected',
|
|
10
|
+
'autopilot.executor-resumed',
|
|
11
|
+
'autopilot.process-monitoring',
|
|
12
|
+
'autopilot.process-result-consumed',
|
|
13
|
+
'autopilot.process-terminated',
|
|
9
14
|
'autopilot.updated',
|
|
10
15
|
'org-plan.attention-required',
|
|
11
16
|
'org-plan.attention-resolved',
|
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
export function autopilotAuditLabel(type, payload) {
|
|
8
8
|
if (type === 'autopilot.turn-started')
|
|
9
9
|
return 'Continued execution automatically';
|
|
10
|
+
if (type === 'autopilot.final-rejected')
|
|
11
|
+
return 'Kept incomplete supervised work active';
|
|
12
|
+
if (type === 'autopilot.executor-resumed')
|
|
13
|
+
return 'Resumed the assigned executor';
|
|
14
|
+
if (type === 'autopilot.process-monitoring')
|
|
15
|
+
return 'Monitoring executor background work';
|
|
16
|
+
if (type === 'autopilot.process-result-consumed')
|
|
17
|
+
return 'Consumed background work result';
|
|
18
|
+
if (type === 'autopilot.process-terminated')
|
|
19
|
+
return property(payload, 'reason') === 'resourceBudget'
|
|
20
|
+
? 'Stopped background work at its resource limit'
|
|
21
|
+
: 'Stopped executor background work';
|
|
10
22
|
if (type === 'autopilot.turn-failed') {
|
|
11
23
|
const code = property(payload, 'code');
|
|
12
24
|
return code === 'START_UNAVAILABLE'
|
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
export function orgPlanPosition(l1Position, l2Position) {
|
|
7
7
|
return l2Position === undefined ? `L${l1Position}` : `L${l1Position}.${l2Position}`;
|
|
8
8
|
}
|
|
9
|
+
export function parseOrgPlanAgentIdentity(nameOrPath) {
|
|
10
|
+
const name = nameOrPath.split('/').filter(Boolean).at(-1) ?? nameOrPath;
|
|
11
|
+
const match = /^(l([1-9]\d*)(?:_([1-9]\d*))?)(?:_g([1-9]\d*))?$/.exec(name);
|
|
12
|
+
if (!match)
|
|
13
|
+
return null;
|
|
14
|
+
return {
|
|
15
|
+
canonicalTaskName: match[1],
|
|
16
|
+
canonicalPosition: orgPlanPosition(Number(match[2]), match[3] ? Number(match[3]) : undefined),
|
|
17
|
+
generation: match[4] ? Number(match[4]) : 1,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
/** Converts the collaboration API's tool-safe task name into its canonical plan label. */
|
|
10
21
|
export function orgPlanAgentDisplayName(name) {
|
|
11
|
-
|
|
12
|
-
return match ? orgPlanPosition(Number(match[1]), match[2] ? Number(match[2]) : undefined) : name;
|
|
22
|
+
return parseOrgPlanAgentIdentity(name)?.canonicalPosition ?? name;
|
|
13
23
|
}
|