termdock 1.4.345 → 1.4.347
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/server/agent/collaborationCli.js +3 -0
- package/dist/server/agent/collaborationStore.js +40 -12
- package/dist/server/agent/integrationCli.js +16 -2
- package/dist/server/agent/integrationServer.js +61 -32
- package/dist/server/agent/integrationSessions.js +36 -8
- package/dist/server/agent/integrationShellWait.js +33 -0
- package/dist/server/agent/integrationStore.js +10 -5
- package/dist/server/agent/nativeProcessIdentity.js +21 -0
- package/dist/server/agent/sessionLaunchDiagnostics.js +2 -0
- package/dist/server/agent/sessionLaunchState.js +35 -1
- package/dist/server/routes/terminal.js +72 -26
- package/docs/integrations.md +40 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -69,6 +69,9 @@ export const COLLAB_HELP = `td collab — durable messages; no agent-specific ho
|
|
|
69
69
|
消息历史只覆盖仍保留记录;不存在的 after-id 返回 MESSAGE_NOT_FOUND,不能据此声称历史完整。
|
|
70
70
|
session create --group <id> --launch-profile <id> --cwd <absolute-path> --idempotency-key <key>
|
|
71
71
|
session get <id> | session restore <id> --idempotency-key <key>
|
|
72
|
+
启动一致性/原shell有限核验需服务1.4.346:session_launch_reload_fence/session_shell_startup_wait。
|
|
73
|
+
只等同一承载的前台shell证明,不判断应用input-ready,不自动重试失败启动。
|
|
74
|
+
独立Agent数据目录可由管理员在profile配置nativeIdentity;需Linux及session_profile_native_identity。
|
|
72
75
|
get 可对账 tmux recovery 后仍指向旧 pane 的 IntegrationSession;需 Linux 服务1.4.328、session_recovery_binding_reconcile=true。
|
|
73
76
|
仅凭成功恢复记录、唯一归属/承载与当前精确 native writer-lock 证明更新 binding/generation;证据不足仍拒绝。
|
|
74
77
|
不另起 Agent、不重放已写入消息;成功后以 get 返回的 generation/terminal_binding 为准。
|
|
@@ -467,6 +467,12 @@ export class CollaborationStore {
|
|
|
467
467
|
const group = this.getGroup(groupId);
|
|
468
468
|
if (!group || group.deleted || !context || typeof context !== 'object')
|
|
469
469
|
return;
|
|
470
|
+
if (this.applyContext(group, context))
|
|
471
|
+
this.persist();
|
|
472
|
+
}
|
|
473
|
+
applyContext(group, context) {
|
|
474
|
+
if (group.deleted)
|
|
475
|
+
return false;
|
|
470
476
|
let changed = false;
|
|
471
477
|
const rules = context.instructions;
|
|
472
478
|
if (rules && validRevision(rules) && typeof rules.text === 'string' && Buffer.byteLength(rules.text) <= 8192
|
|
@@ -488,8 +494,7 @@ export class CollaborationStore {
|
|
|
488
494
|
delete group.roles[id];
|
|
489
495
|
changed = true;
|
|
490
496
|
}
|
|
491
|
-
|
|
492
|
-
this.persist();
|
|
497
|
+
return changed;
|
|
493
498
|
}
|
|
494
499
|
removeSession(sessionId) {
|
|
495
500
|
const count = this.document.groups.filter(group => group.sessionIds.includes(sessionId)).length;
|
|
@@ -712,12 +717,17 @@ export class CollaborationStore {
|
|
|
712
717
|
if (existing && (!existing.federated || (existing.deleted && (!group.deleted || existing.updatedAt >= group.updatedAt))
|
|
713
718
|
|| (!group.deleted && existing.updatedAt > group.updatedAt)))
|
|
714
719
|
return;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
720
|
+
// Directory refreshes repeat the same group every few seconds. Compare
|
|
721
|
+
// content as well as the timestamp: equal timestamps can carry real edits.
|
|
722
|
+
if (existing && stableJson(existing) === stableJson(group))
|
|
723
|
+
return;
|
|
724
|
+
const incoming = structuredClone(group.deleted && existing && !existing.deleted
|
|
725
|
+
? { ...group, updatedAt: Math.max(group.updatedAt, existing.updatedAt + 1) } : group);
|
|
726
|
+
if (existing)
|
|
727
|
+
this.applyContext(incoming, this.context(group.id));
|
|
728
|
+
if (existing && stableJson(existing) === stableJson(incoming))
|
|
729
|
+
return;
|
|
730
|
+
this.document.groups = [...this.document.groups.filter((item) => item.id !== group.id), incoming];
|
|
721
731
|
if (group.deleted)
|
|
722
732
|
this.document.messages = this.document.messages.filter((item) => item.groupId !== group.id);
|
|
723
733
|
this.persist();
|
|
@@ -861,16 +871,34 @@ export class CollaborationStore {
|
|
|
861
871
|
const message = this.getMessage(id);
|
|
862
872
|
if (!message)
|
|
863
873
|
throw new CollaborationError('MESSAGE_NOT_FOUND', 'Message not found or no longer retained', 404);
|
|
864
|
-
|
|
865
|
-
|
|
874
|
+
return this.messageReceipt(message, this.document.messages.filter(reply => reply.replyTo === id), raw);
|
|
875
|
+
}
|
|
876
|
+
/** Reconcile message events in one pass without rescanning the complete
|
|
877
|
+
* history for every receipt or formatting snapshots which events omit. */
|
|
878
|
+
snapshotMessagesWithReceipts(groups) {
|
|
879
|
+
const replies = new Map();
|
|
880
|
+
for (const message of this.document.messages)
|
|
881
|
+
if (message.replyTo) {
|
|
882
|
+
const list = replies.get(message.replyTo) ?? [];
|
|
883
|
+
list.push(message);
|
|
884
|
+
replies.set(message.replyTo, list);
|
|
885
|
+
}
|
|
886
|
+
return structuredClone(this.document.messages.filter(message => groups.has(message.groupId)).map(message => {
|
|
887
|
+
const { snapshot: _, ...receipt } = this.messageReceipt(message, replies.get(message.id) ?? [], false, false);
|
|
888
|
+
return { message, receipt };
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
messageReceipt(message, candidates, raw, includeSnapshot = true) {
|
|
892
|
+
const id = message.id;
|
|
893
|
+
const replies = candidates.filter(reply => (message.toPrincipalId ? reply.integrationOrigin?.integrationId === message.toPrincipalId : reply.fromSessionId === message.toSessionId)
|
|
866
894
|
&& (message.integrationOrigin ? reply.toPrincipalId === message.integrationOrigin.integrationId : reply.toSessionId === message.fromSessionId));
|
|
867
895
|
const diagnostic = this.diagnostic(id);
|
|
868
|
-
const facts = terminalMessage(message, raw);
|
|
896
|
+
const facts = terminalMessage(includeSnapshot ? message : { ...message, snapshot: null }, raw);
|
|
869
897
|
return { message_id: id, thread_id: message.threadId, status: facts.status, queued_at: message.status === 'stored' ? null : message.createdAt, stored_at: message.storedAt ?? null,
|
|
870
898
|
from_principal_id: message.integrationOrigin?.integrationId ?? null, to_principal_id: message.toPrincipalId ?? null,
|
|
871
899
|
delivered_at: facts.deliveredAt, expires_at: message.expiresAt ?? null,
|
|
872
900
|
failure_reason: message.failureReason ?? null, delivery_semantics: message.status === 'stored' ? 'durable_storage' : facts.deliverySource ?? (facts.deliveredAt === null ? 'not_delivered' : 'legacy_or_unspecified'),
|
|
873
|
-
snapshot: message.snapshot ? (raw ? message.snapshot : plainCollaborationSnapshot(message.snapshot)) : null,
|
|
901
|
+
snapshot: includeSnapshot && message.snapshot ? (raw ? message.snapshot : plainCollaborationSnapshot(message.snapshot)) : null,
|
|
874
902
|
delivery: { status: facts.status,
|
|
875
903
|
stage: message.status === 'stored' ? 'stored' : facts.deliveredAt !== null ? 'terminal_written' : diagnostic?.route_state === 'identity-mismatch' || diagnostic?.last_error === 'TMUX_PANE_CHANGED' ? 'blocked' : diagnostic?.remote_received_at ? 'remote_received' : 'queued',
|
|
876
904
|
...(!diagnostic?.route_diagnostics && diagnostic?.last_error === 'TMUX_PANE_CHANGED' ? { requires_confirmation: true } : {}),
|
|
@@ -78,6 +78,16 @@ export const INTEGRATION_HELP = `td integration — local scoped integration adm
|
|
|
78
78
|
Prior versions cannot reconstruct missing launch evidence retroactively.
|
|
79
79
|
create --file <policy.json> --credential-file <private-output.json>
|
|
80
80
|
update --file <policy.json> (same identity/group; preserves the credential)
|
|
81
|
+
1.4.346 launch profiles may declare nativeIdentity={kind:"linux-flock-owner",
|
|
82
|
+
directory:"{cwd}/.agent-home/cli/thread-writer-locks"} for an isolated Agent home.
|
|
83
|
+
Linux only; session_profile_native_identity=true required on the running service.
|
|
84
|
+
This configures read-only identity proof; it does not set Agent environment,
|
|
85
|
+
install hooks, create directories, or modify old session snapshots.
|
|
86
|
+
session_launch_reload_fence/session_shell_startup_wait (minimum 1.4.346)
|
|
87
|
+
protect launch tokens during disk reload and wait at most 5s for the same
|
|
88
|
+
unique foreground shell. This does not wait for application input-ready.
|
|
89
|
+
diagnostics may include shell_check (reason/pane/pgid/tpgid/samples/waited_ms),
|
|
90
|
+
never shell argv/output. A timed-out launch does not automatically retry.
|
|
81
91
|
revoke <principal-id>
|
|
82
92
|
Credentials are saved as a private file and never printed. Workers set
|
|
83
93
|
TERMDOCK_INTEGRATION_CREDENTIAL_FILE, then use td collab --principal <id> …
|
|
@@ -248,6 +258,8 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
248
258
|
throw new Error('update requires only --file');
|
|
249
259
|
const policy = JSON.parse(fs.readFileSync(options['--file'], 'utf8'));
|
|
250
260
|
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
261
|
+
if (policy.launchProfiles?.some((profile) => profile.nativeIdentity !== undefined) && capabilities.session_profile_native_identity !== true)
|
|
262
|
+
throw new CollaborationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'CLI and Linux service must support profile native identity (minimum 1.4.346)', 409);
|
|
251
263
|
if (capabilities.integration_policy_update !== true || policy.launchProfiles?.some((profile) => profile.startupInput !== undefined) && capabilities.startup_input_conditions !== true)
|
|
252
264
|
throw new CollaborationError('STARTUP_INPUT_CONDITION_UNSUPPORTED', 'Running service does not support this policy update', 409);
|
|
253
265
|
write(JSON.stringify(parsed(await request(socketPath, 'POST', `/admin/principals/${encodeURIComponent(policy.id)}/policy`, policy, headers(undefined, adminToken)))));
|
|
@@ -257,9 +269,11 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
257
269
|
if (!options['--file'] || !options['--credential-file'] || Object.keys(options).length !== 2)
|
|
258
270
|
throw new Error('create requires --file and --credential-file');
|
|
259
271
|
const policy = JSON.parse(fs.readFileSync(options['--file'], 'utf8'));
|
|
260
|
-
if (policy.launchProfiles?.some((profile) => profile.startupInput !== undefined)) {
|
|
272
|
+
if (policy.launchProfiles?.some((profile) => profile.startupInput !== undefined || profile.nativeIdentity !== undefined)) {
|
|
261
273
|
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
262
|
-
if (capabilities.
|
|
274
|
+
if (policy.launchProfiles.some((profile) => profile.nativeIdentity !== undefined) && capabilities.session_profile_native_identity !== true)
|
|
275
|
+
throw new CollaborationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'CLI and Linux service must support profile native identity (minimum 1.4.346)', 409);
|
|
276
|
+
if (policy.launchProfiles.some((profile) => profile.startupInput !== undefined) && capabilities.startup_input_conditions !== true)
|
|
263
277
|
throw new CollaborationError('STARTUP_INPUT_CONDITION_UNSUPPORTED', 'Running service does not support startup input conditions', 409);
|
|
264
278
|
}
|
|
265
279
|
const destination = path.resolve(options['--credential-file']);
|
|
@@ -29,6 +29,8 @@ export class IntegrationRuntime {
|
|
|
29
29
|
streams = new Set();
|
|
30
30
|
sent = new Map();
|
|
31
31
|
syncing = false;
|
|
32
|
+
dirtySources = new Set(['messages', 'tasks', 'sessions']);
|
|
33
|
+
syncedGroups = '';
|
|
32
34
|
retry;
|
|
33
35
|
releaseBatchFile;
|
|
34
36
|
releaseBatchOperations = [];
|
|
@@ -57,14 +59,17 @@ export class IntegrationRuntime {
|
|
|
57
59
|
throw error;
|
|
58
60
|
}
|
|
59
61
|
this.stops.push(options.messages.registerRuntimeGuard(id => this.sessions.assertRuntimeMessage(id)));
|
|
60
|
-
const changed = () =>
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const changed = (source) => () => {
|
|
63
|
+
this.dirtySources.add(source);
|
|
64
|
+
try {
|
|
65
|
+
this.sync();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.failStreams(error);
|
|
69
|
+
this.scheduleRetry();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
this.stops.push(options.messages.subscribe(changed('messages')), options.taskStore.subscribe(changed('tasks')), this.sessions.subscribe(changed('sessions')));
|
|
68
73
|
const app = express();
|
|
69
74
|
app.use(express.json({ limit: '2mb' }));
|
|
70
75
|
app.use((req, res, next) => {
|
|
@@ -144,7 +149,7 @@ export class IntegrationRuntime {
|
|
|
144
149
|
app.post('/admin/principals/:id/policy', run(req => { admin(req); if (req.body?.id !== req.params.id)
|
|
145
150
|
integrationError('INVALID_INTEGRATION_POLICY', 'Policy identity must match the requested identity'); this.group(req.body?.groupId); this.store.update(req.body); return { ok: true, id: req.params.id }; }));
|
|
146
151
|
app.post('/admin/principals/:id/revoke', run(req => { admin(req); this.store.revoke(req.params.id); return { ok: true }; }));
|
|
147
|
-
app.get('/capabilities', run(req => { const p = principal(req); return { ...this.capabilities(), principal: { id: p.id, group_id: p.groupId, permissions: p.permissions, launch_profiles: p.launchProfiles.map(profile => ({ id: profile.id, agent_slug: profile.agentSlug, cwd_roots: profile.cwdRoots, startup_input_condition: !!profile.startupInput })) } }; }));
|
|
152
|
+
app.get('/capabilities', run(req => { const p = principal(req); return { ...this.capabilities(), principal: { id: p.id, group_id: p.groupId, permissions: p.permissions, launch_profiles: p.launchProfiles.map(profile => ({ id: profile.id, agent_slug: profile.agentSlug, cwd_roots: profile.cwdRoots, startup_input_condition: !!profile.startupInput, native_identity_scope: !!profile.nativeIdentity })) } }; }));
|
|
148
153
|
app.get('/tasks', run(req => { const p = principal(req); this.authorize(p, 'task.read', req.query.group); return { tasks: options.tasks.list(p.groupId, null, req.query.purpose) }; }));
|
|
149
154
|
app.get('/tasks/:id', run(async (req) => { const p = principal(req); this.taskPermission(p, req.params.id, 'task.read'); return { task: await options.tasks.get(req.params.id, null) }; }));
|
|
150
155
|
app.post('/tasks', run(async (req) => {
|
|
@@ -299,6 +304,9 @@ export class IntegrationRuntime {
|
|
|
299
304
|
terminal_input_runtime_fence: true, terminal_input_runtime_fence_min_version: '1.4.313',
|
|
300
305
|
agent_launch_generation_consistency: true, agent_launch_generation_consistency_min_version: '1.4.322',
|
|
301
306
|
terminal_input_check_diagnostics: true,
|
|
307
|
+
session_launch_reload_fence: true, session_launch_reload_fence_min_version: '1.4.346',
|
|
308
|
+
session_shell_startup_wait: true, session_shell_startup_wait_min_version: '1.4.346',
|
|
309
|
+
session_profile_native_identity: process.platform === 'linux', session_profile_native_identity_min_version: '1.4.346',
|
|
302
310
|
session_launch_diagnostics: true, session_launch_diagnostics_min_version: '1.4.314',
|
|
303
311
|
session_cold_retry: true, session_cold_retry_min_version: '1.4.314',
|
|
304
312
|
session_rename: true, session_rename_min_version: '1.4.315',
|
|
@@ -458,11 +466,25 @@ export class IntegrationRuntime {
|
|
|
458
466
|
if (this.syncing)
|
|
459
467
|
return;
|
|
460
468
|
this.syncing = true;
|
|
469
|
+
let sources = new Set();
|
|
461
470
|
try {
|
|
462
471
|
const groups = new Set(this.store.policies().filter(p => !p.revoked).map(p => p.groupId)), updates = [];
|
|
463
|
-
|
|
472
|
+
const groupVersion = JSON.stringify([...groups].sort());
|
|
473
|
+
// Newly granted groups need a complete source reconciliation, including
|
|
474
|
+
// retained facts that predate the grant. Never advance dirty state until
|
|
475
|
+
// the durable event append succeeds.
|
|
476
|
+
if (groupVersion !== this.syncedGroups)
|
|
477
|
+
for (const source of ['messages', 'tasks', 'sessions'])
|
|
478
|
+
this.dirtySources.add(source);
|
|
479
|
+
sources = this.dirtySources;
|
|
480
|
+
this.dirtySources = new Set();
|
|
481
|
+
if (!sources.size)
|
|
482
|
+
return;
|
|
483
|
+
if (!groups.size) {
|
|
484
|
+
this.syncedGroups = groupVersion;
|
|
464
485
|
return;
|
|
465
|
-
|
|
486
|
+
}
|
|
487
|
+
for (const task of sources.has('tasks') ? this.options.taskStore.snapshot() : [])
|
|
466
488
|
if (groups.has(task.groupId)) {
|
|
467
489
|
for (const event of task.events)
|
|
468
490
|
updates.push({ sourceKey: `task:${task.id}:${event.id}`, sourceVersion: '1', eventId: event.id, event: {
|
|
@@ -482,31 +504,30 @@ export class IntegrationRuntime {
|
|
|
482
504
|
task_id: task.id, attempt_id: delivery.attemptId, message_id: delivery.messageId, source: 'system', payload: { delivery }
|
|
483
505
|
} });
|
|
484
506
|
}
|
|
485
|
-
for (const message of this.options.messages.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
for (const session of this.sessions.snapshot())
|
|
507
|
+
for (const { message, receipt } of sources.has('messages') ? this.options.messages.snapshotMessagesWithReceipts(groups) : []) {
|
|
508
|
+
const link = message.metadata?.termdockTask;
|
|
509
|
+
const context = { group_id: message.groupId, task_id: link?.taskId ?? null, attempt_id: link?.attemptId ?? null,
|
|
510
|
+
reply_to_event_id: link?.replyToEventId ?? null, message_id: message.id, source: message.integrationOrigin ? 'integration' : message.fromSessionId ? 'member' : 'system',
|
|
511
|
+
actor: message.integrationOrigin ? { integration_id: message.integrationOrigin.integrationId } : message.fromSessionId ? { session_id: message.fromSessionId } : null,
|
|
512
|
+
external_actor: message.integrationOrigin?.externalActor ?? null };
|
|
513
|
+
updates.push({ sourceKey: `message:${message.id}`, sourceVersion: '1', event: { ...context,
|
|
514
|
+
kind: message.kind === 'reply' ? 'message.reply' : 'message.queued', created_at: message.createdAt,
|
|
515
|
+
payload: { message_id: message.id, thread_id: message.threadId, reply_to: message.replyTo ?? null,
|
|
516
|
+
from_session_id: message.fromSessionId, to_session_id: message.toPrincipalId ? null : message.toSessionId,
|
|
517
|
+
to_principal_id: message.toPrincipalId ?? null, integration_origin: message.integrationOrigin ?? null,
|
|
518
|
+
response_kind: message.responseKind ?? null, stored_at: message.storedAt ?? null,
|
|
519
|
+
content: message.content, metadata: message.metadata ?? null } } });
|
|
520
|
+
updates.push({ sourceKey: `message-status:${message.id}`, sourceVersion: JSON.stringify(receipt), event: {
|
|
521
|
+
...context, kind: 'message.delivery', created_at: message.deliveredAt ?? message.createdAt, payload: { receipt }
|
|
522
|
+
} });
|
|
523
|
+
}
|
|
524
|
+
const sessions = sources.has('sessions') ? this.sessions.snapshot() : [];
|
|
525
|
+
for (const session of sessions)
|
|
505
526
|
if (groups.has(session.group_id))
|
|
506
527
|
updates.push({ sourceKey: `session:${session.session_id}`, sourceVersion: JSON.stringify(publicIntegrationSession(session)), event: {
|
|
507
528
|
group_id: session.group_id, kind: `session.${session.state}`, created_at: session.updated_at, source: 'system', actor: { integration_id: session.principal_id }, payload: { session: publicIntegrationSession(session) }
|
|
508
529
|
} });
|
|
509
|
-
for (const session of
|
|
530
|
+
for (const session of sessions)
|
|
510
531
|
if (groups.has(session.group_id) && session.name_updated_at)
|
|
511
532
|
updates.push({
|
|
512
533
|
sourceKey: `session-name:${session.session_id}`, sourceVersion: JSON.stringify([session.name, session.name_updated_at, session.name_generation]), event: {
|
|
@@ -516,9 +537,17 @@ export class IntegrationRuntime {
|
|
|
516
537
|
});
|
|
517
538
|
updates.sort((a, b) => a.event.created_at - b.event.created_at);
|
|
518
539
|
this.store.append(updates);
|
|
540
|
+
this.syncedGroups = groupVersion;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
for (const source of sources)
|
|
544
|
+
this.dirtySources.add(source);
|
|
545
|
+
throw error;
|
|
519
546
|
}
|
|
520
547
|
finally {
|
|
521
548
|
this.syncing = false;
|
|
549
|
+
if (this.dirtySources.size)
|
|
550
|
+
this.scheduleRetry();
|
|
522
551
|
}
|
|
523
552
|
}
|
|
524
553
|
failStreams(error) {
|
|
@@ -68,6 +68,8 @@ export class IntegrationSessions {
|
|
|
68
68
|
stopDeliveryGuard;
|
|
69
69
|
observing = false;
|
|
70
70
|
observeAgain = false;
|
|
71
|
+
observationTimer;
|
|
72
|
+
stopped = false;
|
|
71
73
|
constructor(file, adapter, bindingTimeout = 120000) {
|
|
72
74
|
this.file = file;
|
|
73
75
|
this.adapter = adapter;
|
|
@@ -94,7 +96,22 @@ export class IntegrationSessions {
|
|
|
94
96
|
if (record.startup_input && record.startup_input.state !== 'observed')
|
|
95
97
|
record.startup_input.matched_since = null;
|
|
96
98
|
this.stopDeliveryGuard = adapter.registerDeliveryGuard?.(id => this.deliveryReadiness(id));
|
|
97
|
-
this.stopObservation = adapter.subscribe(() =>
|
|
99
|
+
this.stopObservation = adapter.subscribe(() => this.scheduleObservation());
|
|
100
|
+
}
|
|
101
|
+
scheduleObservation() {
|
|
102
|
+
if (this.stopped)
|
|
103
|
+
return;
|
|
104
|
+
if (this.observing) {
|
|
105
|
+
this.observeAgain = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (this.observationTimer)
|
|
109
|
+
return;
|
|
110
|
+
this.observationTimer = setTimeout(() => {
|
|
111
|
+
this.observationTimer = undefined;
|
|
112
|
+
void this.refreshAll().catch(() => { });
|
|
113
|
+
}, 250);
|
|
114
|
+
this.observationTimer.unref();
|
|
98
115
|
}
|
|
99
116
|
scheduleRefresh(record) {
|
|
100
117
|
for (const deadline of new Set([record.deadline, ...(record.startup_input ? [record.startup_input.deadline] : [])])) {
|
|
@@ -974,18 +991,29 @@ export class IntegrationSessions {
|
|
|
974
991
|
this.observeAgain = true;
|
|
975
992
|
return;
|
|
976
993
|
}
|
|
994
|
+
if (this.observationTimer) {
|
|
995
|
+
clearTimeout(this.observationTimer);
|
|
996
|
+
this.observationTimer = undefined;
|
|
997
|
+
}
|
|
977
998
|
this.observing = true;
|
|
999
|
+
this.observeAgain = false;
|
|
978
1000
|
try {
|
|
979
|
-
|
|
980
|
-
this.
|
|
981
|
-
for (const record of this.snapshot())
|
|
982
|
-
await this.refresh(record.session_id);
|
|
983
|
-
} while (this.observeAgain);
|
|
1001
|
+
for (const record of this.snapshot())
|
|
1002
|
+
await this.refresh(record.session_id);
|
|
984
1003
|
}
|
|
985
1004
|
finally {
|
|
986
1005
|
this.observing = false;
|
|
1006
|
+
if (this.observeAgain)
|
|
1007
|
+
this.scheduleObservation();
|
|
987
1008
|
}
|
|
988
1009
|
}
|
|
989
|
-
close() {
|
|
990
|
-
|
|
1010
|
+
close() {
|
|
1011
|
+
this.stopped = true;
|
|
1012
|
+
this.stopObservation();
|
|
1013
|
+
this.stopDeliveryGuard?.();
|
|
1014
|
+
if (this.observationTimer)
|
|
1015
|
+
clearTimeout(this.observationTimer);
|
|
1016
|
+
for (const timer of this.timers)
|
|
1017
|
+
clearTimeout(timer);
|
|
1018
|
+
}
|
|
991
1019
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
|
+
export class IntegrationShellWaitError extends CollaborationError {
|
|
3
|
+
shellEvidence;
|
|
4
|
+
constructor(code, shellEvidence) {
|
|
5
|
+
super(code, 'The original runtime did not prove a unique foreground shell before launch', 409);
|
|
6
|
+
this.shellEvidence = shellEvidence;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const samePane = (a, b) => a.serverPid === b.serverPid && a.sessionId === b.sessionId && a.paneId === b.paneId && a.panePid === b.panePid;
|
|
10
|
+
/** Wait for kernel/process evidence, never a prompt, fixed startup sleep, or
|
|
11
|
+
* application readiness. This observes one runtime and never sends input. */
|
|
12
|
+
export async function waitForIntegrationShell(input) {
|
|
13
|
+
const now = input.now ?? Date.now, started = now();
|
|
14
|
+
const pause = input.pause ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
15
|
+
let pinned = input.expected ?? null, samples = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const observation = await input.observe();
|
|
18
|
+
samples++;
|
|
19
|
+
const evidence = { samples, waited_ms: now() - started,
|
|
20
|
+
reason: observation.reason, binding: observation.binding, pgid: observation.pgid, tpgid: observation.tpgid };
|
|
21
|
+
if (!observation.binding || pinned && !samePane(pinned, observation.binding)) {
|
|
22
|
+
throw new IntegrationShellWaitError('SESSION_IDENTITY_MISMATCH', evidence);
|
|
23
|
+
}
|
|
24
|
+
pinned ??= observation.binding;
|
|
25
|
+
if (observation.agent)
|
|
26
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', evidence);
|
|
27
|
+
if (observation.confirmed)
|
|
28
|
+
return pinned;
|
|
29
|
+
if (now() - started >= (input.timeoutMs ?? 5000))
|
|
30
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', evidence);
|
|
31
|
+
await pause(Math.min(100, (input.timeoutMs ?? 5000) - (now() - started)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
4
|
import { CollaborationError } from './collaborationProtocol.js';
|
|
5
|
+
import { resolveProfileNativeIdentity } from './nativeProcessIdentity.js';
|
|
5
6
|
export const INTEGRATION_PROTOCOL = 1;
|
|
6
7
|
export const INTEGRATION_OPERATIONS = ['message.send', 'message.read', 'task.read', 'task.create', 'task.configure', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore', 'session.release', 'session.rename', 'session.close'];
|
|
7
8
|
export function integrationError(code, message, status = 400) { throw new CollaborationError(code, message, status); }
|
|
@@ -54,6 +55,9 @@ export function validatePolicy(value) {
|
|
|
54
55
|
integrationError('INVALID_INTEGRATION_POLICY', 'resumeArgv requires one {sessionId} and one {launchArgs}; --last is forbidden');
|
|
55
56
|
if (!Array.isArray(profile.cwdRoots) || !profile.cwdRoots.length || profile.cwdRoots.some(root => typeof root !== 'string' || !path.isAbsolute(root) || /[\x00-\x1f\x7f]/.test(root)))
|
|
56
57
|
integrationError('INVALID_INTEGRATION_POLICY', 'cwdRoots must be absolute paths');
|
|
58
|
+
if (profile.nativeIdentity !== undefined && profile.cwdRoots.some(root => !resolveProfileNativeIdentity(profile.nativeIdentity, root))) {
|
|
59
|
+
integrationError('INVALID_INTEGRATION_POLICY', 'nativeIdentity must be an explicit owned-home writer-lock directory or a safe {cwd}/ relative template');
|
|
60
|
+
}
|
|
57
61
|
if (profile.startupInput !== undefined) {
|
|
58
62
|
const condition = profile.startupInput;
|
|
59
63
|
const validTerms = (terms) => Array.isArray(terms) && terms.length <= 16
|
|
@@ -148,18 +152,19 @@ export class IntegrationStore {
|
|
|
148
152
|
integrationError('INTEGRATION_PERMISSION_DENIED', 'Operation is outside the integration grant', 403);
|
|
149
153
|
}
|
|
150
154
|
append(updates) {
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
// Most reconciliations repeat already-journaled facts. Do not copy the
|
|
156
|
+
// retained event history until the first actual source-version change.
|
|
157
|
+
let next;
|
|
153
158
|
for (const update of updates) {
|
|
154
|
-
if (next.sources[update.sourceKey] === update.sourceVersion)
|
|
159
|
+
if ((next ?? this.doc).sources[update.sourceKey] === update.sourceVersion)
|
|
155
160
|
continue;
|
|
161
|
+
next ??= structuredClone(this.doc);
|
|
156
162
|
const group = update.event.group_id, sequence = (next.sequences[group] ?? 0) + 1;
|
|
157
163
|
next.events.push({ ...update.event, event_id: update.eventId ?? randomUUID(), sequence, cursor: this.cursor(group, sequence) });
|
|
158
164
|
next.sequences[group] = sequence;
|
|
159
165
|
next.sources[update.sourceKey] = update.sourceVersion;
|
|
160
|
-
changed = true;
|
|
161
166
|
}
|
|
162
|
-
if (!
|
|
167
|
+
if (!next)
|
|
163
168
|
return;
|
|
164
169
|
const counts = new Map();
|
|
165
170
|
next.events = next.events.reverse().filter(event => {
|
|
@@ -14,6 +14,27 @@ export function nativeIdentityDirectory(config) {
|
|
|
14
14
|
const directory = path.resolve(expanded), relative = path.relative(os.homedir(), directory);
|
|
15
15
|
return relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) ? directory : null;
|
|
16
16
|
}
|
|
17
|
+
/** Administrator-declared profile scope, never inferred from process env.
|
|
18
|
+
* {cwd} is permitted only as a leading directory anchor and cannot escape it. */
|
|
19
|
+
export function resolveProfileNativeIdentity(config, cwd) {
|
|
20
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
21
|
+
return null;
|
|
22
|
+
const value = config;
|
|
23
|
+
if (value.kind !== 'linux-flock-owner' || typeof value.directory !== 'string'
|
|
24
|
+
|| Object.keys(value).some(key => !['kind', 'directory'].includes(key)))
|
|
25
|
+
return null;
|
|
26
|
+
let directory = value.directory;
|
|
27
|
+
if (directory.startsWith('{cwd}/')) {
|
|
28
|
+
const suffix = directory.slice(6);
|
|
29
|
+
if (/[{}]/.test(suffix) || suffix.split(/[\\/]/).includes('..') || !path.isAbsolute(cwd))
|
|
30
|
+
return null;
|
|
31
|
+
directory = path.join(cwd, suffix);
|
|
32
|
+
}
|
|
33
|
+
else if (/[{}]/.test(directory))
|
|
34
|
+
return null;
|
|
35
|
+
const normalized = nativeIdentityDirectory({ kind: value.kind, directory });
|
|
36
|
+
return normalized ? { kind: 'linux-flock-owner', directory: normalized } : null;
|
|
37
|
+
}
|
|
17
38
|
const unavailable = { nativeId: null, outcome: 'unavailable' };
|
|
18
39
|
function processStat(text, pid) {
|
|
19
40
|
const close = text.lastIndexOf(')'), open = text.indexOf('(');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
2
|
import { TerminalInputTargetChangedError } from './terminalInput.js';
|
|
3
|
+
import { IntegrationShellWaitError } from './integrationShellWait.js';
|
|
3
4
|
export function beginLaunchDiagnostics(operation_id, generation, kind) {
|
|
4
5
|
const now = Date.now();
|
|
5
6
|
return { operation_id, generation, kind, phase: 'configuration', updated_at: now,
|
|
@@ -22,5 +23,6 @@ export function launchFailure(current, error, error_code) {
|
|
|
22
23
|
: typeof value?.code === 'string' && standard.has(value.code) ? value.code
|
|
23
24
|
: typeof value?.message === 'string' && standard.has(value.message) ? value.message : 'UNCLASSIFIED_LAUNCH_ERROR';
|
|
24
25
|
return { ...current, updated_at: Date.now(), error_code, reason_code,
|
|
26
|
+
...(error instanceof IntegrationShellWaitError ? { shell_check: structuredClone(error.shellEvidence) } : {}),
|
|
25
27
|
...(error instanceof TerminalInputTargetChangedError ? { target_change: error.evidence } : {}) };
|
|
26
28
|
}
|
|
@@ -6,6 +6,7 @@ export function mergeSessionLaunchState(candidate, current, change) {
|
|
|
6
6
|
return current ? { ...candidate,
|
|
7
7
|
agentLaunchGeneration: current.agentLaunchGeneration ?? null,
|
|
8
8
|
agentLaunchCheck: current.agentLaunchCheck ?? null,
|
|
9
|
+
agentNativeIdentity: current.agentNativeIdentity ?? null,
|
|
9
10
|
} : candidate;
|
|
10
11
|
if (!current || (current.agentLaunchGeneration ?? null) !== change.expectedGeneration
|
|
11
12
|
|| (current.agentLaunchCheck?.id ?? null) !== change.expectedCheckId) {
|
|
@@ -16,5 +17,38 @@ export function mergeSessionLaunchState(candidate, current, change) {
|
|
|
16
17
|
check_phase: 'launch_state_commit', checked_at: Date.now(),
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
|
-
return { ...candidate, agentLaunchGeneration: change.generation, agentLaunchCheck: change.check
|
|
20
|
+
return { ...candidate, agentLaunchGeneration: change.generation, agentLaunchCheck: change.check,
|
|
21
|
+
agentNativeIdentity: change.nativeIdentity === undefined ? current.agentNativeIdentity ?? null : change.nativeIdentity };
|
|
22
|
+
}
|
|
23
|
+
/** A file watcher can finish reading an old snapshot after an in-memory launch
|
|
24
|
+
* or deletion. It must not revert server-owned fields or resurrect that ID. */
|
|
25
|
+
export function mergeReloadedSessionState(disk, beforeRead, current) {
|
|
26
|
+
const before = new Map(beforeRead.map(record => [record.sessionId, record]));
|
|
27
|
+
const live = new Map(current.map(record => [record.sessionId, record]));
|
|
28
|
+
const result = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const candidate of disk) {
|
|
31
|
+
const id = candidate.sessionId, now = live.get(id), old = before.get(id);
|
|
32
|
+
if (seen.has(id) || old && !now)
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(id);
|
|
35
|
+
result.push(now ? JSON.stringify(now) !== JSON.stringify(old) ? now
|
|
36
|
+
: mergeSessionLaunchState(candidate, now) : candidate);
|
|
37
|
+
}
|
|
38
|
+
for (const record of current) {
|
|
39
|
+
if (!seen.has(record.sessionId) && (JSON.stringify(record) !== JSON.stringify(before.get(record.sessionId))
|
|
40
|
+
|| record.agentLaunchGeneration || record.agentNativeIdentity))
|
|
41
|
+
result.push(record);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/** Serialize durable global snapshots, taking a fresh value when each write
|
|
46
|
+
* acquires the queue. A delayed old rename must not overwrite a later launch. */
|
|
47
|
+
export function createSessionStateWriter(current, write) {
|
|
48
|
+
let pending = Promise.resolve();
|
|
49
|
+
return () => {
|
|
50
|
+
const next = pending.catch(() => undefined).then(() => write(structuredClone(current())));
|
|
51
|
+
pending = next;
|
|
52
|
+
return next;
|
|
53
|
+
};
|
|
20
54
|
}
|
|
@@ -4,7 +4,7 @@ import { isConfirmedEmptyTmuxServer } from '../utils/tmuxEmptyServerProbe.js';
|
|
|
4
4
|
import { collaborationSessionDisplayName } from '../utils/sessionDisplayName.js';
|
|
5
5
|
import { integrationError } from '../agent/integrationStore.js';
|
|
6
6
|
import { assertNativeResumeAvailable, collectNativeResumeOwnerCandidates } from '../agent/nativeResumeOwner.js';
|
|
7
|
-
import { readNativeProcessIdentity } from '../agent/nativeProcessIdentity.js';
|
|
7
|
+
import { nativeIdentityDirectory, readNativeProcessIdentity, resolveProfileNativeIdentity } from '../agent/nativeProcessIdentity.js';
|
|
8
8
|
import { closeSessionRuntime, foregroundProcessInstance, processStartIdentity } from '../agent/sessionManagement.js';
|
|
9
9
|
import { releaseSessionRuntime } from '../agent/sessionLifecycle.js';
|
|
10
10
|
import { resumeConfigurationFingerprint } from '../agent/sessionResumeConfiguration.js';
|
|
@@ -59,7 +59,8 @@ import { assessAgentResourceBudget, collectHostResourceSnapshot, configuredAgent
|
|
|
59
59
|
import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
|
|
60
60
|
import { writeBracketedPrompt, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
|
|
61
61
|
import { captureTerminalInputGuard, captureRuntimeInputGuard, waitAndWriteTerminalInput } from '../agent/terminalInput.js';
|
|
62
|
-
import { mergeSessionLaunchState } from '../agent/sessionLaunchState.js';
|
|
62
|
+
import { createSessionStateWriter, mergeReloadedSessionState, mergeSessionLaunchState } from '../agent/sessionLaunchState.js';
|
|
63
|
+
import { waitForIntegrationShell, IntegrationShellWaitError } from '../agent/integrationShellWait.js';
|
|
63
64
|
import { agentLaunchBlockReason, normalizeAgentLaunchCheck } from '../agent/agentLaunchCheck.js';
|
|
64
65
|
import { prepareSupervisionEnable } from '../utils/enableSupervision.js';
|
|
65
66
|
import { buildServerHealthState, dismissServerHealth } from '../utils/serverHealth.js';
|
|
@@ -531,12 +532,14 @@ async function migrateFromClientStatesFile() {
|
|
|
531
532
|
return null;
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
|
-
async function loadGlobalSessionStateFromDisk() {
|
|
535
|
+
async function loadGlobalSessionStateFromDisk(reload = false) {
|
|
536
|
+
const beforeRead = structuredClone(globalSessionState.sessions);
|
|
535
537
|
try {
|
|
536
538
|
const data = await readJsonFileIfExists(GLOBAL_SESSION_STATE_FILE);
|
|
537
539
|
if (data) {
|
|
540
|
+
const sessions = deduplicateGlobalSessions((data.sessions || []).map(s => normalizePersistedClientSession(s)).filter((s) => s !== null));
|
|
538
541
|
globalSessionState = {
|
|
539
|
-
sessions:
|
|
542
|
+
sessions: reload ? mergeReloadedSessionState(sessions, beforeRead, globalSessionState.sessions) : sessions,
|
|
540
543
|
updatedAt: typeof data.updatedAt === 'number' ? data.updatedAt : Date.now(),
|
|
541
544
|
};
|
|
542
545
|
console.log(`[session-persist] Loaded ${globalSessionState.sessions.length} sessions from global state`);
|
|
@@ -587,7 +590,7 @@ async function persistTmuxRecoveryState() {
|
|
|
587
590
|
async function reloadGlobalSessionStateFromDisk(source) {
|
|
588
591
|
try {
|
|
589
592
|
const previous = JSON.stringify(globalSessionState);
|
|
590
|
-
await loadGlobalSessionStateFromDisk();
|
|
593
|
+
await loadGlobalSessionStateFromDisk(true);
|
|
591
594
|
const next = JSON.stringify(globalSessionState);
|
|
592
595
|
if (previous === next) {
|
|
593
596
|
return;
|
|
@@ -637,12 +640,13 @@ async function watchGlobalSessionStateFile() {
|
|
|
637
640
|
reportFailure('[session-persist] Failed to watch global state file:', error);
|
|
638
641
|
}
|
|
639
642
|
}
|
|
643
|
+
const writeGlobalSessionState = createSessionStateWriter(() => globalSessionState, snapshot => writeJsonFile(GLOBAL_SESSION_STATE_FILE, snapshot));
|
|
640
644
|
function schedulePersistGlobalState() {
|
|
641
645
|
if (persistGlobalStateTimer)
|
|
642
646
|
clearTimeout(persistGlobalStateTimer);
|
|
643
647
|
persistGlobalStateTimer = setTimeout(() => {
|
|
644
648
|
try {
|
|
645
|
-
void
|
|
649
|
+
void writeGlobalSessionState().catch((error) => {
|
|
646
650
|
console.warn('[session-persist] Failed to persist global state:', getErrorMessage(error));
|
|
647
651
|
});
|
|
648
652
|
}
|
|
@@ -657,7 +661,7 @@ async function persistGlobalStateNow(strict = false) {
|
|
|
657
661
|
persistGlobalStateTimer = null;
|
|
658
662
|
}
|
|
659
663
|
try {
|
|
660
|
-
await
|
|
664
|
+
await writeGlobalSessionState();
|
|
661
665
|
}
|
|
662
666
|
catch (error) {
|
|
663
667
|
console.warn('[session-persist] Failed to persist global state:', getErrorMessage(error));
|
|
@@ -1002,6 +1006,8 @@ function normalizePersistedClientSession(input) {
|
|
|
1002
1006
|
agentLaunchCheck: normalizeAgentLaunchCheck(candidate.agentLaunchCheck),
|
|
1003
1007
|
agentLaunchGeneration: typeof candidate.agentLaunchGeneration === 'string' ? candidate.agentLaunchGeneration
|
|
1004
1008
|
: normalizeAgentLaunchCheck(candidate.agentLaunchCheck)?.id ?? null,
|
|
1009
|
+
agentNativeIdentity: nativeIdentityDirectory(candidate.agentNativeIdentity)
|
|
1010
|
+
? { kind: 'linux-flock-owner', directory: nativeIdentityDirectory(candidate.agentNativeIdentity) } : null,
|
|
1005
1011
|
};
|
|
1006
1012
|
}
|
|
1007
1013
|
function generateTmuxSessionName() {
|
|
@@ -1485,14 +1491,18 @@ function searchMetadataForBackend(backendSessionId, session) {
|
|
|
1485
1491
|
updatedAt: session.lastActivity,
|
|
1486
1492
|
};
|
|
1487
1493
|
}
|
|
1488
|
-
async function beginAgentLaunchCheck(frontendSessionId, agentSlug, nativeSessionId = null) {
|
|
1494
|
+
async function beginAgentLaunchCheck(frontendSessionId, agentSlug, nativeSessionId = null, nativeIdentity) {
|
|
1489
1495
|
const record = globalSessionState.sessions.find(candidate => candidate.sessionId === frontendSessionId);
|
|
1490
1496
|
if (!record)
|
|
1491
1497
|
throw new Error('SESSION_REMOVED');
|
|
1492
1498
|
const id = randomUUID();
|
|
1493
1499
|
const updated = upsertGlobalSessionRecord(record, { expectedGeneration: record.agentLaunchGeneration ?? null,
|
|
1494
1500
|
expectedCheckId: record.agentLaunchCheck?.id ?? null, generation: id,
|
|
1495
|
-
check: { id, agentSlug, nativeSessionId, deadline: Date.now() + 120_000 }
|
|
1501
|
+
check: { id, agentSlug, nativeSessionId, deadline: Date.now() + 120_000 },
|
|
1502
|
+
nativeIdentity: nativeIdentity === undefined
|
|
1503
|
+
? record.agentResume?.slug === agentSlug || collaborationRouting.get(frontendSessionId)?.agentSlug === agentSlug
|
|
1504
|
+
? record.agentNativeIdentity ?? null : null
|
|
1505
|
+
: nativeIdentity });
|
|
1496
1506
|
// Capture the guard before persistence yields. The launch command must use
|
|
1497
1507
|
// this exact generation, even if another launch occurs during an await.
|
|
1498
1508
|
const assertCurrent = terminalInputGuard(updated);
|
|
@@ -1711,7 +1721,7 @@ function formatLocalCollaborationMessages(frontendSessionId, messages) {
|
|
|
1711
1721
|
* agent-keyed selector can never reach it. Delivery still needs an agent (the
|
|
1712
1722
|
* confirm gate and prompt formatting assume a TUI); driving a terminal —
|
|
1713
1723
|
* run/capture — is a shell operation and works on any pane. */
|
|
1714
|
-
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false, describeNativeFailure = false) {
|
|
1724
|
+
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false, describeNativeFailure = false, identityOverride) {
|
|
1715
1725
|
if (!binding.tmuxSessionName)
|
|
1716
1726
|
return { state: 'offline', reason: 'TMUX_BINDING_MISSING' };
|
|
1717
1727
|
const name = `=${binding.tmuxSessionName}`;
|
|
@@ -1737,7 +1747,7 @@ async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane =
|
|
|
1737
1747
|
// pane_current_command alone cannot prove an exact resumed conversation.
|
|
1738
1748
|
const program = await resolveTmuxPaneProgram(pane, true, nativeProcessIdentity || freshProcess);
|
|
1739
1749
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1740
|
-
const identity = nativeProcessIdentity ? await observeProgramNativeIdentity(agent, program)
|
|
1750
|
+
const identity = nativeProcessIdentity ? await observeProgramNativeIdentity(agent, program, identityOverride ?? globalSessionState.sessions.find(record => record.sessionId === binding.sessionId)?.agentNativeIdentity ?? undefined)
|
|
1741
1751
|
: { nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null, failure: null, source: null };
|
|
1742
1752
|
if (identity.failure && !describeNativeFailure)
|
|
1743
1753
|
integrationError(identity.failure === 'NATIVE_SESSION_IDENTITY_CONFLICT'
|
|
@@ -1755,7 +1765,7 @@ async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane =
|
|
|
1755
1765
|
// pane so a plain shell member is drivable (run/capture are shell ops).
|
|
1756
1766
|
return selectDrivePane(binding, candidates, layout.activePaneId);
|
|
1757
1767
|
}
|
|
1758
|
-
return { ...selectCollaborationPane(binding, candidates), candidates };
|
|
1768
|
+
return { ...selectCollaborationPane(binding, candidates), candidates, layout };
|
|
1759
1769
|
}
|
|
1760
1770
|
function routeOwnershipConfirmed(frontendSessionId, binding, panes) {
|
|
1761
1771
|
const record = globalSessionState.sessions.find(candidate => candidate.sessionId === frontendSessionId);
|
|
@@ -2050,6 +2060,37 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
2050
2060
|
async function integrationPaneIsIdleShell(pane) {
|
|
2051
2061
|
return (await inspectIntegrationShell(pane)).confirmed;
|
|
2052
2062
|
}
|
|
2063
|
+
function integrationNativeIdentity(record) {
|
|
2064
|
+
if (!record.profile.nativeIdentity)
|
|
2065
|
+
return undefined;
|
|
2066
|
+
if (process.platform !== 'linux')
|
|
2067
|
+
integrationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'Profile writer-lock identity requires Linux', 409);
|
|
2068
|
+
const config = resolveProfileNativeIdentity(record.profile.nativeIdentity, record.cwd);
|
|
2069
|
+
if (!config)
|
|
2070
|
+
integrationError('INVALID_NATIVE_IDENTITY_CONFIG', 'Stored native identity scope is invalid', 409);
|
|
2071
|
+
return config;
|
|
2072
|
+
}
|
|
2073
|
+
async function waitForOwnedIntegrationShell(record, expected) {
|
|
2074
|
+
return waitForIntegrationShell({ expected, async observe() {
|
|
2075
|
+
const terminal = integrationTerminalRecord(record);
|
|
2076
|
+
if (!terminal)
|
|
2077
|
+
return { binding: null, confirmed: false, agent: false, reason: 'RUNTIME_NOT_REGISTERED', pgid: null, tpgid: null };
|
|
2078
|
+
const layout = await getTmuxLayout(terminal.tmuxSessionName);
|
|
2079
|
+
if (layout.windows.flatMap(window => window.panes).length !== 1) {
|
|
2080
|
+
return { binding: null, confirmed: false, agent: false, reason: 'MULTIPLE_TERMINAL_PANES', pgid: null, tpgid: null };
|
|
2081
|
+
}
|
|
2082
|
+
const inspected = await inspectCollaborationTmux({ sessionId: record.session_id, backendSessionId: terminal.backendSessionId,
|
|
2083
|
+
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null, pane: expected }, null, true, false, true);
|
|
2084
|
+
const pane = inspected.pane;
|
|
2085
|
+
if (inspected.state !== 'ready' || !pane)
|
|
2086
|
+
return { binding: null, confirmed: false, agent: false,
|
|
2087
|
+
reason: inspected.reason ?? 'PANE_UNCONFIRMED', pgid: null, tpgid: null };
|
|
2088
|
+
const shell = await inspectIntegrationShell(pane);
|
|
2089
|
+
return { binding: { serverPid: pane.serverPid, sessionId: pane.sessionId, paneId: pane.paneId, panePid: pane.panePid,
|
|
2090
|
+
agentSlug: '', nativeSessionId: null }, confirmed: shell.confirmed, agent: !!pane.agentSlug,
|
|
2091
|
+
reason: pane.isShell ? shell.failure : 'PANE_NOT_SHELL', pgid: shell.pgid, tpgid: shell.tpgid };
|
|
2092
|
+
} });
|
|
2093
|
+
}
|
|
2053
2094
|
async function inspectIntegrationShell(pane) {
|
|
2054
2095
|
if (!pane.isShell)
|
|
2055
2096
|
return { confirmed: false, failure: null, pgid: null, tpgid: null };
|
|
@@ -2157,7 +2198,7 @@ async function observeIntegrationCloseTarget(record) {
|
|
|
2157
2198
|
if (!record.agent_native_session_id || agent.slug !== record.agent_slug || program?.source !== 'tmux-tty' || !program.processPid) {
|
|
2158
2199
|
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'A running Agent requires a live exact native identity', 409);
|
|
2159
2200
|
}
|
|
2160
|
-
const identity = await observeProgramNativeIdentity(agent, program);
|
|
2201
|
+
const identity = await observeProgramNativeIdentity(agent, program, integrationNativeIdentity(record));
|
|
2161
2202
|
if (identity.failure || identity.nativeId !== record.agent_native_session_id)
|
|
2162
2203
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Current Agent native identity differs from the owned record', 409);
|
|
2163
2204
|
kind = 'agent';
|
|
@@ -2246,7 +2287,7 @@ export const integrationSessionAdapter = {
|
|
|
2246
2287
|
if (!agent || !command)
|
|
2247
2288
|
integrationError('EXACT_RESUME_UNSUPPORTED', 'Agent must support exact native recovery', 409);
|
|
2248
2289
|
return { version: 1, slug: agent.slug, aliases: agent.aliases, capabilities: agent.capabilities ?? [],
|
|
2249
|
-
resume: getPluginResumeConfig(agent.slug) ?? command, nativeIdentity: getPluginNativeIdentityConfig(agent.slug) ?? null };
|
|
2290
|
+
resume: getPluginResumeConfig(agent.slug) ?? command, nativeIdentity: integrationNativeIdentity(record) ?? getPluginNativeIdentityConfig(agent.slug) ?? null };
|
|
2250
2291
|
},
|
|
2251
2292
|
async runtimePresent(record) {
|
|
2252
2293
|
if (integrationTerminalRecord(record))
|
|
@@ -2303,13 +2344,13 @@ export const integrationSessionAdapter = {
|
|
|
2303
2344
|
const binding = { sessionId: record.session_id, backendSessionId: terminal.backendSessionId,
|
|
2304
2345
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: record.agent_slug,
|
|
2305
2346
|
nativeSessionId: record.agent_native_session_id, pane: record.terminal_binding ?? null };
|
|
2306
|
-
const inspected = await inspectCollaborationTmux(binding, undefined, false, true);
|
|
2347
|
+
const inspected = await inspectCollaborationTmux(binding, undefined, false, true, false, false, integrationNativeIdentity(record));
|
|
2307
2348
|
if (inspected.state === 'offline')
|
|
2308
2349
|
return { exists: false, running: false, shell: false, agentSlug: null, nativeId: null };
|
|
2309
2350
|
if (inspected.state !== 'ready' || !inspected.pane)
|
|
2310
2351
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Owned tmux pane cannot be uniquely verified', 409);
|
|
2311
2352
|
const pane = inspected.pane;
|
|
2312
|
-
const layout =
|
|
2353
|
+
const layout = inspected.layout;
|
|
2313
2354
|
if (layout.windows.flatMap(window => window.panes).length !== 1 || layout.activePaneId !== pane.paneId)
|
|
2314
2355
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Integration native binding requires the single owned pane', 409);
|
|
2315
2356
|
const backend = terminal.backendSessionId ? terminalSessions.get(terminal.backendSessionId) : null;
|
|
@@ -2443,7 +2484,7 @@ async function assertIntegrationNativeResumeAvailable(record) {
|
|
|
2443
2484
|
return Promise.all(layout.windows.flatMap(window => window.panes).map(async (pane) => {
|
|
2444
2485
|
const program = await resolveTmuxPaneProgram(pane, true, true);
|
|
2445
2486
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
2446
|
-
const identity = await observeProgramNativeIdentity(agent, program);
|
|
2487
|
+
const identity = await observeProgramNativeIdentity(agent, program, globalSessionState.sessions.find(record => record.sessionId === candidate.sessionId)?.agentNativeIdentity ?? integrationNativeIdentity(record));
|
|
2447
2488
|
const shell = await inspectIntegrationShell({ panePid: pane.pid,
|
|
2448
2489
|
isShell: !agent && shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
2449
2490
|
return { confirmed: !identity.failure && (shell.confirmed || program?.source === 'tmux-tty' && Boolean(program.rawArgs)),
|
|
@@ -2512,13 +2553,17 @@ async function launchIntegrationTerminal(record, restoring, prepared, progress,
|
|
|
2512
2553
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null,
|
|
2513
2554
|
// A recreated tmux may get a new pane only during explicit exact resume.
|
|
2514
2555
|
pane: original.exists ? record.terminal_binding ?? null : null }, null, true);
|
|
2515
|
-
if (inspected.state !== 'ready' || !inspected.pane
|
|
2516
|
-
|
|
2556
|
+
if (inspected.state !== 'ready' || !inspected.pane)
|
|
2557
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', {
|
|
2558
|
+
samples: 1, waited_ms: 0, reason: inspected.reason ?? 'PANE_UNCONFIRMED', binding: null, pgid: null, tpgid: null
|
|
2559
|
+
});
|
|
2517
2560
|
const pane = inspected.pane;
|
|
2518
2561
|
const pinned = { serverPid: pane.serverPid, sessionId: pane.sessionId,
|
|
2519
2562
|
paneId: pane.paneId, panePid: pane.panePid, agentSlug: record.agent_slug, nativeSessionId: record.agent_native_session_id };
|
|
2520
2563
|
progress?.('binding_persist');
|
|
2521
2564
|
prepared(pinned); // Must persist before the command enters the terminal.
|
|
2565
|
+
progress?.('runtime_shell_wait');
|
|
2566
|
+
await waitForOwnedIntegrationShell(record, pinned);
|
|
2522
2567
|
collaborationRouting.bind({ sessionId: record.session_id, backendSessionId: opened.terminalSession.sessionId,
|
|
2523
2568
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: record.agent_slug, nativeSessionId: record.agent_native_session_id, pane: pinned });
|
|
2524
2569
|
const latest = collaborationStore.getGroup(group.id);
|
|
@@ -2540,17 +2585,14 @@ async function launchIntegrationTerminal(record, restoring, prepared, progress,
|
|
|
2540
2585
|
await assertIntegrationNativeResumeAvailable(record);
|
|
2541
2586
|
}
|
|
2542
2587
|
progress?.('runtime_verify');
|
|
2543
|
-
|
|
2544
|
-
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null, pane: pinned });
|
|
2545
|
-
if (shell.state !== 'ready' || !shell.pane || !await integrationPaneIsIdleShell(shell.pane))
|
|
2546
|
-
integrationError('SESSION_TARGET_NOT_SHELL', 'Owned shell changed before launch', 409);
|
|
2588
|
+
await waitForOwnedIntegrationShell(record, pinned);
|
|
2547
2589
|
// This owned pane is a verified shell; clear stale rich state before a new
|
|
2548
2590
|
// process starts, so its new SessionStart must supply the native binding.
|
|
2549
2591
|
backend.agentSession = null;
|
|
2550
2592
|
backend.agent = null;
|
|
2551
2593
|
backend.agentResumeRecovered = false;
|
|
2552
2594
|
progress?.('launch_intent');
|
|
2553
|
-
const launchGuard = await beginAgentLaunchCheck(record.session_id, record.agent_slug, restoring ? record.agent_native_session_id : null);
|
|
2595
|
+
const launchGuard = await beginAgentLaunchCheck(record.session_id, record.agent_slug, restoring ? record.agent_native_session_id : null, integrationNativeIdentity(record) ?? null);
|
|
2554
2596
|
await writeAutomaticTerminalPrompt(record.session_id, integrationLaunchCommand(record, restoring), progress, launchGuard);
|
|
2555
2597
|
broadcastClientState();
|
|
2556
2598
|
});
|
|
@@ -4532,9 +4574,9 @@ async function resolveTmuxPaneProgram(pane, requireArgs = false, refreshTty = fa
|
|
|
4532
4574
|
}
|
|
4533
4575
|
return { command, source: 'tmux-pane', rawArgs: null };
|
|
4534
4576
|
}
|
|
4535
|
-
async function observeProgramNativeIdentity(agent, program) {
|
|
4577
|
+
async function observeProgramNativeIdentity(agent, program, override) {
|
|
4536
4578
|
const fromArgs = agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null;
|
|
4537
|
-
const config = agent ? getPluginNativeIdentityConfig(agent.slug) : undefined;
|
|
4579
|
+
const config = agent ? override ?? getPluginNativeIdentityConfig(agent.slug) : undefined;
|
|
4538
4580
|
if (config && program?.processPid) {
|
|
4539
4581
|
const proof = await readNativeProcessIdentity(program.processPid, config, program.processComm, program.rawArgs ?? undefined);
|
|
4540
4582
|
if (proof.outcome === 'ambiguous' || proof.outcome === 'process_changed' || proof.outcome === 'unconfirmed')
|
|
@@ -4546,7 +4588,11 @@ async function observeProgramNativeIdentity(agent, program) {
|
|
|
4546
4588
|
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_IDENTITY_CONFLICT' };
|
|
4547
4589
|
return { nativeId: proof.nativeId, source: 'linux-flock-owner', failure: null };
|
|
4548
4590
|
}
|
|
4591
|
+
if (override)
|
|
4592
|
+
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_OWNERSHIP_UNCONFIRMED' };
|
|
4549
4593
|
}
|
|
4594
|
+
if (override && agent)
|
|
4595
|
+
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_OWNERSHIP_UNCONFIRMED' };
|
|
4550
4596
|
return { nativeId: fromArgs, source: fromArgs ? 'argv' : null, failure: null };
|
|
4551
4597
|
}
|
|
4552
4598
|
/**
|
package/docs/integrations.md
CHANGED
|
@@ -1354,3 +1354,43 @@ flock-owner 身份独立核验出原 agent slug 与原生 UUID。目标元组会
|
|
|
1354
1354
|
fail-closed,旧绑定与原消息保留,`session get` 仍报告身份错误;不会按 tmux 名称、PID
|
|
1355
1355
|
缓存、cwd、argv 中的 resume 参数或显示名猜测认领。此 capability 不新增权限;仅通过
|
|
1356
1356
|
`session.read` 认证的所属 principal 可触发其自身记录的对账。
|
|
1357
|
+
|
|
1358
|
+
## 1.4.346:启动磁盘重载、原 shell 核验与独立原生目录
|
|
1359
|
+
|
|
1360
|
+
最低 CLI / 实际服务均为 **1.4.346**。通过所属 principal 的 `capabilities` 回读:
|
|
1361
|
+
|
|
1362
|
+
- `session_launch_reload_fence=true`:服务运行期间的磁盘重载保留当前服务生成的 launch token、已解除的一次性 check 和显式原生目录。读取期间新增的会话不会消失,已删除的会话不会由旧读盘复活;全局状态写入串行,并在执行写入时采集当前快照。服务首次启动仍加载磁盘记录。这修复了磁盘 watch 路径绕过元数据 upsert 保护的缺口,不把每一例历史 `TERMINAL_INPUT_TARGET_CHANGED` 都归因为此。
|
|
1363
|
+
- `session_shell_startup_wait=true`:启动前先记录唯一原 pane 身份,再最多等待 5 秒核实该 shell 的前台关系和合格 shell 参数。只观察同一 tmux server/session/pane/PID,采样使用新鲜进程证据;承载改变、pane 不唯一或已有 Agent 时拒绝。不输入探针、不清理画面、不重建承载、不发送额外回车,也不把 shell 核验当成应用 input-ready。
|
|
1364
|
+
- `session_profile_native_identity=true`:当前仅 Linux 支持 profile 级只读 writer-lock 目录,覆盖该受管会话的插件缺省目录,不改变共享插件配置。三个字段分别有同名 `_min_version` 字段,值为 `1.4.346`。能力支持不等于 principal 已被授予权限。
|
|
1365
|
+
|
|
1366
|
+
本机管理员可在现有完整 policy 的目标 `launchProfiles` 中加:
|
|
1367
|
+
|
|
1368
|
+
```json
|
|
1369
|
+
"nativeIdentity": {
|
|
1370
|
+
"kind": "linux-flock-owner",
|
|
1371
|
+
"directory": "{cwd}/.agent-home/cli/thread-writer-locks"
|
|
1372
|
+
}
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
目录是示例,必须按实际 Agent 的持锁协议填写。支持管理员指定的绝对目录、`~/` 目录,以及仅以前缀 `{cwd}/` 锚定的模板;模板不允许 `..` 或其它变量,解析后的目录须在 TD 服务用户 home 内。调用现有 `td integration update --file <完整policy.json>`,不增加权限、不重建 principal 或凭据。CLI 在旧服务/非 Linux 服务上拒绝该配置,不能静默忽略。原有不配置此项的 profile 保持插件缺省行为。
|
|
1376
|
+
|
|
1377
|
+
wrapper 负责实际设置 Agent 自己的数据目录、准备 hooks/参数,并最终运行真正的 Agent;这个字段只声明 TD 的核验目录,不设置环境、不执行脚本、不创建目录,不自动读取 `TRAECLI_HOME` 等私有变量。TD 仍检查目录与文件权限、进程 starttime、前台关系、实际 FD/inode 和当前内核 flock,要求唯一 native UUID。显式 profile 目录不可证明时不降级到 argv/hook/旧 UUID,多个持锁 UUID 仍拒绝。目录语义纳入恢复配置指纹,保存至启动代次;服务重启、metadata 更新、check 解除不会丢失它。
|
|
1378
|
+
|
|
1379
|
+
该配置只用于之后创建的 session;旧 session 保留原 profile 快照,policy update 不迁移旧记录或修改运行 Agent。不要为旧未知结果自动 create 新 ID 或 retry-cold。原 UUID 恢复、重复 owner 保护、关闭仍遵守现有身份/代次/幂等门禁;独立数据目录不构成对同 UID 进程的安全隔离。
|
|
1380
|
+
|
|
1381
|
+
启动失败管理员用 `td integration diagnostics <原TD-ID>` 回读;shell 等待失败新增 `shell_check={samples,waited_ms,reason,binding,pgid,tpgid}`,不包含 argv/env/终端正文。`SESSION_TARGET_NOT_SHELL` 表示有限核验未通过,不表示 wrapper 已执行;`paste/submit=not_attempted` 与后续 `runtime_present` 现场回读分开保存。失败不自动再次启动,只有符合原 `session retry-cold` 所有条件才可明确重试。
|
|
1382
|
+
|
|
1383
|
+
用 stdin 文件给启动命令附带业务 prompt 是接入方的 bootstrap:TD 不为其中业务副作用提供消息幂等或执行确认;无 TD message 历史也不等于没有业务执行。冷启动与精确 resume 应分开处理,恢复时不得重放初始业务文件。host 收取 final/hook receipt 是 host 侧证据,不伪造成 TD 明确 reply。仍只粘贴一次、等待 50ms、写一次 CR。
|
|
1384
|
+
|
|
1385
|
+
回归包含确定性交错的旧读盘覆盖/串行写入/删除不复活、同 pane shell 初始化等待与超时/换 pane/已有 Agent 拒绝、CLI 对旧服务拒绝,以及 Linux 真实 tmux/内核 flock 的独立目录核验。私有 TraeX/DevBox 的启动兼容仍需接入方维护窗实机验证;维护机合成测试不冒充该项通过。
|
|
1386
|
+
|
|
1387
|
+
## 1.4.347:无变化同步与后台观察的开销
|
|
1388
|
+
|
|
1389
|
+
这是服务运行时优化,不增加接口、权限或配置。需要更新实际运行服务到 **1.4.347**;仅安装新 CLI 不会更新已运行进程。
|
|
1390
|
+
|
|
1391
|
+
- 跨服务目录重复合并相同群内容时不写盘、不通知事件同步。比较包含内容而非仅时间戳;同时间戳的真实变化仍处理。合并独立群规/角色版本后如有效内容未变也不写盘;有效群更新与保留的新上下文一次持久化。
|
|
1392
|
+
- 空事件追加及 source/version 全重复的追加在克隆事件历史之前返回。真实更新继续持久提交,失败保留可重试源版本,不改变事件 ID、顺序、ACK 或保留策略。
|
|
1393
|
+
- 事件同步按消息、任务、会话三个变化来源分别扫描;会话通知不再复制消息历史和生成全部消息回执。新授权组及服务重启仍对保留事实完整对账;失败来源继续重试。消息变化时一次建立回复索引,避免每条回执重扫历史;回执仍核验双方身份,并省去事件中不包含的终端快照格式化。这是按来源增量同步,真实变化仍会扫描该来源的保留记录。
|
|
1394
|
+
- 终端输出触发的后台会话观察按 250ms 窗口合并;一轮观察期间的新通知安排到后续一轮,不连续抢占。显式 `session get`、生命周期操作及投递前核验保留原路径,不以此窗口判断 Agent 可输入;不延迟业务输入来猜测应用状态。同一次 integration 观察复用其新鲜 tmux layout,下一次观察重新读取,不把缓存用作身份写入凭据。
|
|
1395
|
+
|
|
1396
|
+
无删除消息/事件、缩短历史、停用消费者、额外 Enter 或正文重发。`pty_written` 仍只代表终端写入调用完成,不代表应用提交或任务完成。回归包含约 26 MB journal 的空/重复追加零克隆、相同群同步零写入、真实变化/持久化失败重试、授权后历史补收、通知洪峰合并、换 pane/原生身份冲突拒绝。生产 CPU、事件循环延迟和 health 改善须在实际维护窗升级后对比采样,不能用隔离基准代替实机验收。
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.347",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-ObG1OGq7e7fMFQmYoPxPp3jRodEusYHulVNa5mT1aW8=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-FXUdT0eV3/+rgHBnc0Jg9dn2sAb4hcLY83xScGapcKQ=",
|
|
10
10
|
"clientBundleHash": "sha256-kn3Tg+BTj+H4rZe0g9OE5JUEQNsBGiUSGew8Epm5A38=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|