termdock 1.4.347 → 1.4.348
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/integrationServer.js +4 -3
- package/dist/server/agent/integrationSessions.js +20 -5
- package/dist/server/agent/integrationStore.js +125 -20
- package/dist/server/agent/nativeProcessIdentity.js +17 -2
- package/dist/server/routes/terminal.js +24 -165
- package/dist/server/utils/tmuxLayoutReader.js +74 -0
- package/docs/integrations.md +17 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -307,6 +307,7 @@ export class IntegrationRuntime {
|
|
|
307
307
|
session_launch_reload_fence: true, session_launch_reload_fence_min_version: '1.4.346',
|
|
308
308
|
session_shell_startup_wait: true, session_shell_startup_wait_min_version: '1.4.346',
|
|
309
309
|
session_profile_native_identity: process.platform === 'linux', session_profile_native_identity_min_version: '1.4.346',
|
|
310
|
+
session_profile_native_identity_home_aliases: process.platform === 'linux', session_profile_native_identity_home_aliases_min_version: '1.4.348',
|
|
310
311
|
session_launch_diagnostics: true, session_launch_diagnostics_min_version: '1.4.314',
|
|
311
312
|
session_cold_retry: true, session_cold_retry_min_version: '1.4.314',
|
|
312
313
|
session_rename: true, session_rename_min_version: '1.4.315',
|
|
@@ -570,7 +571,7 @@ export class IntegrationRuntime {
|
|
|
570
571
|
const key = `${p.id}:${p.groupId}:${consumer}`;
|
|
571
572
|
// Resolve the initial cursor before sending headers so retention gaps
|
|
572
573
|
// and bad consumer names are ordinary machine-readable HTTP errors.
|
|
573
|
-
const initial = this.store.
|
|
574
|
+
const initial = this.store.streamPage(p, consumer);
|
|
574
575
|
cursor = initial.cursor;
|
|
575
576
|
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
576
577
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -585,13 +586,13 @@ export class IntegrationRuntime {
|
|
|
585
586
|
try {
|
|
586
587
|
this.authorize(p, 'events.read');
|
|
587
588
|
for (;;) {
|
|
588
|
-
const page = this.store.
|
|
589
|
+
const page = this.store.streamPage(p, consumer, cursor);
|
|
589
590
|
if (!page.events.length)
|
|
590
591
|
break;
|
|
591
592
|
for (const event of page.events) {
|
|
592
593
|
if (res.writableLength > 1024 * 1024)
|
|
593
594
|
integrationError('EVENT_CONSUMER_SLOW', 'Reconnect from the last acknowledged cursor', 503);
|
|
594
|
-
res.write(
|
|
595
|
+
res.write(event.line);
|
|
595
596
|
cursor = event.cursor;
|
|
596
597
|
this.sent.set(key, Math.max(this.sent.get(key) ?? 0, event.sequence));
|
|
597
598
|
}
|
|
@@ -714,10 +714,12 @@ export class IntegrationSessions {
|
|
|
714
714
|
return record;
|
|
715
715
|
}));
|
|
716
716
|
}
|
|
717
|
-
async refresh(id, allowBindingReconciliation = true) {
|
|
717
|
+
async refresh(id, allowBindingReconciliation = true, presence) {
|
|
718
718
|
const record = structuredClone(this.doc.sessions.find(s => s.session_id === id));
|
|
719
719
|
if (!record)
|
|
720
720
|
return;
|
|
721
|
+
const runtimePresent = () => presence && JSON.stringify(record) === JSON.stringify(presence.record)
|
|
722
|
+
? Promise.resolve(presence.present) : this.adapter.runtimePresent?.(record);
|
|
721
723
|
if (record.rename_pending) {
|
|
722
724
|
if (this.lifecycle.active(id))
|
|
723
725
|
return;
|
|
@@ -744,7 +746,7 @@ export class IntegrationSessions {
|
|
|
744
746
|
integrationError('SESSION_CLOSE_UNSUPPORTED', 'Close reconciliation is unavailable', 409);
|
|
745
747
|
await this.adapter.closeRuntime(record, true);
|
|
746
748
|
}
|
|
747
|
-
if (!this.adapter.runtimePresent || await
|
|
749
|
+
if (!this.adapter.runtimePresent || await runtimePresent())
|
|
748
750
|
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'Runtime exists for a closed or closing session', 409);
|
|
749
751
|
const next = { ...record, state: 'closed', runtime_present: false, terminal_binding: null, close_intent: undefined,
|
|
750
752
|
operation_uncertain: false, error_code: null, startup_input: undefined };
|
|
@@ -772,7 +774,7 @@ export class IntegrationSessions {
|
|
|
772
774
|
try {
|
|
773
775
|
if (record.state !== 'released')
|
|
774
776
|
await this.adapter.release?.(record, true);
|
|
775
|
-
const present = await
|
|
777
|
+
const present = await runtimePresent();
|
|
776
778
|
if (present === undefined)
|
|
777
779
|
integrationError('SESSION_RELEASE_UNSUPPORTED', 'Runtime presence is unavailable', 409);
|
|
778
780
|
if (record.state !== 'released') {
|
|
@@ -998,8 +1000,21 @@ export class IntegrationSessions {
|
|
|
998
1000
|
this.observing = true;
|
|
999
1001
|
this.observeAgain = false;
|
|
1000
1002
|
try {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
+
const records = this.snapshot();
|
|
1004
|
+
const dormant = records.filter(record => ['closed', 'released'].includes(record.state) && record.runtime_present === false
|
|
1005
|
+
&& !record.operation_uncertain && !record.close_intent && !record.rename_pending && !record.cold_retry_pending);
|
|
1006
|
+
let presence;
|
|
1007
|
+
if (dormant.length && this.adapter.runtimePresenceBatch) {
|
|
1008
|
+
try {
|
|
1009
|
+
presence = await this.adapter.runtimePresenceBatch(dormant);
|
|
1010
|
+
}
|
|
1011
|
+
catch { /* Failed batch observations fall back to fresh per-record checks. */ }
|
|
1012
|
+
}
|
|
1013
|
+
const candidates = new Map(dormant.map(record => [record.session_id, record]));
|
|
1014
|
+
for (const record of records) {
|
|
1015
|
+
const candidate = candidates.get(record.session_id), present = presence?.get(record.session_id);
|
|
1016
|
+
await this.refresh(record.session_id, true, candidate && typeof present === 'boolean' ? { record: candidate, present } : undefined);
|
|
1017
|
+
}
|
|
1003
1018
|
}
|
|
1004
1019
|
finally {
|
|
1005
1020
|
this.observing = false;
|
|
@@ -7,12 +7,18 @@ export const INTEGRATION_PROTOCOL = 1;
|
|
|
7
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'];
|
|
8
8
|
export function integrationError(code, message, status = 400) { throw new CollaborationError(code, message, status); }
|
|
9
9
|
export function atomicJson(file, value) {
|
|
10
|
+
atomicJsonText(file, JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
function atomicJsonText(file, serialized) {
|
|
13
|
+
atomicJsonWrite(file, descriptor => fs.writeFileSync(descriptor, serialized));
|
|
14
|
+
}
|
|
15
|
+
function atomicJsonWrite(file, write) {
|
|
10
16
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
11
17
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
12
18
|
const descriptor = fs.openSync(temporary, 'w', 0o600);
|
|
13
19
|
try {
|
|
14
20
|
fs.fchmodSync(descriptor, 0o600);
|
|
15
|
-
|
|
21
|
+
write(descriptor);
|
|
16
22
|
fs.fsyncSync(descriptor);
|
|
17
23
|
}
|
|
18
24
|
finally {
|
|
@@ -27,6 +33,42 @@ export function atomicJson(file, value) {
|
|
|
27
33
|
fs.closeSync(directory);
|
|
28
34
|
}
|
|
29
35
|
}
|
|
36
|
+
function atomicJsonBuffers(file, parts) {
|
|
37
|
+
atomicJsonWrite(file, descriptor => {
|
|
38
|
+
let index = 0, offset = 0;
|
|
39
|
+
while (index < parts.length) {
|
|
40
|
+
// Bound vectors below platform IOV_MAX, and handle short writes without
|
|
41
|
+
// replaying or dropping any byte of the checked JSON document.
|
|
42
|
+
const vectors = parts.slice(index, index + 512);
|
|
43
|
+
if (offset)
|
|
44
|
+
vectors[0] = vectors[0].subarray(offset);
|
|
45
|
+
let written;
|
|
46
|
+
try {
|
|
47
|
+
written = fs.writevSync(descriptor, vectors);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code === 'EINTR')
|
|
51
|
+
continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (written <= 0)
|
|
55
|
+
throw new Error('Integration journal write made no progress');
|
|
56
|
+
while (index < parts.length) {
|
|
57
|
+
const available = parts[index].length - offset;
|
|
58
|
+
if (written < available) {
|
|
59
|
+
offset += written;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
written -= available;
|
|
63
|
+
index++;
|
|
64
|
+
offset = 0;
|
|
65
|
+
if (!written)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const JSON_OPEN = Buffer.from('{'), JSON_CLOSE = Buffer.from('}'), JSON_COMMA = Buffer.from(','), ARRAY_OPEN = Buffer.from('['), ARRAY_CLOSE = Buffer.from(']'), EVENT_PREFIX = Buffer.from('{"type":"event",'), NEWLINE = Buffer.from('\n');
|
|
30
72
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
31
73
|
function identifier(value, label) {
|
|
32
74
|
if (typeof value !== 'string' || !/^[a-zA-Z0-9._-]{1,80}$/.test(value))
|
|
@@ -77,6 +119,11 @@ export class IntegrationStore {
|
|
|
77
119
|
retention;
|
|
78
120
|
doc = { version: 1, principals: [], events: [], sequences: {}, prunedThrough: {}, sources: {}, consumers: {} };
|
|
79
121
|
listeners = new Set();
|
|
122
|
+
// Committed events and metadata are immutable inside the store. Keep the
|
|
123
|
+
// encoded event fragments, not another deep copy of their payloads. Pruned
|
|
124
|
+
// objects become collectible with their encodings.
|
|
125
|
+
encoded = new WeakMap();
|
|
126
|
+
groupEvents = new Map();
|
|
80
127
|
constructor(file, retention = 10000) {
|
|
81
128
|
this.file = file;
|
|
82
129
|
this.retention = retention;
|
|
@@ -90,12 +137,52 @@ export class IntegrationStore {
|
|
|
90
137
|
if (error.code !== 'ENOENT')
|
|
91
138
|
throw error;
|
|
92
139
|
}
|
|
140
|
+
this.indexEvents();
|
|
141
|
+
}
|
|
142
|
+
indexEvents() {
|
|
143
|
+
this.groupEvents = new Map();
|
|
144
|
+
for (const event of this.doc.events) {
|
|
145
|
+
const events = this.groupEvents.get(event.group_id) ?? [];
|
|
146
|
+
events.push(event);
|
|
147
|
+
this.groupEvents.set(event.group_id, events);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
encode(value) {
|
|
151
|
+
if (!value || typeof value !== 'object')
|
|
152
|
+
return Buffer.from(JSON.stringify(value));
|
|
153
|
+
const cached = this.encoded.get(value);
|
|
154
|
+
if (cached !== undefined)
|
|
155
|
+
return cached;
|
|
156
|
+
const serialized = Buffer.from(JSON.stringify(value));
|
|
157
|
+
this.encoded.set(value, serialized);
|
|
158
|
+
return serialized;
|
|
93
159
|
}
|
|
94
160
|
write(next) {
|
|
95
|
-
|
|
161
|
+
const parts = [JSON_OPEN];
|
|
162
|
+
for (const [index, [key, value]] of Object.entries(next).entries()) {
|
|
163
|
+
if (index)
|
|
164
|
+
parts.push(JSON_COMMA);
|
|
165
|
+
parts.push(Buffer.from(`${JSON.stringify(key)}:`));
|
|
166
|
+
if (key === 'events') {
|
|
167
|
+
parts.push(ARRAY_OPEN);
|
|
168
|
+
for (const [index, event] of next.events.entries()) {
|
|
169
|
+
if (index)
|
|
170
|
+
parts.push(JSON_COMMA);
|
|
171
|
+
parts.push(this.encode(event));
|
|
172
|
+
}
|
|
173
|
+
parts.push(ARRAY_CLOSE);
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
parts.push(this.encode(value));
|
|
177
|
+
}
|
|
178
|
+
parts.push(JSON_CLOSE);
|
|
179
|
+
if (parts.reduce((bytes, part) => bytes + part.length, 0) > 64 * 1024 * 1024)
|
|
96
180
|
integrationError('EVENT_STORAGE_FULL', 'Integration journal is full; no event was acknowledged', 503);
|
|
97
|
-
|
|
181
|
+
atomicJsonBuffers(this.file, parts);
|
|
182
|
+
const eventsChanged = next.events !== this.doc.events;
|
|
98
183
|
this.doc = next;
|
|
184
|
+
if (eventsChanged)
|
|
185
|
+
this.indexEvents();
|
|
99
186
|
for (const listener of this.listeners) {
|
|
100
187
|
try {
|
|
101
188
|
listener();
|
|
@@ -110,13 +197,12 @@ export class IntegrationStore {
|
|
|
110
197
|
if (this.doc.principals.some(p => p.id === checked.id))
|
|
111
198
|
integrationError('INTEGRATION_EXISTS', 'Use a new id; credentials cannot be silently replaced', 409);
|
|
112
199
|
const token = randomBytes(32).toString('base64url');
|
|
113
|
-
const next =
|
|
114
|
-
next.principals.push({ ...checked, tokenHash: hash(token), revoked: false });
|
|
200
|
+
const next = { ...this.doc, principals: [...this.doc.principals, { ...checked, tokenHash: hash(token), revoked: false }] };
|
|
115
201
|
this.write(next);
|
|
116
|
-
return { principal: checked, token };
|
|
202
|
+
return { principal: structuredClone(checked), token };
|
|
117
203
|
}
|
|
118
204
|
update(policy) {
|
|
119
|
-
const checked = validatePolicy(policy), next =
|
|
205
|
+
const checked = validatePolicy(policy), next = { ...this.doc, principals: [...this.doc.principals] };
|
|
120
206
|
const index = next.principals.findIndex(principal => principal.id === checked.id);
|
|
121
207
|
if (index < 0)
|
|
122
208
|
integrationError('INTEGRATION_NOT_FOUND', 'Integration does not exist', 404);
|
|
@@ -129,11 +215,12 @@ export class IntegrationStore {
|
|
|
129
215
|
this.write(next);
|
|
130
216
|
}
|
|
131
217
|
revoke(id) {
|
|
132
|
-
const
|
|
218
|
+
const principal = this.doc.principals.find(p => p.id === id);
|
|
133
219
|
if (!principal)
|
|
134
220
|
integrationError('INTEGRATION_NOT_FOUND', 'Integration does not exist', 404);
|
|
135
|
-
principal.revoked
|
|
136
|
-
|
|
221
|
+
if (principal.revoked)
|
|
222
|
+
return;
|
|
223
|
+
this.write({ ...this.doc, principals: this.doc.principals.map(p => p.id === id ? { ...p, revoked: true } : p) });
|
|
137
224
|
}
|
|
138
225
|
authenticate(id, token) {
|
|
139
226
|
const principal = this.doc.principals.find(p => p.id === id);
|
|
@@ -158,9 +245,10 @@ export class IntegrationStore {
|
|
|
158
245
|
for (const update of updates) {
|
|
159
246
|
if ((next ?? this.doc).sources[update.sourceKey] === update.sourceVersion)
|
|
160
247
|
continue;
|
|
161
|
-
next ??=
|
|
248
|
+
next ??= { ...this.doc, events: [...this.doc.events], sequences: { ...this.doc.sequences },
|
|
249
|
+
sources: { ...this.doc.sources }, prunedThrough: { ...this.doc.prunedThrough } };
|
|
162
250
|
const group = update.event.group_id, sequence = (next.sequences[group] ?? 0) + 1;
|
|
163
|
-
next.events.push({ ...update.event, event_id: update.eventId ?? randomUUID(), sequence, cursor: this.cursor(group, sequence) });
|
|
251
|
+
next.events.push({ ...structuredClone(update.event), event_id: update.eventId ?? randomUUID(), sequence, cursor: this.cursor(group, sequence) });
|
|
164
252
|
next.sequences[group] = sequence;
|
|
165
253
|
next.sources[update.sourceKey] = update.sourceVersion;
|
|
166
254
|
}
|
|
@@ -191,21 +279,39 @@ export class IntegrationStore {
|
|
|
191
279
|
return value.sequence;
|
|
192
280
|
}
|
|
193
281
|
key(principal, consumer) { identifier(consumer, 'consumer'); return `${principal.id}:${principal.groupId}:${consumer}`; }
|
|
194
|
-
|
|
282
|
+
selectPage(principal, consumer, cursor) {
|
|
195
283
|
this.authorize(principal, 'events.read');
|
|
196
284
|
const key = this.key(principal, consumer), group = principal.groupId;
|
|
197
285
|
const floor = this.doc.prunedThrough[group] ?? 0;
|
|
198
286
|
// A new consumer explicitly starts at the earliest retained fact. Existing
|
|
199
287
|
// consumers never silently skip a retention gap.
|
|
200
288
|
if (this.doc.consumers[key] === undefined) {
|
|
201
|
-
|
|
202
|
-
next.consumers[key] = floor;
|
|
203
|
-
this.write(next);
|
|
289
|
+
this.write({ ...this.doc, consumers: { ...this.doc.consumers, [key]: floor } });
|
|
204
290
|
}
|
|
205
291
|
const after = cursor ? this.decode(group, cursor) : this.doc.consumers[key];
|
|
206
292
|
if (after < floor)
|
|
207
293
|
integrationError('EVENT_RETENTION_GAP', 'Confirmed cursor is older than retained history; reconcile task/session snapshots and use a new consumer', 409);
|
|
208
|
-
|
|
294
|
+
const events = this.groupEvents.get(group) ?? [];
|
|
295
|
+
let lo = 0, hi = events.length;
|
|
296
|
+
while (lo < hi) {
|
|
297
|
+
const mid = (lo + hi) >>> 1;
|
|
298
|
+
if (events[mid].sequence <= after)
|
|
299
|
+
lo = mid + 1;
|
|
300
|
+
else
|
|
301
|
+
hi = mid;
|
|
302
|
+
}
|
|
303
|
+
return { events: events.slice(lo, lo + 100), cursor: this.cursor(group, after) };
|
|
304
|
+
}
|
|
305
|
+
page(principal, consumer, cursor) {
|
|
306
|
+
return structuredClone(this.selectPage(principal, consumer, cursor));
|
|
307
|
+
}
|
|
308
|
+
/** Push uses immutable encoded facts rather than copying and re-encoding
|
|
309
|
+
* every retained payload for each consumer. No mutable record escapes. */
|
|
310
|
+
streamPage(principal, consumer, cursor) {
|
|
311
|
+
const page = this.selectPage(principal, consumer, cursor);
|
|
312
|
+
return { cursor: page.cursor, events: page.events.map(event => ({ sequence: event.sequence, cursor: event.cursor,
|
|
313
|
+
line: Object.hasOwn(event, 'type') ? Buffer.from(`${JSON.stringify({ type: 'event', ...event })}\n`)
|
|
314
|
+
: Buffer.concat([EVENT_PREFIX, this.encode(event).subarray(1), NEWLINE]) })) };
|
|
209
315
|
}
|
|
210
316
|
ack(principal, consumer, cursor) {
|
|
211
317
|
this.authorize(principal, 'events.read');
|
|
@@ -218,9 +324,8 @@ export class IntegrationStore {
|
|
|
218
324
|
integrationError('EVENT_RETENTION_GAP', 'Consumer has a retention gap', 409);
|
|
219
325
|
// Cursor is a cumulative declaration that every preceding event is safely
|
|
220
326
|
// in the bridge inbox. The subscription server separately checks it was sent.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.write(next);
|
|
327
|
+
if (sequence !== confirmed)
|
|
328
|
+
this.write({ ...this.doc, consumers: { ...this.doc.consumers, [key]: sequence } });
|
|
224
329
|
return { cursor, ack_semantics: 'durable_received' };
|
|
225
330
|
}
|
|
226
331
|
cursorSequence(group, cursor) { return this.decode(group, cursor); }
|
|
@@ -11,8 +11,23 @@ export function nativeIdentityDirectory(config) {
|
|
|
11
11
|
const expanded = c.directory.startsWith('~/') ? path.join(os.homedir(), c.directory.slice(2)) : c.directory;
|
|
12
12
|
if (!path.isAbsolute(expanded))
|
|
13
13
|
return null;
|
|
14
|
-
const directory = path.resolve(expanded),
|
|
15
|
-
|
|
14
|
+
const directory = path.resolve(expanded), home = os.homedir();
|
|
15
|
+
const within = (root) => {
|
|
16
|
+
const relative = path.relative(root, directory);
|
|
17
|
+
return !!relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
18
|
+
};
|
|
19
|
+
if (within(home))
|
|
20
|
+
return directory;
|
|
21
|
+
// cwd is canonicalized before launch. A symlinked home may therefore have
|
|
22
|
+
// two legitimate spellings even before the lock directory is created.
|
|
23
|
+
// Accept only the real home alias; the live provider still verifies the
|
|
24
|
+
// actual directory ownership, realpath, FD/inode, starttime and kernel lock.
|
|
25
|
+
try {
|
|
26
|
+
return within(fs.realpathSync(home)) ? directory : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
16
31
|
}
|
|
17
32
|
/** Administrator-declared profile scope, never inferred from process env.
|
|
18
33
|
* {cwd} is permitted only as a leading directory anchor and cannot escape it. */
|
|
@@ -77,6 +77,7 @@ import { CollaborationError } from '../agent/collaborationProtocol.js';
|
|
|
77
77
|
import { resolveCollaborationBackend, resolveCollaborationSessionId } from '../agent/sessionBindingRecovery.js';
|
|
78
78
|
import { CollaborationRoutingStore, selectCollaborationPane, selectDrivePane, assessCollaborationRouteIdentity } from '../agent/collaborationRouting.js';
|
|
79
79
|
import { selectRecoveredIntegrationBinding } from '../agent/integrationRecoveryBinding.js';
|
|
80
|
+
import { readTmuxLayout, readTmuxLayouts } from '../utils/tmuxLayoutReader.js';
|
|
80
81
|
import { CollaborationDeliveryWorker } from '../agent/collaborationDeliveryWorker.js';
|
|
81
82
|
import { approveCollaborationDialog, captureTmuxPaneHistory, captureTmuxPaneText, sendTmuxPaneKey, writeCollaborationTmuxPane } from '../agent/collaborationTmuxDelivery.js';
|
|
82
83
|
import { listAllHookAgents, refreshStaleHooksAtLaunch, installHooksForSlug, uninstallHooksForSlug, } from '../agent/installers.js';
|
|
@@ -1724,9 +1725,9 @@ function formatLocalCollaborationMessages(frontendSessionId, messages) {
|
|
|
1724
1725
|
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false, describeNativeFailure = false, identityOverride) {
|
|
1725
1726
|
if (!binding.tmuxSessionName)
|
|
1726
1727
|
return { state: 'offline', reason: 'TMUX_BINDING_MISSING' };
|
|
1727
|
-
|
|
1728
|
+
let layout;
|
|
1728
1729
|
try {
|
|
1729
|
-
await
|
|
1730
|
+
layout = await getTmuxLayout(binding.tmuxSessionName);
|
|
1730
1731
|
}
|
|
1731
1732
|
catch (error) {
|
|
1732
1733
|
if (/can't find session|no server running|no sessions|error connecting.*No such file/i.test(getErrorMessage(error))) {
|
|
@@ -1734,10 +1735,9 @@ async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane =
|
|
|
1734
1735
|
}
|
|
1735
1736
|
throw error;
|
|
1736
1737
|
}
|
|
1737
|
-
const layout = await getTmuxLayout(binding.tmuxSessionName);
|
|
1738
1738
|
if (layout.sessionName !== binding.tmuxSessionName)
|
|
1739
1739
|
throw new Error('TMUX_SESSION_IDENTITY_CHANGED');
|
|
1740
|
-
const serverPid = Number(
|
|
1740
|
+
const serverPid = Number(layout.serverPid);
|
|
1741
1741
|
if (!Number.isInteger(serverPid) || serverPid <= 0)
|
|
1742
1742
|
throw new Error('TMUX_SERVER_IDENTITY_UNAVAILABLE');
|
|
1743
1743
|
if ((nativeProcessIdentity || freshProcess) && process.platform === 'linux')
|
|
@@ -2150,6 +2150,11 @@ async function integrationTmuxPresent(record) {
|
|
|
2150
2150
|
throw error;
|
|
2151
2151
|
}
|
|
2152
2152
|
}
|
|
2153
|
+
function integrationRuntimeRegistered(record) {
|
|
2154
|
+
return !!integrationTerminalRecord(record)
|
|
2155
|
+
|| [...terminalSessions.values()].some(s => s.mode === 'tmux' && s.tmuxSessionName === `wt-integration-${record.session_id}`)
|
|
2156
|
+
|| !!collaborationStore.getGroup(record.group_id)?.sessionIds.includes(record.session_id);
|
|
2157
|
+
}
|
|
2153
2158
|
async function verifyIntegrationRuntimeOwnership(record) {
|
|
2154
2159
|
await integrationSessionAdapter.prepareRelease(record);
|
|
2155
2160
|
const terminal = integrationTerminalRecord(record);
|
|
@@ -2290,14 +2295,22 @@ export const integrationSessionAdapter = {
|
|
|
2290
2295
|
resume: getPluginResumeConfig(agent.slug) ?? command, nativeIdentity: integrationNativeIdentity(record) ?? getPluginNativeIdentityConfig(agent.slug) ?? null };
|
|
2291
2296
|
},
|
|
2292
2297
|
async runtimePresent(record) {
|
|
2293
|
-
if (
|
|
2294
|
-
return true;
|
|
2295
|
-
if ([...terminalSessions.values()].some(s => s.mode === 'tmux' && s.tmuxSessionName === `wt-integration-${record.session_id}`))
|
|
2296
|
-
return true;
|
|
2297
|
-
if (collaborationStore.getGroup(record.group_id)?.sessionIds.includes(record.session_id))
|
|
2298
|
+
if (integrationRuntimeRegistered(record))
|
|
2298
2299
|
return true;
|
|
2299
2300
|
return integrationTmuxPresent(record);
|
|
2300
2301
|
},
|
|
2302
|
+
async runtimePresenceBatch(records) {
|
|
2303
|
+
let names;
|
|
2304
|
+
try {
|
|
2305
|
+
names = new Set((await runTmux(['list-sessions', '-F', '#{session_name}'])).trimEnd().split('\n'));
|
|
2306
|
+
}
|
|
2307
|
+
catch (error) {
|
|
2308
|
+
if (!/no server running|no sessions|error connecting.*No such file/i.test(getErrorMessage(error)))
|
|
2309
|
+
throw error;
|
|
2310
|
+
names = new Set();
|
|
2311
|
+
}
|
|
2312
|
+
return new Map(records.map(record => [record.session_id, integrationRuntimeRegistered(record) || names.has(`wt-integration-${record.session_id}`)]));
|
|
2313
|
+
},
|
|
2301
2314
|
async release(record, reconcile) {
|
|
2302
2315
|
await releaseSessionRuntime({
|
|
2303
2316
|
barrier: action => collaborationDeliveryWorker.reconfigure(record.session_id, action),
|
|
@@ -5069,166 +5082,12 @@ async function prepareManagedTmuxSession(sessionName, cwd) {
|
|
|
5069
5082
|
await tmuxLifecycle.run(`prepare:${sessionName}`, () => prepareManagedTmuxSessionUnlocked(sessionName, cwd));
|
|
5070
5083
|
}
|
|
5071
5084
|
async function getTmuxLayout(sessionName) {
|
|
5072
|
-
|
|
5073
|
-
'display-message',
|
|
5074
|
-
'-t',
|
|
5075
|
-
sessionName,
|
|
5076
|
-
'-p',
|
|
5077
|
-
`#{session_id}${TMUX_DELIMITER}#{session_name}${TMUX_DELIMITER}#{window_id}${TMUX_DELIMITER}#{pane_id}${TMUX_DELIMITER}#{pane_in_mode}`,
|
|
5078
|
-
])).trim();
|
|
5079
|
-
const sessionInfo = parseDelimitedRow(sessionInfoRaw, 5);
|
|
5080
|
-
if (!sessionInfo) {
|
|
5081
|
-
throw new Error(`Failed to parse tmux session info: ${sessionInfoRaw}`);
|
|
5082
|
-
}
|
|
5083
|
-
const [sessionId, resolvedSessionName, activeWindowId, activePaneId, paneInMode] = sessionInfo;
|
|
5084
|
-
const windowsRaw = await runTmux([
|
|
5085
|
-
'list-windows',
|
|
5086
|
-
'-t',
|
|
5087
|
-
sessionName,
|
|
5088
|
-
'-F',
|
|
5089
|
-
`#{window_id}${TMUX_DELIMITER}#{window_name}${TMUX_DELIMITER}#{window_index}${TMUX_DELIMITER}#{window_active}`,
|
|
5090
|
-
]);
|
|
5091
|
-
const windows = [];
|
|
5092
|
-
for (const line of windowsRaw.trim().split('\n')) {
|
|
5093
|
-
if (!line) {
|
|
5094
|
-
continue;
|
|
5095
|
-
}
|
|
5096
|
-
const row = parseDelimitedRow(line, 4);
|
|
5097
|
-
if (!row) {
|
|
5098
|
-
continue;
|
|
5099
|
-
}
|
|
5100
|
-
const [windowId, windowName, windowIndexRaw, windowActiveRaw] = row;
|
|
5101
|
-
const panesRaw = await runTmux([
|
|
5102
|
-
'list-panes',
|
|
5103
|
-
'-t',
|
|
5104
|
-
windowId,
|
|
5105
|
-
'-F',
|
|
5106
|
-
`#{pane_id}${TMUX_DELIMITER}#{pane_index}${TMUX_DELIMITER}#{pane_active}${TMUX_DELIMITER}#{pane_width}${TMUX_DELIMITER}#{pane_height}${TMUX_DELIMITER}#{pane_top}${TMUX_DELIMITER}#{pane_left}${TMUX_DELIMITER}#{pane_current_command}${TMUX_DELIMITER}#{pane_pid}${TMUX_DELIMITER}#{pane_tty}${TMUX_DELIMITER}#{pane_title}${TMUX_DELIMITER}#{pane_current_path}`,
|
|
5107
|
-
]);
|
|
5108
|
-
const panes = panesRaw.trim().split('\n').filter(Boolean).map((paneLine) => {
|
|
5109
|
-
const paneRow = parseDelimitedRow(paneLine, 12);
|
|
5110
|
-
if (!paneRow) {
|
|
5111
|
-
return null;
|
|
5112
|
-
}
|
|
5113
|
-
const [paneId, paneIndexRaw, paneActiveRaw, widthRaw, heightRaw, topRaw, leftRaw, command, pidRaw, tty, title, currentPath] = paneRow;
|
|
5114
|
-
return {
|
|
5115
|
-
id: paneId,
|
|
5116
|
-
index: parseInt(paneIndexRaw || '0', 10),
|
|
5117
|
-
active: paneActiveRaw === '1',
|
|
5118
|
-
width: parseInt(widthRaw || '0', 10),
|
|
5119
|
-
height: parseInt(heightRaw || '0', 10),
|
|
5120
|
-
top: parseInt(topRaw || '0', 10),
|
|
5121
|
-
left: parseInt(leftRaw || '0', 10),
|
|
5122
|
-
command: command || '',
|
|
5123
|
-
pid: parseInt(pidRaw || '0', 10) || 0,
|
|
5124
|
-
tty: tty || '',
|
|
5125
|
-
title: title || '',
|
|
5126
|
-
currentPath: currentPath || '',
|
|
5127
|
-
};
|
|
5128
|
-
}).filter((pane) => pane !== null);
|
|
5129
|
-
windows.push({
|
|
5130
|
-
id: windowId,
|
|
5131
|
-
name: windowName || '',
|
|
5132
|
-
index: parseInt(windowIndexRaw || '0', 10),
|
|
5133
|
-
active: windowActiveRaw === '1',
|
|
5134
|
-
panes,
|
|
5135
|
-
});
|
|
5136
|
-
}
|
|
5137
|
-
return {
|
|
5138
|
-
sessionId,
|
|
5139
|
-
sessionName: resolvedSessionName,
|
|
5140
|
-
windows,
|
|
5141
|
-
activeWindowId,
|
|
5142
|
-
activePaneId,
|
|
5143
|
-
inCopyMode: paneInMode === '1',
|
|
5144
|
-
};
|
|
5085
|
+
return readTmuxLayout(runTmux, sessionName);
|
|
5145
5086
|
}
|
|
5146
5087
|
let tmuxLayoutsSnapshot = null;
|
|
5147
5088
|
let tmuxLayoutsSnapshotPromise = null;
|
|
5148
5089
|
async function buildTmuxLayoutsSnapshot() {
|
|
5149
|
-
|
|
5150
|
-
runTmux([
|
|
5151
|
-
'list-sessions',
|
|
5152
|
-
'-F',
|
|
5153
|
-
`#{session_id}${TMUX_DELIMITER}#{session_name}${TMUX_DELIMITER}#{window_id}${TMUX_DELIMITER}#{pane_id}${TMUX_DELIMITER}#{pane_in_mode}`,
|
|
5154
|
-
]),
|
|
5155
|
-
runTmux([
|
|
5156
|
-
'list-windows',
|
|
5157
|
-
'-a',
|
|
5158
|
-
'-F',
|
|
5159
|
-
`#{session_name}${TMUX_DELIMITER}#{window_id}${TMUX_DELIMITER}#{window_name}${TMUX_DELIMITER}#{window_index}${TMUX_DELIMITER}#{window_active}`,
|
|
5160
|
-
]),
|
|
5161
|
-
runTmux([
|
|
5162
|
-
'list-panes',
|
|
5163
|
-
'-a',
|
|
5164
|
-
'-F',
|
|
5165
|
-
`#{session_name}${TMUX_DELIMITER}#{window_id}${TMUX_DELIMITER}#{pane_id}${TMUX_DELIMITER}#{pane_index}${TMUX_DELIMITER}#{pane_active}${TMUX_DELIMITER}#{pane_width}${TMUX_DELIMITER}#{pane_height}${TMUX_DELIMITER}#{pane_top}${TMUX_DELIMITER}#{pane_left}${TMUX_DELIMITER}#{pane_current_command}${TMUX_DELIMITER}#{pane_pid}${TMUX_DELIMITER}#{pane_tty}${TMUX_DELIMITER}#{pane_title}${TMUX_DELIMITER}#{pane_current_path}`,
|
|
5166
|
-
]),
|
|
5167
|
-
]);
|
|
5168
|
-
const layouts = new Map();
|
|
5169
|
-
for (const line of sessionsRaw.trim().split('\n')) {
|
|
5170
|
-
if (!line)
|
|
5171
|
-
continue;
|
|
5172
|
-
const row = parseDelimitedRow(line, 5);
|
|
5173
|
-
if (!row)
|
|
5174
|
-
continue;
|
|
5175
|
-
const [sessionId, sessionName, activeWindowId, activePaneId, paneInMode] = row;
|
|
5176
|
-
layouts.set(sessionName, {
|
|
5177
|
-
sessionId,
|
|
5178
|
-
sessionName,
|
|
5179
|
-
windows: [],
|
|
5180
|
-
activeWindowId,
|
|
5181
|
-
activePaneId,
|
|
5182
|
-
inCopyMode: paneInMode === '1',
|
|
5183
|
-
});
|
|
5184
|
-
}
|
|
5185
|
-
const windowsById = new Map();
|
|
5186
|
-
for (const line of windowsRaw.trim().split('\n')) {
|
|
5187
|
-
if (!line)
|
|
5188
|
-
continue;
|
|
5189
|
-
const row = parseDelimitedRow(line, 5);
|
|
5190
|
-
if (!row)
|
|
5191
|
-
continue;
|
|
5192
|
-
const [sessionName, windowId, windowName, windowIndexRaw, windowActiveRaw] = row;
|
|
5193
|
-
const layout = layouts.get(sessionName);
|
|
5194
|
-
if (!layout)
|
|
5195
|
-
continue;
|
|
5196
|
-
const window = {
|
|
5197
|
-
id: windowId,
|
|
5198
|
-
name: windowName || '',
|
|
5199
|
-
index: parseInt(windowIndexRaw || '0', 10),
|
|
5200
|
-
active: windowActiveRaw === '1',
|
|
5201
|
-
panes: [],
|
|
5202
|
-
};
|
|
5203
|
-
layout.windows.push(window);
|
|
5204
|
-
windowsById.set(`${sessionName}\0${windowId}`, window);
|
|
5205
|
-
}
|
|
5206
|
-
for (const line of panesRaw.trim().split('\n')) {
|
|
5207
|
-
if (!line)
|
|
5208
|
-
continue;
|
|
5209
|
-
const row = parseDelimitedRow(line, 14);
|
|
5210
|
-
if (!row)
|
|
5211
|
-
continue;
|
|
5212
|
-
const [sessionName, windowId, paneId, paneIndexRaw, paneActiveRaw, widthRaw, heightRaw, topRaw, leftRaw, command, pidRaw, tty, title, currentPath] = row;
|
|
5213
|
-
const window = windowsById.get(`${sessionName}\0${windowId}`);
|
|
5214
|
-
if (!window)
|
|
5215
|
-
continue;
|
|
5216
|
-
window.panes.push({
|
|
5217
|
-
id: paneId,
|
|
5218
|
-
index: parseInt(paneIndexRaw || '0', 10),
|
|
5219
|
-
active: paneActiveRaw === '1',
|
|
5220
|
-
width: parseInt(widthRaw || '0', 10),
|
|
5221
|
-
height: parseInt(heightRaw || '0', 10),
|
|
5222
|
-
top: parseInt(topRaw || '0', 10),
|
|
5223
|
-
left: parseInt(leftRaw || '0', 10),
|
|
5224
|
-
command: command || '',
|
|
5225
|
-
pid: parseInt(pidRaw || '0', 10) || 0,
|
|
5226
|
-
tty: tty || '',
|
|
5227
|
-
title: title || '',
|
|
5228
|
-
currentPath: currentPath || '',
|
|
5229
|
-
});
|
|
5230
|
-
}
|
|
5231
|
-
return layouts;
|
|
5090
|
+
return readTmuxLayouts(runTmux);
|
|
5232
5091
|
}
|
|
5233
5092
|
async function getCachedTmuxLayout(sessionName) {
|
|
5234
5093
|
if (tmuxLayoutsSnapshot && Date.now() - tmuxLayoutsSnapshot.fetchedAt < TMUX_LAYOUT_CACHE_TTL_MS) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const separator = '\x1f';
|
|
2
|
+
export const TMUX_LAYOUT_FORMAT = [
|
|
3
|
+
'pid', 'session_id', 'session_name', 'window_id', 'window_name', 'window_index', 'window_active',
|
|
4
|
+
'pane_id', 'pane_index', 'pane_active', 'pane_width', 'pane_height', 'pane_top', 'pane_left',
|
|
5
|
+
'pane_current_command', 'pane_pid', 'pane_tty', 'pane_title', 'pane_in_mode', 'pane_current_path',
|
|
6
|
+
].map(field => `#{${field}}`).join(separator);
|
|
7
|
+
export function parseTmuxLayout(stdout, requestedName) {
|
|
8
|
+
let layout;
|
|
9
|
+
const windows = new Map(), ids = new Set();
|
|
10
|
+
for (const line of stdout.trimEnd().split('\n')) {
|
|
11
|
+
if (!line)
|
|
12
|
+
continue;
|
|
13
|
+
const columns = line.replaceAll('\\037', separator).replaceAll('\\x1f', separator).split(separator);
|
|
14
|
+
if (columns.length < 20)
|
|
15
|
+
throw new Error('TMUX_LAYOUT_UNAVAILABLE');
|
|
16
|
+
const [server, sessionId, sessionName, windowId, name, windowIndex, windowActive, id, index, active, width, height, top, left, command, pid, tty, title, inMode] = columns;
|
|
17
|
+
const serverPid = Number(server);
|
|
18
|
+
if (!Number.isSafeInteger(serverPid) || serverPid <= 0 || sessionName !== requestedName || !sessionId || !windowId || !id
|
|
19
|
+
|| ids.has(id) || layout && (layout.serverPid !== serverPid || layout.sessionId !== sessionId))
|
|
20
|
+
throw new Error('TMUX_SESSION_IDENTITY_CHANGED');
|
|
21
|
+
ids.add(id);
|
|
22
|
+
layout ??= { serverPid, sessionId, sessionName, windows: [], activeWindowId: '', activePaneId: '', inCopyMode: false };
|
|
23
|
+
let window = windows.get(windowId);
|
|
24
|
+
if (!window) {
|
|
25
|
+
window = { id: windowId, name, index: Number(windowIndex), active: windowActive === '1', panes: [] };
|
|
26
|
+
windows.set(windowId, window);
|
|
27
|
+
layout.windows.push(window);
|
|
28
|
+
}
|
|
29
|
+
else if (window.name !== name || window.index !== Number(windowIndex) || window.active !== (windowActive === '1')) {
|
|
30
|
+
throw new Error('TMUX_SESSION_IDENTITY_CHANGED');
|
|
31
|
+
}
|
|
32
|
+
const pane = { id, index: Number(index), active: active === '1', width: Number(width), height: Number(height),
|
|
33
|
+
top: Number(top), left: Number(left), command, pid: Number(pid), tty, title, currentPath: columns.slice(19).join(separator) };
|
|
34
|
+
if (![window.index, pane.index, pane.width, pane.height, pane.top, pane.left, pane.pid].every(Number.isSafeInteger)
|
|
35
|
+
|| pane.pid <= 0)
|
|
36
|
+
throw new Error('TMUX_LAYOUT_UNAVAILABLE');
|
|
37
|
+
window.panes.push(pane);
|
|
38
|
+
if (window.active && pane.active) {
|
|
39
|
+
if (layout.activePaneId)
|
|
40
|
+
throw new Error('TMUX_SESSION_IDENTITY_CHANGED');
|
|
41
|
+
layout.activeWindowId = window.id;
|
|
42
|
+
layout.activePaneId = pane.id;
|
|
43
|
+
layout.inCopyMode = inMode === '1';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!layout || !layout.activePaneId)
|
|
47
|
+
throw new Error('TMUX_LAYOUT_UNAVAILABLE');
|
|
48
|
+
layout.windows.sort((a, b) => a.index - b.index);
|
|
49
|
+
for (const window of layout.windows)
|
|
50
|
+
window.panes.sort((a, b) => a.index - b.index);
|
|
51
|
+
return layout;
|
|
52
|
+
}
|
|
53
|
+
export async function readTmuxLayout(run, sessionName) {
|
|
54
|
+
return parseTmuxLayout(await run(['list-panes', '-s', '-t', `=${sessionName}:`, '-F', TMUX_LAYOUT_FORMAT]), sessionName);
|
|
55
|
+
}
|
|
56
|
+
export async function readTmuxLayouts(run) {
|
|
57
|
+
const rows = new Map();
|
|
58
|
+
for (const row of (await run(['list-panes', '-a', '-F', TMUX_LAYOUT_FORMAT])).trimEnd().split('\n')) {
|
|
59
|
+
const name = row.replaceAll('\\037', separator).replaceAll('\\x1f', separator).split(separator)[2];
|
|
60
|
+
if (!name)
|
|
61
|
+
continue;
|
|
62
|
+
const lines = rows.get(name) ?? [];
|
|
63
|
+
lines.push(row);
|
|
64
|
+
rows.set(name, lines);
|
|
65
|
+
}
|
|
66
|
+
const layouts = new Map();
|
|
67
|
+
for (const [name, lines] of rows) {
|
|
68
|
+
try {
|
|
69
|
+
layouts.set(name, parseTmuxLayout(lines.join('\n'), name));
|
|
70
|
+
}
|
|
71
|
+
catch { /* UI discovery can fall back to an exact fresh read of that session. */ }
|
|
72
|
+
}
|
|
73
|
+
return layouts;
|
|
74
|
+
}
|
package/docs/integrations.md
CHANGED
|
@@ -1394,3 +1394,20 @@ wrapper 负责实际设置 Agent 自己的数据目录、准备 hooks/参数,
|
|
|
1394
1394
|
- 终端输出触发的后台会话观察按 250ms 窗口合并;一轮观察期间的新通知安排到后续一轮,不连续抢占。显式 `session get`、生命周期操作及投递前核验保留原路径,不以此窗口判断 Agent 可输入;不延迟业务输入来猜测应用状态。同一次 integration 观察复用其新鲜 tmux layout,下一次观察重新读取,不把缓存用作身份写入凭据。
|
|
1395
1395
|
|
|
1396
1396
|
无删除消息/事件、缩短历史、停用消费者、额外 Enter 或正文重发。`pty_written` 仍只代表终端写入调用完成,不代表应用提交或任务完成。回归包含约 26 MB journal 的空/重复追加零克隆、相同群同步零写入、真实变化/持久化失败重试、授权后历史补收、通知洪峰合并、换 pane/原生身份冲突拒绝。生产 CPU、事件循环延迟和 health 改善须在实际维护窗升级后对比采样,不能用隔离基准代替实机验收。
|
|
1397
|
+
|
|
1398
|
+
## 1.4.348:真实提交、事件推送与 tmux 查询
|
|
1399
|
+
|
|
1400
|
+
本轮透明性能优化不改变现有接口、JSON journal 格式、保留策略、权限、消息/ACK 语义或默认交互。实际运行服务需升级到 **1.4.348**;不是仅替换 CLI 文件。性能修复无新增权限或配置。
|
|
1401
|
+
|
|
1402
|
+
- 真实事件/ACK/授权变更采用 copy-on-write,仅复制发生变化的容器与新事件,不再深拷贝整个 journal。内部已提交事实由 store 独占且不修改,公共 page/policy 返回独立副本;调用者修改输入或返回值不影响历史或编码缓存。
|
|
1403
|
+
- 未变化事件与元数据复用 UTF-8 编码,按有界向量写入临时文件,不再为大小检查和落盘重复生成全库 JSON,也避免临时巨型 UTF-16 字符串。64 MiB 上限检查覆盖实际待写全部字节。短写、Unicode 字节边界与 EINTR 均保留位置;文件 fsync → 原子 rename → 目录 fsync 后才发布提交结果。中途失败不推进内存 source/consumer 状态,不向订阅者报告成功;重试仍沿原来源/游标。
|
|
1404
|
+
- 同 cursor 重复 ACK 在权限、consumer、cursor、rewind、retention 检查后 no-op;HTTP 的已发送连续前缀检查仍先执行。此前已持久确认的进度不再次克隆/写盘/fsync,不能借相等游标绕过撤权或保留缺口。重复 revoke 也不重写未变化状态。
|
|
1405
|
+
- 事件按组建引用索引,并按 sequence 二分定位 page;仅公开 page 复制最多 100 条返回事实。主动推送复用已编码事实,不为每个 consumer 重复制/重编码历史 payload,保留原 NDJSON 字段、顺序、背压、授权复核及断线重放边界。
|
|
1406
|
+
- 新鲜 session layout 由一次只读 `list-panes -s` 获取 server/session/window/pane 元数据;常见单窗口的 integration 观察从 5 次 tmux 命令降到 1 次,另行原生进程/锁核验保持。侧栏原有多会话 layout 缓存的数据获取也由 3 次全局查询改为 1 次;缓存仍只服务原有发现用途。没有启用持久 attach/control client,不新增 client、改尺寸、焦点、copy-mode 或发送输入。
|
|
1407
|
+
- 同轮已关闭/释放且已确认零承载的后台存在性观察用一次新鲜会话名查询合并。结果仅用于该轮、且 descriptor 完整快照不变时才复用;批量查询失败、记录改变、操作不确定或进行中的生命周期走原逐项核验。显式 get、close/release/restore、实际投递前身份核验不使用该后台结果。
|
|
1408
|
+
|
|
1409
|
+
仍是兼容的全量 JSON 快照落盘:真实提交的物理写入量和同步 fsync 等待未变为增量日志或异步存储,慢磁盘仍可能阻塞。不能把隔离基准解释为所有 HTTPS 长尾已解决。服务重启会重建内存索引/编码缓存,首次提交需编码保留事实;不会删除历史换取速度,也不会暂停业务消费者。
|
|
1410
|
+
|
|
1411
|
+
回归覆盖重复 ACK 零克隆/编码/写入、真实持久提交与中途写入/fsync 失败后重试、短写与 Unicode、64 MiB 精确字节上限、输入/输出不可修改历史、分页/保留缺口/撤权、推送事实等价,以及隔离真实 tmux 的多窗口/分屏/激活 pane/copy-mode 与 client/PID/尺寸不变。Linux 维护机验证不代替 macOS 实机或私有 Agent 的业务验收。生产升级须与共享服务的在途任务和维护窗协调,不能因为收到版本通知就自动重启别人的实验。
|
|
1412
|
+
|
|
1413
|
+
同时修复独立 profile 目录的 home 别名误拒:当逻辑 home 指向真实 home,规范化 cwd 下的 `{cwd}` 路径可匹配逻辑 home 或 `realpath(home)` 的严格子目录;不会将同名前缀兄弟目录视为 home,不创建尚不存在的锁目录。实际 provider 继续核验 canonicalHome、目录权限、精确进程实例、FD/inode 和内核持锁关系。Linux capabilities 新增 `session_profile_native_identity_home_aliases=true` 及 `session_profile_native_identity_home_aliases_min_version="1.4.348"`。旧 profile 快照不迁移,不自动 retry/create;347 的 `~/` 每 job 精确路径声明仍可沿原契约使用,更新同一 profile 与捕获快照需要由接入方串行协调。
|
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.348",
|
|
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-Jai7Kpw3RvG8IR/qVeAVGADGQlxRldYgpIR5+wLY9so=",
|
|
10
10
|
"clientBundleHash": "sha256-kn3Tg+BTj+H4rZe0g9OE5JUEQNsBGiUSGew8Epm5A38=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|