@zq-silk/yui 2.18.0 → 2.20.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 +2 -0
- package/dist/commands/controllerCommands.js +1 -0
- package/dist/commands/roleConfiguration.js +17 -0
- package/dist/controller/controller.js +10 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +26 -2
- package/dist/controller/messageHealth.js +354 -0
- package/dist/controller/runtime.js +9 -1
- package/dist/controller/runtimeLaunchCoordinator.js +12 -1
- package/dist/controller/taskHostTerminalRecovery.js +11 -0
- package/dist/executor/agentExecutor.js +7 -0
- package/dist/executor/fileRoleLaunchPlanner.js +22 -6
- 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/contextRecords.js +16 -0
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/sqliteStore.js +16 -0
- package/dist/storage/storageVersions.js +1 -1
- package/docs/managed-turn-and-session-runtime.md +96 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -0
- package/docs/release-workflow.md +49 -0
- package/docs/release-workflow.zh-CN.md +37 -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/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.
|
|
|
3
3
|
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
4
4
|
/** Existing full reconciliation pass only; no new timer, input or Session. */
|
|
5
5
|
export async function recoverTaskHostTerminals(home, store, onError, reconcile = reconcileAgentHostTurn) {
|
|
6
|
+
const observations = [];
|
|
6
7
|
for (const candidate of store.listRuntimeSessionCandidates({ scope: "task" })) {
|
|
7
8
|
const owner = candidate.owner;
|
|
8
9
|
if (owner.scope !== "task")
|
|
@@ -20,14 +21,24 @@ export async function recoverTaskHostTerminals(home, store, onError, reconcile =
|
|
|
20
21
|
attemptId: turn.attemptId, nativeTurnId: turn.nativeTurnId,
|
|
21
22
|
authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId }
|
|
22
23
|
} });
|
|
24
|
+
observations.push({ taskId: owner.taskId, roleName: owner.roleName,
|
|
25
|
+
nativeSessionId: currentProviderConversation(binding).conversationId, attemptId: turn.attemptId,
|
|
26
|
+
state: result.snapshot.state, reconciliation: result.reconciliation ?? null,
|
|
27
|
+
outcome: result.outcome, observedAt: new Date().toISOString() });
|
|
23
28
|
if (result.outcome === "rejected" || result.reconciliation === "unsupported") {
|
|
24
29
|
throw new Error(result.failure?.detail
|
|
25
30
|
?? "Host terminal reconciliation is unsupported; retain the original input and inspect Host/Inbox evidence.");
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
catch (error) {
|
|
34
|
+
if (!observations.some(item => item.taskId === owner.taskId && item.roleName === owner.roleName)) {
|
|
35
|
+
observations.push({ taskId: owner.taskId, roleName: owner.roleName,
|
|
36
|
+
nativeSessionId: currentProviderConversation(binding).conversationId, attemptId: turn.attemptId,
|
|
37
|
+
state: "unobservable", reconciliation: null, outcome: "unavailable", observedAt: new Date().toISOString() });
|
|
38
|
+
}
|
|
29
39
|
onError(new Error(`Task Host terminal recovery ${owner.taskId}/${owner.roleName}/${turn.attemptId}: `
|
|
30
40
|
+ `${error instanceof Error ? error.message : String(error)} No input was replayed.`, { cause: error }));
|
|
31
41
|
}
|
|
32
42
|
}
|
|
43
|
+
return observations;
|
|
33
44
|
}
|
|
@@ -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
|
}
|
|
@@ -54,6 +54,7 @@ export function renderRoleDetails(title, role, input) {
|
|
|
54
54
|
...(!isEnabled(role) ? [` Availability ${roleDisabledReason(role)}`] : []),
|
|
55
55
|
` Active Agent ${role.activeAgentId}`,
|
|
56
56
|
` Workspace ${role.workspace}`,
|
|
57
|
+
` Instance code ${role.instanceCode ?? "-"} (display only; next Session launch)`,
|
|
57
58
|
...("taskId" in role ? [
|
|
58
59
|
` Desired environment ${role.executionEnvironment === undefined
|
|
59
60
|
? "managed workspace"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function validateInstanceCode(value) {
|
|
2
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 24
|
|
3
|
+
|| !/^[\p{L}\p{N}\p{M}_-]+$/u.test(value)) {
|
|
4
|
+
throw new Error("Instance code must be 1–24 UTF-16 units of Unicode letters, numbers, marks, '_' or '-'; omit '@'.");
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
package/dist/role/role.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { validateInstanceCode } from "./instanceCode.js";
|
|
2
3
|
import { assertRoleEnabled, validateEnabled } from "./availability.js";
|
|
3
4
|
import { requireKnownFields } from "../domain/validation.js";
|
|
4
5
|
import { jobScopeKey } from "../job/jobDefinition.js";
|
|
@@ -49,6 +50,7 @@ export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalR
|
|
|
49
50
|
const timestamp = now.toISOString();
|
|
50
51
|
return validateTaskRole({
|
|
51
52
|
...cloneProfile(globalRole),
|
|
53
|
+
...(globalRole.instanceCode === undefined ? {} : { instanceCode: globalRole.instanceCode }),
|
|
52
54
|
schemaVersion: 1,
|
|
53
55
|
enabled: true,
|
|
54
56
|
launchRevision: 1,
|
|
@@ -75,6 +77,8 @@ export function updateGlobalRole(role, patch, now) {
|
|
|
75
77
|
updatedAt: now.toISOString()
|
|
76
78
|
};
|
|
77
79
|
clearProfileFields(updated, patch);
|
|
80
|
+
if (Object.hasOwn(patch, "instanceCode") && patch.instanceCode === undefined)
|
|
81
|
+
delete updated.instanceCode;
|
|
78
82
|
updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
|
|
79
83
|
? role.launchRevision
|
|
80
84
|
: role.launchRevision + 1;
|
|
@@ -94,6 +98,8 @@ export function updateRole(role, patch, now) {
|
|
|
94
98
|
updated.executionEnvironment = cloneJson(patch.executionEnvironment);
|
|
95
99
|
}
|
|
96
100
|
clearProfileFields(updated, patch);
|
|
101
|
+
if (Object.hasOwn(patch, "instanceCode") && patch.instanceCode === undefined)
|
|
102
|
+
delete updated.instanceCode;
|
|
97
103
|
updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
|
|
98
104
|
? role.launchRevision
|
|
99
105
|
: role.launchRevision + 1;
|
|
@@ -222,6 +228,7 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
|
|
|
222
228
|
const timestamp = now.toISOString();
|
|
223
229
|
return {
|
|
224
230
|
...cloneProfile(profile),
|
|
231
|
+
...(profile.instanceCode === undefined ? {} : { instanceCode: validateInstanceCode(profile.instanceCode) }),
|
|
225
232
|
enabled: true,
|
|
226
233
|
launchRevision: 1,
|
|
227
234
|
defaultAccess,
|
|
@@ -234,6 +241,8 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
|
|
|
234
241
|
};
|
|
235
242
|
}
|
|
236
243
|
function validateRoleOwner(role) {
|
|
244
|
+
if (role.instanceCode !== undefined)
|
|
245
|
+
validateInstanceCode(role.instanceCode);
|
|
237
246
|
validateEnabled(role.enabled);
|
|
238
247
|
if (role.schemaVersion !== 1) {
|
|
239
248
|
throw new Error("Role schema version is invalid.");
|
|
@@ -303,6 +312,7 @@ function assertSessionOwnerMatchesRole(role, sessions) {
|
|
|
303
312
|
}
|
|
304
313
|
function cloneRolePatch(patch) {
|
|
305
314
|
return {
|
|
315
|
+
...(patch.instanceCode === undefined ? {} : { instanceCode: validateInstanceCode(patch.instanceCode) }),
|
|
306
316
|
...(patch.enabled === undefined ? {} : { enabled: patch.enabled }),
|
|
307
317
|
...(patch.name === undefined ? {} : { name: requireSafeIdentity(patch.name, "Role name") }),
|
|
308
318
|
...(patch.activeAgentId === undefined
|
|
@@ -81,6 +81,7 @@ class BuiltinAgentEndpoint {
|
|
|
81
81
|
driver;
|
|
82
82
|
configuration;
|
|
83
83
|
get nativeAccess() { return this.driver.nativeAccess; }
|
|
84
|
+
get appliedGeneratedTitle() { return this.driver.appliedGeneratedTitle; }
|
|
84
85
|
get ownedProcessId() { return this.driver.ownedProcessId; }
|
|
85
86
|
get nativeAccountHome() { return this.driver.nativeAccountHome; }
|
|
86
87
|
capabilities;
|
|
@@ -120,7 +120,13 @@ export async function runAgentHost(input) {
|
|
|
120
120
|
const recentSteerAttempts = new Map();
|
|
121
121
|
await replayExitOutbox(input.home);
|
|
122
122
|
const updateSnapshot = (next) => {
|
|
123
|
-
|
|
123
|
+
const { appliedGeneratedTitle: _oldTitle, ...rest } = next;
|
|
124
|
+
snapshot = validateSnapshot({
|
|
125
|
+
...rest,
|
|
126
|
+
...(session?.appliedGeneratedTitle === undefined ? {} : {
|
|
127
|
+
appliedGeneratedTitle: session.appliedGeneratedTitle
|
|
128
|
+
})
|
|
129
|
+
});
|
|
124
130
|
};
|
|
125
131
|
const authorityFields = () => ({
|
|
126
132
|
...(session === undefined ? {} : { endpointImplementation: session.configuration.implementation }),
|
|
@@ -374,6 +380,8 @@ export async function runAgentHost(input) {
|
|
|
374
380
|
&& currentPayload.environment.YUI_ROLE === "operator")
|
|
375
381
|
? {}
|
|
376
382
|
: { sessionTitle: previousControl.sessionTitle }),
|
|
383
|
+
...(previousControl.previousSessionTitle === undefined
|
|
384
|
+
? {} : { previousSessionTitle: previousControl.previousSessionTitle }),
|
|
377
385
|
codexThread: disconnectedSession.configuration.threadOptions,
|
|
378
386
|
endpointImplementation: disconnectedSession.configuration.implementation,
|
|
379
387
|
...(ownedTurn === undefined ? {} : { ownedTurn }),
|
|
@@ -167,6 +167,9 @@ function validateProviderControl(control) {
|
|
|
167
167
|
throw new Error("Agent Host Provider session title is invalid.");
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
if (control.previousSessionTitle !== undefined) {
|
|
171
|
+
text(control.previousSessionTitle, "previousSessionTitle");
|
|
172
|
+
}
|
|
170
173
|
validateProviderAuthorityFence(control.authority);
|
|
171
174
|
}
|
|
172
175
|
function validateCodexThreadOptions(options) {
|
|
@@ -15,6 +15,9 @@ export function createRuntimeBinding(input) {
|
|
|
15
15
|
...(input.nativeSessionId === undefined
|
|
16
16
|
? {}
|
|
17
17
|
: { nativeSessionId: requireText(input.nativeSessionId, "Native session id") }),
|
|
18
|
+
...(input.appliedGeneratedTitle === undefined ? {} : {
|
|
19
|
+
appliedGeneratedTitle: requireText(input.appliedGeneratedTitle, "Applied generated title")
|
|
20
|
+
}),
|
|
18
21
|
...(input.providerAuthority === undefined
|
|
19
22
|
? {}
|
|
20
23
|
: { providerAuthority: validateProviderAuthorityFence(input.providerAuthority) })
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import { resolveTimeZone } from "../config/timeZone.js";
|
|
2
|
+
import { validateInstanceCode } from "../role/instanceCode.js";
|
|
2
3
|
const TITLE_SEPARATOR = " · ";
|
|
3
4
|
export const MAX_SESSION_TITLE_LENGTH = 80;
|
|
4
5
|
const TASK_TITLE_MAX_LENGTH = 20;
|
|
5
|
-
export function taskRoleSessionTitle(task, roleName) {
|
|
6
|
-
const prefix = `Yui ${roleLabel(roleName)} ${normalizeSegment(task.id)}
|
|
6
|
+
export function taskRoleSessionTitle(task, roleName, options = {}) {
|
|
7
|
+
const prefix = `Yui ${roleLabel(roleName)} ${normalizeSegment(task.id)}`
|
|
8
|
+
+ (roleName === "leader" && options.executionAuthority === "planning" ? " Draft" : "")
|
|
9
|
+
+ instanceSegment(options.instanceCode);
|
|
7
10
|
const title = displayTitle(normalizeSegment(task.title), TASK_TITLE_MAX_LENGTH);
|
|
8
11
|
const full = `${prefix}${TITLE_SEPARATOR}${title}`;
|
|
9
12
|
if (full.length <= MAX_SESSION_TITLE_LENGTH)
|
|
10
13
|
return full;
|
|
11
14
|
if (prefix.length + TITLE_SEPARATOR.length + 1 > MAX_SESSION_TITLE_LENGTH) {
|
|
12
|
-
|
|
15
|
+
throw new Error("Session title identity exceeds the maximum length; shorten the Role name or instance code.");
|
|
13
16
|
}
|
|
14
|
-
const titleLength = MAX_SESSION_TITLE_LENGTH - prefix.length - TITLE_SEPARATOR.length
|
|
17
|
+
const titleLength = MAX_SESSION_TITLE_LENGTH - prefix.length - TITLE_SEPARATOR.length;
|
|
15
18
|
return `${prefix}${TITLE_SEPARATOR}${displayTitle(title, Math.max(titleLength, 1))}`;
|
|
16
19
|
}
|
|
17
|
-
export function resolveTaskRoleSessionTitle(existingTitle, task, roleName) {
|
|
20
|
+
export function resolveTaskRoleSessionTitle(existingTitle, task, roleName, options = {}) {
|
|
18
21
|
if (existingTitle !== undefined
|
|
19
22
|
&& existingTitle.length > 0
|
|
20
23
|
&& existingTitle.length <= MAX_SESSION_TITLE_LENGTH
|
|
21
|
-
&& !/[\r\n\0]/u.test(existingTitle)
|
|
24
|
+
&& !/[\r\n\0]/u.test(existingTitle)
|
|
25
|
+
&& existingTitle !== options.generatedTitle) {
|
|
22
26
|
return existingTitle;
|
|
23
27
|
}
|
|
24
|
-
return taskRoleSessionTitle(task, roleName);
|
|
28
|
+
return taskRoleSessionTitle(task, roleName, options);
|
|
25
29
|
}
|
|
26
|
-
export function operatorSessionTitle(now, configuredTimeZone) {
|
|
30
|
+
export function operatorSessionTitle(now, configuredTimeZone, instanceCode) {
|
|
27
31
|
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
28
32
|
throw new TypeError("Operator session title date is invalid.");
|
|
29
33
|
}
|
|
@@ -35,7 +39,23 @@ export function operatorSessionTitle(now, configuredTimeZone) {
|
|
|
35
39
|
if (parts.month === undefined || parts.day === undefined) {
|
|
36
40
|
throw new TypeError("Operator session title date is unavailable.");
|
|
37
41
|
}
|
|
38
|
-
|
|
42
|
+
const prefix = instanceCode === undefined
|
|
43
|
+
? `Yui${TITLE_SEPARATOR}Operator`
|
|
44
|
+
: `Yui Operator${instanceSegment(instanceCode)}`;
|
|
45
|
+
return `${prefix}${TITLE_SEPARATOR}${parts.month}${parts.day}`;
|
|
46
|
+
}
|
|
47
|
+
export function globalRoleSessionTitle(roleName, now, configuredTimeZone, instanceCode) {
|
|
48
|
+
if (roleName === "operator")
|
|
49
|
+
return operatorSessionTitle(now, configuredTimeZone, instanceCode);
|
|
50
|
+
if (instanceCode === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const title = `Yui ${roleLabel(roleName)}${instanceSegment(instanceCode)}`;
|
|
53
|
+
if (title.length > MAX_SESSION_TITLE_LENGTH)
|
|
54
|
+
throw new Error("Session title identity exceeds the maximum length.");
|
|
55
|
+
return title;
|
|
56
|
+
}
|
|
57
|
+
function instanceSegment(value) {
|
|
58
|
+
return value === undefined ? "" : ` @${validateInstanceCode(value)}`;
|
|
39
59
|
}
|
|
40
60
|
function displayTitle(value, maxLength) {
|
|
41
61
|
if (value.length <= maxLength)
|
|
@@ -384,6 +384,7 @@ class CodexStructuredProviderSession {
|
|
|
384
384
|
onActivity;
|
|
385
385
|
mirror;
|
|
386
386
|
nativeAccountHome;
|
|
387
|
+
appliedGeneratedTitle;
|
|
387
388
|
nativeAccess;
|
|
388
389
|
#turnAttempts = new Map();
|
|
389
390
|
adapterId = "codex";
|
|
@@ -445,12 +446,20 @@ class CodexStructuredProviderSession {
|
|
|
445
446
|
throw error;
|
|
446
447
|
}
|
|
447
448
|
}
|
|
448
|
-
|
|
449
|
+
const threadMetadata = nativeConfiguration.thread;
|
|
450
|
+
const nativeTitle = typeof threadMetadata === "object" && threadMetadata !== null
|
|
451
|
+
&& !Array.isArray(threadMetadata) ? threadMetadata.name : undefined;
|
|
452
|
+
// A native rename belongs to the user. Missing metadata is not evidence
|
|
453
|
+
// that an old title is still ours; leave it alone on resume.
|
|
454
|
+
let appliedGeneratedTitle;
|
|
455
|
+
if (control.sessionTitle !== undefined && (control.mode === "new"
|
|
456
|
+
|| (control.previousSessionTitle !== undefined && nativeTitle === control.previousSessionTitle))) {
|
|
449
457
|
try {
|
|
450
458
|
await runtime.setConversationName({
|
|
451
459
|
conversationId,
|
|
452
460
|
name: control.sessionTitle
|
|
453
461
|
});
|
|
462
|
+
appliedGeneratedTitle = control.sessionTitle;
|
|
454
463
|
}
|
|
455
464
|
catch (error) {
|
|
456
465
|
onDiagnostic?.({
|
|
@@ -465,6 +474,7 @@ class CodexStructuredProviderSession {
|
|
|
465
474
|
}
|
|
466
475
|
const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
|
|
467
476
|
session.nativeAccess = createCodexNativeAccess(conversationId, initialized, channel, openingMessages, nativeConfiguration);
|
|
477
|
+
session.appliedGeneratedTitle = appliedGeneratedTitle;
|
|
468
478
|
session.#activeTurnId = resumedActiveTurnId;
|
|
469
479
|
const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
|
|
470
480
|
session.#clientOwnedTurnId = ownedTurn?.turnId;
|
|
@@ -246,6 +246,7 @@ export class TmuxSessionHost {
|
|
|
246
246
|
agentId: request.agentId,
|
|
247
247
|
adapterId: request.adapterId,
|
|
248
248
|
effective: request.effective,
|
|
249
|
+
...(planned.generatedTitle === undefined ? {} : { generatedTitle: planned.generatedTitle }),
|
|
249
250
|
...(planned.launch.env.YUI_SESSION_TITLE === undefined
|
|
250
251
|
? {}
|
|
251
252
|
: { sessionTitle: planned.launch.env.YUI_SESSION_TITLE }),
|
|
@@ -436,6 +437,9 @@ export class TmuxSessionHost {
|
|
|
436
437
|
roleName: request.owner.roleName
|
|
437
438
|
}),
|
|
438
439
|
hostCreated,
|
|
440
|
+
...(providerSnapshot?.appliedGeneratedTitle === undefined ? {} : {
|
|
441
|
+
appliedGeneratedTitle: providerSnapshot.appliedGeneratedTitle
|
|
442
|
+
}),
|
|
439
443
|
...((providerSnapshot?.nativeSessionId ?? nativeSessionId) === undefined
|
|
440
444
|
? {}
|
|
441
445
|
: { nativeSessionId: providerSnapshot?.nativeSessionId ?? nativeSessionId }),
|
|
@@ -54,6 +54,22 @@ export function queryContextRecords(db, taskId, query) {
|
|
|
54
54
|
const from = `${table} r${candidate ? ", json_each(r.payload, '$.candidates') candidate" : ""}`;
|
|
55
55
|
const where = ["r.task_id = @taskId"];
|
|
56
56
|
const params = { taskId };
|
|
57
|
+
if (query.eventAttemptId !== undefined) {
|
|
58
|
+
if (query.family !== "task-event")
|
|
59
|
+
throw new Error("Attempt selection requires Events.");
|
|
60
|
+
where.push("json_extract(r.payload, '$.payload.attemptId') = @eventAttemptId");
|
|
61
|
+
params.eventAttemptId = query.eventAttemptId;
|
|
62
|
+
}
|
|
63
|
+
if (query.queuedForRole !== undefined) {
|
|
64
|
+
if (query.family !== "task-message")
|
|
65
|
+
throw new Error("Queued Role selection requires Messages.");
|
|
66
|
+
where.push(`json_extract(r.payload, '$.recipient.roleName') = @queuedRole
|
|
67
|
+
AND json_extract(r.payload, '$.recipient.ownerRunId') IS NOT NULL
|
|
68
|
+
AND json_extract(r.payload, '$.inbox') IS NULL
|
|
69
|
+
AND json_extract(r.payload, '$.continuation.runId') IS NULL
|
|
70
|
+
AND COALESCE(json_extract(r.payload, '$.inputControl.action'), '') != 'steer'`);
|
|
71
|
+
params.queuedRole = query.queuedForRole;
|
|
72
|
+
}
|
|
57
73
|
if (query.ids !== undefined) {
|
|
58
74
|
where.push(`${id} IN (SELECT value FROM json_each(@ids))`);
|
|
59
75
|
params.ids = JSON.stringify(query.ids);
|
|
@@ -276,6 +276,16 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
276
276
|
// No payload rewriting or historical revision invention. A newly opened
|
|
277
277
|
// reader starts empty; zero is the initial token until the first mutation.
|
|
278
278
|
sql: TASK_READ_REVISION_SCHEMA_SQL
|
|
279
|
+
}, {
|
|
280
|
+
fromVersion: "1.24",
|
|
281
|
+
toVersion: "1.25",
|
|
282
|
+
name: "role-instance-code-session-titles",
|
|
283
|
+
introducedIn: "next",
|
|
284
|
+
sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
285
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
286
|
+
// Optional Role presentation and generated-title provenance. Never infer
|
|
287
|
+
// ownership of historical titles, rewrite frozen profiles, or fill defaults.
|
|
288
|
+
sql: ""
|
|
279
289
|
}]);
|
|
280
290
|
export function storageMinorUpgradePlan(from) {
|
|
281
291
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -1088,6 +1088,22 @@ export class SqliteTaskStore {
|
|
|
1088
1088
|
}
|
|
1089
1089
|
if (query.operatorNotice)
|
|
1090
1090
|
where.push("json_extract(payload, '$.inputControl.requestId') LIKE 'operator-notice:%'");
|
|
1091
|
+
if (query.awaitingDelivery)
|
|
1092
|
+
where.push(`(json_extract(payload, '$.delivery') IS NULL
|
|
1093
|
+
OR json_extract(payload, '$.delivery.via') = 'transport')
|
|
1094
|
+
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
1095
|
+
AND json_extract(payload, '$.inbox.retirement') IS NULL
|
|
1096
|
+
AND (json_extract(payload, '$.inputControl.action') = 'queue'
|
|
1097
|
+
OR json_extract(payload, '$.interruptThen') IS NOT NULL
|
|
1098
|
+
OR json_extract(payload, '$.control.outcome') IN ('pending','delivery-unknown'))`);
|
|
1099
|
+
if (query.excludeHealthNotices)
|
|
1100
|
+
where.push("COALESCE(json_extract(payload, '$.inputControl.requestId'), '') NOT LIKE 'operator-notice:message-health:%'");
|
|
1101
|
+
if (query.systemBodyPrefix !== undefined) {
|
|
1102
|
+
where.push(`json_extract(payload, '$.kind') = 'system'
|
|
1103
|
+
AND json_extract(payload, '$.author.type') = 'system'
|
|
1104
|
+
AND substr(json_extract(payload, '$.body'), 1, length(@bodyPrefix)) = @bodyPrefix`);
|
|
1105
|
+
params.bodyPrefix = query.systemBodyPrefix;
|
|
1106
|
+
}
|
|
1091
1107
|
if (query.afterSequence !== undefined) {
|
|
1092
1108
|
where.push("seq > @after");
|
|
1093
1109
|
params.after = query.afterSequence;
|
|
@@ -643,3 +643,99 @@ Operator storage diagnosis/upgrade also reaches the dedicated migration entry
|
|
|
643
643
|
without first requiring the Home to already have the target storage version.
|
|
644
644
|
Completion, cancellation and archive have distinct authority and resource
|
|
645
645
|
boundaries. Storage changes use the single [upgrade contract](sqlite-control-plane-design.md).
|
|
646
|
+
|
|
647
|
+
## Message delivery health
|
|
648
|
+
|
|
649
|
+
The Controller's existing full reconciliation observes Task Role notification
|
|
650
|
+
mailboxes, Assignment continuations, Global Role input and the Operator attention
|
|
651
|
+
mailbox. It reads the real `pending` suffix separately from `processing`, never
|
|
652
|
+
the compatibility `getPendingWakeup` projection. It does not run an Agent polling
|
|
653
|
+
loop, acknowledge input, release a claim, resend an uncertain input or start an
|
|
654
|
+
unavailable Operator.
|
|
655
|
+
|
|
656
|
+
`yui controller status --verbose --json` exposes a cached, timestamped
|
|
657
|
+
`identity.runtime.observations.messageHealth` view (the Controller RPC exposes
|
|
658
|
+
`runtime.messageHealth`). It includes counts, oldest wait/occupancy, exact
|
|
659
|
+
Session/attempt/wake/message references, answered input references, recorded
|
|
660
|
+
delivery outcome, retry ownership and the existing Host reconciliation evidence.
|
|
661
|
+
`unobserved` is not proof of Host health. Inspect its exact Event/Message read
|
|
662
|
+
commands for durable originals. Status reads perform no database scans or probes.
|
|
663
|
+
The view shows at most 64 owners and reports omissions; records remain readable
|
|
664
|
+
through the existing Task event and Global Role message commands after restart.
|
|
665
|
+
|
|
666
|
+
An eligible stalled delivery warns after 5 minutes and escalates once at
|
|
667
|
+
30 minutes, or after 15 minutes if the queue at least doubles and reaches eight
|
|
668
|
+
items. A single old input is sufficient. Sampling is on the configured full-pass
|
|
669
|
+
interval (default 120 seconds, permitted 5–300 seconds): with a responsive
|
|
670
|
+
Controller, detection occurs on the first completed pass after the threshold.
|
|
671
|
+
These are not wall-clock delivery guarantees during downtime, blocked I/O or
|
|
672
|
+
unavailable Providers. Accepted long Turns without contrary exact Host evidence
|
|
673
|
+
are busy, not faults; explicit stops, disabled Roles, human ownership, native
|
|
674
|
+
pause and matching open input waits are distinct from blocked delivery. Valid
|
|
675
|
+
bounded retry remains owned by its existing recovery mechanism. Historical
|
|
676
|
+
missing ACKs without a new waiting suffix do not generate backlog alerts.
|
|
677
|
+
|
|
678
|
+
Each affected delivery owner has one open diagnostic episode: at most a warning,
|
|
679
|
+
one escalation and a backlog-clear observation. Concurrent arrivals coalesce;
|
|
680
|
+
they do not each call a model. Task evidence uses ordinary `message.health.<role>`
|
|
681
|
+
Events; Global evidence uses record-only system Messages. These are append-only
|
|
682
|
+
content in the existing open Event/Message formats, not a new storage envelope,
|
|
683
|
+
schema version or scheduling aggregate. The newest evidence reconstructs
|
|
684
|
+
notification deduplication after restart. Each notice reuses the ordinary
|
|
685
|
+
Operator Global Message queue with a stable request ID, independent of the
|
|
686
|
+
affected Task's occupied mailbox. Only one unsent engineering notice is admitted;
|
|
687
|
+
later evidence stays durable and is reconsidered on subsequent full passes.
|
|
688
|
+
|
|
689
|
+
If Operator is absent/stopped/uncontrolled, evidence remains generated but no
|
|
690
|
+
new Session is started. If it is busy, the notice stays saved; failures retain
|
|
691
|
+
their exact message and reason and are not replayed. Diagnostics distinguish
|
|
692
|
+
generated evidence, saved notice, transport acceptance, Provider delivery and
|
|
693
|
+
explicit handling. An alert ACK is never recovery. Only an observed empty
|
|
694
|
+
delivery backlog closes an episode; an explicit pause or merely releasing the
|
|
695
|
+
old processing claim while a pending suffix remains does not. Backlog clearance
|
|
696
|
+
itself proves neither business handling nor completion, and grants no recovery
|
|
697
|
+
authority. Moving all pending work to a new processing owner is not clearance:
|
|
698
|
+
retained processing with a deferred delivery remains observable, including its
|
|
699
|
+
exact attempt and rejection detail, even when the current Turn is completed.
|
|
700
|
+
|
|
701
|
+
### Management recovery guidance
|
|
702
|
+
|
|
703
|
+
Notices include the current observation time, exact identities, original read
|
|
704
|
+
commands and conditional recovery guidance, also available in the cached
|
|
705
|
+
`recovery` field. A notice delayed by Operator unavailability uses current facts
|
|
706
|
+
when it is finally saved, not the original incident's intervention advice.
|
|
707
|
+
Already-saved notices remain immutable evidence and are not resent merely to
|
|
708
|
+
update advice: always refresh status and exact Session/wake/message evidence
|
|
709
|
+
before acting. A command template is neither authorization nor proof that a
|
|
710
|
+
later invocation still targets the observed Session; current CLI fences apply.
|
|
711
|
+
|
|
712
|
+
An affected Leader is supervised by Operator; a subordinate Task Role by its
|
|
713
|
+
authorized Leader or Operator. Normal busy work, legitimate pause and owned
|
|
714
|
+
bounded retry receive non-intervention guidance. For an established retained
|
|
715
|
+
Task Host barrier, the supervisor may consider the existing targeted
|
|
716
|
+
`task role session stop` path without waiting for a complete engineering repair
|
|
717
|
+
or perfect historical projections. Inspect exact native/background execution,
|
|
718
|
+
event persistence, authority and cleanup consequences first. Unpersisted Host
|
|
719
|
+
facts must not be destroyed. A saved stop request is not successful physical
|
|
720
|
+
cleanup; retain failure evidence when cleanup cannot be proven. Existing
|
|
721
|
+
authorized lifecycle/delivery may reuse the retained conversation after proven
|
|
722
|
+
stop; verify its identity. Deliberate `task role session new` is a different,
|
|
723
|
+
explicit replacement decision, requiring durable progress and fenced cleanup.
|
|
724
|
+
Goal `stalled` alone neither establishes the cause nor warrants clearing Goal.
|
|
725
|
+
|
|
726
|
+
In this candidate, `task wake retry` requires a proven-rejected, unconsumed
|
|
727
|
+
exact claim, a corrected cause and no accepted/uncertain native execution.
|
|
728
|
+
`task wake resolve` supports an unknown exact claim after shared-native
|
|
729
|
+
quiescence; it releases scheduling without replay or semantic acknowledgement.
|
|
730
|
+
Deferred delivery meets neither precondition. Do not assume another recovery
|
|
731
|
+
package's extended behavior is installed. For Global Roles, this candidate
|
|
732
|
+
does not establish a dedicated targeted Host-stop command: inspect original
|
|
733
|
+
input/control and `session retry <role> show`, then discover current authorized
|
|
734
|
+
capabilities. There is no guessed Task equivalent or stop-all fallback.
|
|
735
|
+
|
|
736
|
+
On refusal or failure, preserve the exact receipt, original inputs and work,
|
|
737
|
+
and escalate only the missing authority, capability or cleanup evidence.
|
|
738
|
+
Verify management request, physical stop, conversation reuse/replacement,
|
|
739
|
+
actual subsequent Provider acceptance and business handling separately.
|
|
740
|
+
Missing evidence remains unverified; no automatic recovery, replay, forced
|
|
741
|
+
ACK, model/account/permission change or Controller-wide restart is introduced.
|
|
@@ -480,3 +480,68 @@ Task。Operator 诊断和 Controller 重启不先要求一个健康的 Controlle
|
|
|
480
480
|
也能到达专用的迁移入口,而不先要求 Home 已经处于目标存储版本。完成、取消和归档具有
|
|
481
481
|
彼此不同的权限和资源边界。存储变更使用唯一的
|
|
482
482
|
[升级合同](sqlite-control-plane-design.zh-CN.md)。
|
|
483
|
+
|
|
484
|
+
## 消息投递健康度
|
|
485
|
+
|
|
486
|
+
Controller 在既有 full reconciliation 中观察 Task Role 通知 mailbox、
|
|
487
|
+
Assignment continuation、Global Role 输入及 Operator attention mailbox。
|
|
488
|
+
分别读取真实 `pending` 后缀与 `processing`,不把 `getPendingWakeup` 的兼容投影
|
|
489
|
+
当作底层队列;不启动 Agent 私有轮询、不自动 ACK、释放占用或重放未知输入。
|
|
490
|
+
|
|
491
|
+
`yui controller status --verbose --json` 的
|
|
492
|
+
`identity.runtime.observations.messageHealth` 提供带采样时间的缓存诊断;Controller RPC 对应
|
|
493
|
+
`runtime.messageHealth`。可查看数量、最老等待与占用时长、精确 Session/attempt/wake/
|
|
494
|
+
message、已回答输入、投递证据、重试归属与既有 Host 核对结果。`unobserved` 不代表
|
|
495
|
+
Host 健康。按其中的 Event/Message 读取命令核对持久原始证据。状态读取不会扫描数据库
|
|
496
|
+
或发起探测;最多展示 64 个 owner 并显式报告省略数量,重启后原始记录仍可读取。
|
|
497
|
+
|
|
498
|
+
符合异常条件的投递等待在 5 分钟后预警,30 分钟后最多升级一次;若队列至少翻倍且达到
|
|
499
|
+
8 条,可在 15 分钟后提前升级。仅一条长期不推进的输入也会检测。采样复用 full-pass
|
|
500
|
+
间隔(默认 120 秒,可设 5–300 秒),健康响应的 Controller 在超过阈值后的首次完整
|
|
501
|
+
检查发现;停机、I/O 堵塞和 Provider 不可用期间不保证墙钟时限或即时送达。没有精确
|
|
502
|
+
Host 反证的 accepted 长 Turn 视为忙碌;显式停止、Role 禁用、human 所有权、原生
|
|
503
|
+
暂停、匹配的未回答输入等待单独区分。有效的有界重试仍由既有恢复机制拥有。只有历史
|
|
504
|
+
缺 ACK、没有新待投递后缀,不会生成积压告警。
|
|
505
|
+
|
|
506
|
+
按受影响的投递 owner 合并一个未关闭异常,最多保存预警、升级和积压消除三个阶段,
|
|
507
|
+
并发到达不会逐条调用模型。Task 使用普通 `message.health.<role>` Event,Global
|
|
508
|
+
使用 record-only system Message,都是既有开放内容格式中的追加证据,不新增存储
|
|
509
|
+
envelope、schema 或调度权威。最新证据用于重启后的去重。告警使用稳定 request ID,
|
|
510
|
+
走独立于受影响 Task 占用的 Operator Global Message 通道;最多保留一条未发送的
|
|
511
|
+
工程通知,其余待告警证据持久保留并在后续 full pass 检查。
|
|
512
|
+
|
|
513
|
+
Operator 缺席、已停止或没有受控 binding 时,不自动创建 Session;忙碌时保留已保存
|
|
514
|
+
通知,失败时保留原 Message 和原因、不重放。诊断区分已生成证据、已保存通知、传输
|
|
515
|
+
接受、Provider 送达与显式处置。告警 ACK 不关闭异常。只有观察到投递积压消失才记录
|
|
516
|
+
恢复;显式暂停、或仅释放旧 processing 但 pending 仍在,不算恢复。积压消除也不证明
|
|
517
|
+
业务处理完成、Task 验收或获得任何额外恢复权限。
|
|
518
|
+
pending 全部转入新 processing owner 也不算清除:即便当前 Turn 已完成,持续 deferred
|
|
519
|
+
的 processing 仍保留监测及精确 attempt、投递拒绝详情。
|
|
520
|
+
|
|
521
|
+
### 管理恢复引导
|
|
522
|
+
|
|
523
|
+
通知直接包含当前观测时间、精确身份、原始读取命令及有前提的恢复引导,缓存诊断的
|
|
524
|
+
`recovery` 字段也提供同一引导。Operator 不可用导致延迟保存的通知使用保存当时的
|
|
525
|
+
当前观测,不复制旧异常的操作建议;已保存通知不重写,也不为更新建议重复发送。
|
|
526
|
+
执行前必须刷新 status 和准确 Session/wake/message 原始证据。命令模板不是授权,
|
|
527
|
+
也不能固定未来调用的目标身份;实际操作仍受现有 CLI 围栏约束。
|
|
528
|
+
|
|
529
|
+
Leader 自身受阻由 Operator 处理,下属 Task Role 由有权限的 Leader 或 Operator
|
|
530
|
+
处理。正常忙碌、合法暂停、有界重试仍拥有恢复时,不推荐打断。确认 Task Host 遗留
|
|
531
|
+
占用后,可评估已有准确 `task role session stop` 管理路径,不必等待完整工程修复包
|
|
532
|
+
或所有历史投影对齐;先核对准确原生/后台执行、事件持久化、权限和清理影响,不能丢失
|
|
533
|
+
Host 中尚未持久化的事实。stop 请求保存不等于物理清理成功,失败时保留原证据。
|
|
534
|
+
证明停止后,既有授权生命周期/投递可在支持时复用原 conversation,须验证实际身份。
|
|
535
|
+
`task role session new` 则是另行明确选择新 Session,需保存进展并通过清理围栏。
|
|
536
|
+
仅显示 Goal stalled 不能确定根因,也不能据此要求清 Goal。
|
|
537
|
+
|
|
538
|
+
本候选的 `task wake retry` 仅适用已纠正原因、原准确 claim 仍未消费的明确 rejected,
|
|
539
|
+
且不能存在已接受或不确定原生执行;`task wake resolve` 仅支持准确 unknown claim
|
|
540
|
+
在共享原生静止证明后释放调度,不重放、不 ACK。deferred 不满足二者前提,不能假定
|
|
541
|
+
其他恢复包的扩展行为已经安装。GlobalRole 未确认有专用准确 Host-stop 命令时,读取
|
|
542
|
+
原输入/control 和 `session retry <role> show`,再发现当前权限下的能力;不臆造
|
|
543
|
+
Task 等价命令,也不退化为 stop-all。
|
|
544
|
+
|
|
545
|
+
动作拒绝或失败后,保留精确回执、原输入和工作区,只升级缺失的权限、能力或清理证据。
|
|
546
|
+
分别验证管理请求、物理停止、原会话复用/显式替换、后续实际 Provider 接受及业务处置;
|
|
547
|
+
缺证据仍标为未验证。不新增自动恢复、重放、强制 ACK、模型/账号/权限变更或全局重启。
|