shennian 0.3.86 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/shennian.js +1 -1
  3. package/dist/publish-build-manifest.json +32 -27
  4. package/dist/src/agents/adapter.js +1 -19
  5. package/dist/src/agents/claude-managed-permissions.js +1 -248
  6. package/dist/src/agents/claude.js +8 -393
  7. package/dist/src/agents/codex-managed-permissions.js +1 -294
  8. package/dist/src/agents/codex-managed-room-tools.js +1 -249
  9. package/dist/src/agents/codex-utils.js +7 -200
  10. package/dist/src/agents/codex.d.ts +7 -1
  11. package/dist/src/agents/codex.js +17 -1352
  12. package/dist/src/agents/config-status.js +1 -226
  13. package/dist/src/agents/cursor-managed-permissions.js +2 -299
  14. package/dist/src/agents/cursor.js +1 -314
  15. package/dist/src/agents/custom.js +4 -418
  16. package/dist/src/agents/external-channel-instructions.js +11 -94
  17. package/dist/src/agents/gemini.js +1 -180
  18. package/dist/src/agents/manager.js +18 -250
  19. package/dist/src/agents/model-registry/cache.js +1 -37
  20. package/dist/src/agents/model-registry/discovery.js +2 -188
  21. package/dist/src/agents/model-registry/parsers.js +4 -521
  22. package/dist/src/agents/model-registry/runner.js +1 -30
  23. package/dist/src/agents/model-registry/service.js +1 -115
  24. package/dist/src/agents/model-registry/types.js +1 -8
  25. package/dist/src/agents/model-registry.js +1 -19
  26. package/dist/src/agents/native-capabilities.js +1 -237
  27. package/dist/src/agents/openclaw.js +2 -293
  28. package/dist/src/agents/opencode-managed-permissions.js +2 -187
  29. package/dist/src/agents/opencode.js +1 -298
  30. package/dist/src/agents/platform-instructions.js +17 -46
  31. package/dist/src/agents/runtime-compatibility.js +1 -44
  32. package/dist/src/agents/workbuddy-managed-permissions.js +2 -329
  33. package/dist/src/agents/workbuddy.js +3 -444
  34. package/dist/src/commands/logout.d.ts +3 -0
  35. package/dist/src/commands/logout.js +1 -0
  36. package/dist/src/daemon-ipc/paths.js +1 -1
  37. package/dist/src/fs/boundary.js +1 -185
  38. package/dist/src/fs/handler.js +1 -130
  39. package/dist/src/fs/security.js +1 -32
  40. package/dist/src/fs/text-decoder.js +1 -110
  41. package/dist/src/index.js +3 -587
  42. package/dist/src/manager/prompt.js +29 -34
  43. package/dist/src/manager/registry.js +1 -317
  44. package/dist/src/manager/retired-external-channel-compat.js +1 -40
  45. package/dist/src/manager/runtime.js +23 -1514
  46. package/dist/src/native-fusion/codex-parser.d.ts +3 -1
  47. package/dist/src/native-fusion/codex-parser.js +4 -660
  48. package/dist/src/native-fusion/codex-user-input.js +5 -102
  49. package/dist/src/native-fusion/config.js +1 -5
  50. package/dist/src/native-fusion/file-watcher.js +1 -191
  51. package/dist/src/native-fusion/local-repository.js +2 -569
  52. package/dist/src/native-fusion/opencode-parser.js +3 -130
  53. package/dist/src/native-fusion/parser-common.d.ts +6 -1
  54. package/dist/src/native-fusion/parser-common.js +4 -4
  55. package/dist/src/native-fusion/parsers.d.ts +4 -2
  56. package/dist/src/native-fusion/parsers.js +3 -140
  57. package/dist/src/native-fusion/service.js +2 -739
  58. package/dist/src/native-fusion/session-header.js +2 -101
  59. package/dist/src/native-fusion/state.js +2 -104
  60. package/dist/src/native-fusion/types.js +1 -1
  61. package/dist/src/personal-sync/machine-authority.js +1 -34
  62. package/dist/src/room/activation-audit.js +2 -45
  63. package/dist/src/room/activation-policy.js +1 -30
  64. package/dist/src/room/activation-service.js +8 -408
  65. package/dist/src/room/invite-bootstrap.js +1 -156
  66. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  67. package/dist/src/room/managed-prompt-composer.js +17 -16
  68. package/dist/src/room/managed-room-binding.js +1 -167
  69. package/dist/src/room/managed-room-ipc.d.ts +4 -0
  70. package/dist/src/room/managed-room-ipc.js +2 -2
  71. package/dist/src/room/mirror-sync-service.d.ts +1 -0
  72. package/dist/src/room/mirror-sync-service.js +1 -183
  73. package/dist/src/room/room-binding-client.js +1 -151
  74. package/dist/src/session/archive-zip.js +1 -220
  75. package/dist/src/session/deletion-sync-state.js +2 -136
  76. package/dist/src/session/handlers/agent-capabilities.js +1 -14
  77. package/dist/src/session/handlers/agent-config.js +1 -150
  78. package/dist/src/session/handlers/agents.js +1 -55
  79. package/dist/src/session/handlers/chat.js +2 -1030
  80. package/dist/src/session/handlers/control.js +1 -61
  81. package/dist/src/session/handlers/fs.js +1 -594
  82. package/dist/src/session/handlers/message-status.js +1 -15
  83. package/dist/src/session/handlers/remote-access.js +1 -43
  84. package/dist/src/session/handlers/room-managed.js +1 -101
  85. package/dist/src/session/handlers/session-refresh.js +1 -35
  86. package/dist/src/session/handlers/title.js +1 -79
  87. package/dist/src/session/handlers/tool-detail.js +1 -149
  88. package/dist/src/session/handlers/voice-window.js +1 -62
  89. package/dist/src/session/history-subscriptions.js +1 -470
  90. package/dist/src/session/manager.d.ts +1 -0
  91. package/dist/src/session/manager.js +1 -658
  92. package/dist/src/session/native-cleanup-outbox.js +2 -287
  93. package/dist/src/session/native-session-lifecycle.js +6 -327
  94. package/dist/src/session/queue.d.ts +8 -0
  95. package/dist/src/session/queue.js +6 -6
  96. package/dist/src/upgrade/scheduler.js +1 -126
  97. package/node_modules/@shennian/wire/dist/agent-workspace.js +2 -2
  98. package/package.json +1 -1
@@ -1,287 +1,2 @@
1
- // @arch docs/features/native-session-deletion-sync.md#七本机原生清理-outbox
2
- // @test src/__tests__/native-cleanup-outbox.test.ts
3
- import crypto from 'node:crypto';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { resolvePersonalSyncV1Path } from '../personal-sync/paths.js';
7
- import { getDaemonInstallationId } from '../personal-sync/identity.js';
8
- import { NativeSourceDeletionStore } from '../native-fusion/native-source-deletions.js';
9
- import { NativeSessionLifecycleRegistry, } from './native-session-lifecycle.js';
10
- const SCHEMA_VERSION = 1;
11
- const DEFAULT_MAX_ATTEMPTS = 8;
12
- const DEFAULT_DRAIN_INTERVAL_MS = 60_000;
13
- const MAX_BACKOFF_MS = 6 * 60 * 60 * 1_000;
14
- const TERMINAL_STATES = new Set([
15
- 'succeeded_archived',
16
- 'succeeded_deleted',
17
- 'succeeded_already_absent',
18
- 'skipped_unsupported',
19
- 'skipped_invalid_identity',
20
- 'failed_permanent',
21
- ]);
22
- export class NativeSessionCleanupOutbox {
23
- options;
24
- lifecycle;
25
- timer = null;
26
- drainPromise = null;
27
- constructor(options = {}) {
28
- this.options = options;
29
- this.lifecycle = options.lifecycle ?? new NativeSessionLifecycleRegistry();
30
- }
31
- start() {
32
- if (this.timer)
33
- return;
34
- void this.drain();
35
- this.timer = setInterval(() => void this.drain(), this.options.drainIntervalMs ?? DEFAULT_DRAIN_INTERVAL_MS);
36
- this.timer.unref?.();
37
- }
38
- stop() {
39
- if (this.timer)
40
- clearInterval(this.timer);
41
- this.timer = null;
42
- }
43
- enqueue(deletion) {
44
- if ((deletion.nativeCleanupPolicy ?? 'none') !== 'auto')
45
- return [];
46
- const file = this.read(true);
47
- const created = [];
48
- for (const source of deletion.sources) {
49
- const jobId = this.jobId(deletion, source.agentType, source.sourceSessionKey);
50
- const existing = file.jobs[jobId];
51
- if (existing) {
52
- created.push(existing);
53
- continue;
54
- }
55
- const job = {
56
- schemaVersion: SCHEMA_VERSION,
57
- daemonInstallationId: this.installationId(),
58
- jobId,
59
- sessionId: deletion.sessionId,
60
- deletionRevision: deletion.deletionRevision,
61
- agentType: source.agentType,
62
- sourceSessionKey: source.sourceSessionKey,
63
- ...(deletion.workDir ? { workDir: deletion.workDir } : {}),
64
- requestedAt: deletion.deletedAt,
65
- state: 'pending',
66
- attempts: 0,
67
- };
68
- file.jobs[jobId] = job;
69
- created.push(job);
70
- }
71
- if (created.some((job) => job.state === 'pending'))
72
- this.write(file);
73
- return created;
74
- }
75
- kick() {
76
- void this.drain();
77
- }
78
- async drain() {
79
- if (this.drainPromise)
80
- return await this.drainPromise;
81
- this.drainPromise = this.drainOnce().finally(() => {
82
- this.drainPromise = null;
83
- });
84
- return await this.drainPromise;
85
- }
86
- listJobs() {
87
- return Object.values(this.read(true).jobs).sort((left, right) => left.deletionRevision - right.deletionRevision || left.jobId.localeCompare(right.jobId));
88
- }
89
- async drainOnce() {
90
- // One global worker is an intentional upper bound. It also guarantees that
91
- // every Provider is serialized without a second lock hierarchy.
92
- while (true) {
93
- const file = this.read(true);
94
- const now = this.now();
95
- const job = Object.values(file.jobs)
96
- .filter((candidate) => candidate.state === 'pending' ||
97
- (candidate.state === 'retry_wait' &&
98
- (!candidate.nextAttemptAt || Date.parse(candidate.nextAttemptAt) <= now.getTime())))
99
- .sort((left, right) => left.deletionRevision - right.deletionRevision || left.jobId.localeCompare(right.jobId))[0];
100
- if (!job)
101
- return;
102
- job.state = 'running';
103
- job.attempts += 1;
104
- delete job.nextAttemptAt;
105
- this.write(file);
106
- let result;
107
- if (!this.sourceGuard(job)) {
108
- result = { outcome: 'invalid_identity' };
109
- }
110
- else {
111
- try {
112
- result = await this.lifecycle.execute(job.agentType, job.sourceSessionKey, {
113
- workDir: job.workDir,
114
- });
115
- }
116
- catch {
117
- result = { outcome: 'transient', errorCode: 'lifecycle_unexpected_failure' };
118
- }
119
- }
120
- this.applyResult(job.jobId, result);
121
- }
122
- }
123
- applyResult(jobId, result) {
124
- const file = this.read(false);
125
- const job = file.jobs[jobId];
126
- if (!job || TERMINAL_STATES.has(job.state))
127
- return;
128
- const completedAt = this.now().toISOString();
129
- switch (result.outcome) {
130
- case 'archived':
131
- job.state = 'succeeded_archived';
132
- break;
133
- case 'deleted':
134
- job.state = 'succeeded_deleted';
135
- break;
136
- case 'already_absent':
137
- job.state = 'succeeded_already_absent';
138
- break;
139
- case 'unsupported':
140
- job.state = 'skipped_unsupported';
141
- break;
142
- case 'invalid_identity':
143
- job.state = 'skipped_invalid_identity';
144
- break;
145
- case 'permanent':
146
- job.state = 'failed_permanent';
147
- job.lastErrorCode = result.errorCode;
148
- break;
149
- case 'transient': {
150
- job.lastErrorCode = result.errorCode;
151
- if (job.attempts >= (this.options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)) {
152
- job.state = 'failed_permanent';
153
- break;
154
- }
155
- job.state = 'retry_wait';
156
- const delay = Math.min((this.options.retryBaseMs ?? 5_000) * 2 ** Math.max(0, job.attempts - 1), MAX_BACKOFF_MS);
157
- job.nextAttemptAt = new Date(this.now().getTime() + delay).toISOString();
158
- delete job.completedAt;
159
- this.write(file);
160
- this.log(job, result.errorCode);
161
- return;
162
- }
163
- }
164
- job.completedAt = completedAt;
165
- delete job.nextAttemptAt;
166
- this.write(file);
167
- this.log(job, job.lastErrorCode ?? result.outcome);
168
- }
169
- sourceGuard(job) {
170
- if (this.options.sourceGuard)
171
- return this.options.sourceGuard(job);
172
- const deletion = new NativeSourceDeletionStore({
173
- rootDir: this.rootDir(),
174
- daemonInstallationId: this.installationId(),
175
- }).find(job.agentType, job.sourceSessionKey);
176
- return Boolean(deletion &&
177
- deletion.sessionId === job.sessionId &&
178
- deletion.deletionRevision === job.deletionRevision);
179
- }
180
- log(job, result) {
181
- this.options.logger?.('[native-cleanup] job state changed', {
182
- job: job.jobId.slice(0, 12),
183
- provider: job.agentType,
184
- state: job.state,
185
- result,
186
- attempts: job.attempts,
187
- });
188
- }
189
- read(recoverRunning) {
190
- const filePath = this.filePath();
191
- if (!fs.existsSync(filePath))
192
- return this.emptyFile();
193
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
194
- if (parsed.schemaVersion !== SCHEMA_VERSION ||
195
- parsed.daemonInstallationId !== this.installationId() ||
196
- !parsed.jobs ||
197
- typeof parsed.jobs !== 'object' ||
198
- Array.isArray(parsed.jobs)) {
199
- throw new Error('invalid native cleanup outbox');
200
- }
201
- let recovered = false;
202
- for (const [key, job] of Object.entries(parsed.jobs)) {
203
- if (key !== job.jobId ||
204
- job.schemaVersion !== SCHEMA_VERSION ||
205
- job.daemonInstallationId !== this.installationId() ||
206
- !job.sessionId ||
207
- !job.agentType ||
208
- !job.sourceSessionKey ||
209
- !Number.isSafeInteger(job.deletionRevision) ||
210
- job.deletionRevision < 1 ||
211
- !Number.isSafeInteger(job.attempts) ||
212
- job.attempts < 0 ||
213
- (!TERMINAL_STATES.has(job.state) &&
214
- job.state !== 'pending' &&
215
- job.state !== 'running' &&
216
- job.state !== 'retry_wait')) {
217
- throw new Error('invalid native cleanup job');
218
- }
219
- if (recoverRunning && job.state === 'running') {
220
- job.state = 'pending';
221
- recovered = true;
222
- }
223
- }
224
- if (recovered)
225
- this.write(parsed);
226
- return parsed;
227
- }
228
- write(file) {
229
- atomicWriteJson(this.filePath(), file);
230
- }
231
- emptyFile() {
232
- return {
233
- schemaVersion: SCHEMA_VERSION,
234
- daemonInstallationId: this.installationId(),
235
- jobs: {},
236
- };
237
- }
238
- jobId(deletion, agentType, sourceSessionKey) {
239
- return crypto
240
- .createHash('sha256')
241
- .update(`${this.installationId()}\0${deletion.deletionRevision}\0${agentType}\0${sourceSessionKey}`)
242
- .digest('hex');
243
- }
244
- installationId() {
245
- return this.options.daemonInstallationId ?? getDaemonInstallationId();
246
- }
247
- rootDir() {
248
- return this.options.rootDir ?? resolvePersonalSyncV1Path();
249
- }
250
- filePath() {
251
- return path.join(this.rootDir(), 'native-session-cleanup-outbox.json');
252
- }
253
- now() {
254
- return this.options.now?.() ?? new Date();
255
- }
256
- }
257
- function atomicWriteJson(file, value) {
258
- const directory = path.dirname(file);
259
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
260
- const temp = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
261
- let descriptor;
262
- try {
263
- descriptor = fs.openSync(temp, 'wx', 0o600);
264
- fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
265
- fs.fsyncSync(descriptor);
266
- fs.closeSync(descriptor);
267
- descriptor = undefined;
268
- fs.renameSync(temp, file);
269
- if (process.platform !== 'win32') {
270
- const directoryDescriptor = fs.openSync(directory, 'r');
271
- try {
272
- fs.fsyncSync(directoryDescriptor);
273
- }
274
- finally {
275
- fs.closeSync(directoryDescriptor);
276
- }
277
- }
278
- }
279
- finally {
280
- if (descriptor !== undefined)
281
- fs.closeSync(descriptor);
282
- try {
283
- fs.unlinkSync(temp);
284
- }
285
- catch { }
286
- }
287
- }
1
+ import l from"node:crypto";import r from"node:fs";import p from"node:path";import{resolvePersonalSyncV1Path as m}from"../personal-sync/paths.js";import{getDaemonInstallationId as h}from"../personal-sync/identity.js";import{NativeSourceDeletionStore as y}from"../native-fusion/native-source-deletions.js";import{NativeSessionLifecycleRegistry as f}from"./native-session-lifecycle.js";const d=1,I=8,S=6e4,b=360*60*1e3,u=new Set(["succeeded_archived","succeeded_deleted","succeeded_already_absent","skipped_unsupported","skipped_invalid_identity","failed_permanent"]);class x{options;lifecycle;timer=null;drainPromise=null;constructor(t={}){this.options=t,this.lifecycle=t.lifecycle??new f}start(){this.timer||(this.drain(),this.timer=setInterval(()=>{this.drain()},this.options.drainIntervalMs??S),this.timer.unref?.())}stop(){this.timer&&clearInterval(this.timer),this.timer=null}enqueue(t){if((t.nativeCleanupPolicy??"none")!=="auto")return[];const s=this.read(!0),i=[];for(const e of t.sources){const o=this.jobId(t,e.agentType,e.sourceSessionKey),n=s.jobs[o];if(n){i.push(n);continue}const c={schemaVersion:d,daemonInstallationId:this.installationId(),jobId:o,sessionId:t.sessionId,deletionRevision:t.deletionRevision,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,...t.workDir?{workDir:t.workDir}:{},requestedAt:t.deletedAt,state:"pending",attempts:0};s.jobs[o]=c,i.push(c)}return i.some(e=>e.state==="pending")&&this.write(s),i}kick(){this.drain()}async drain(){return this.drainPromise?await this.drainPromise:(this.drainPromise=this.drainOnce().finally(()=>{this.drainPromise=null}),await this.drainPromise)}listJobs(){return Object.values(this.read(!0).jobs).sort((t,s)=>t.deletionRevision-s.deletionRevision||t.jobId.localeCompare(s.jobId))}async drainOnce(){for(;;){const t=this.read(!0),s=this.now(),i=Object.values(t.jobs).filter(o=>o.state==="pending"||o.state==="retry_wait"&&(!o.nextAttemptAt||Date.parse(o.nextAttemptAt)<=s.getTime())).sort((o,n)=>o.deletionRevision-n.deletionRevision||o.jobId.localeCompare(n.jobId))[0];if(!i)return;i.state="running",i.attempts+=1,delete i.nextAttemptAt,this.write(t);let e;if(!this.sourceGuard(i))e={outcome:"invalid_identity"};else try{e=await this.lifecycle.execute(i.agentType,i.sourceSessionKey,{workDir:i.workDir})}catch{e={outcome:"transient",errorCode:"lifecycle_unexpected_failure"}}this.applyResult(i.jobId,e)}}applyResult(t,s){const i=this.read(!1),e=i.jobs[t];if(!e||u.has(e.state))return;const o=this.now().toISOString();switch(s.outcome){case"archived":e.state="succeeded_archived";break;case"deleted":e.state="succeeded_deleted";break;case"already_absent":e.state="succeeded_already_absent";break;case"unsupported":e.state="skipped_unsupported";break;case"invalid_identity":e.state="skipped_invalid_identity";break;case"permanent":e.state="failed_permanent",e.lastErrorCode=s.errorCode;break;case"transient":{if(e.lastErrorCode=s.errorCode,e.attempts>=(this.options.maxAttempts??I)){e.state="failed_permanent";break}e.state="retry_wait";const n=Math.min((this.options.retryBaseMs??5e3)*2**Math.max(0,e.attempts-1),b);e.nextAttemptAt=new Date(this.now().getTime()+n).toISOString(),delete e.completedAt,this.write(i),this.log(e,s.errorCode);return}}e.completedAt=o,delete e.nextAttemptAt,this.write(i),this.log(e,e.lastErrorCode??s.outcome)}sourceGuard(t){if(this.options.sourceGuard)return this.options.sourceGuard(t);const s=new y({rootDir:this.rootDir(),daemonInstallationId:this.installationId()}).find(t.agentType,t.sourceSessionKey);return!!(s&&s.sessionId===t.sessionId&&s.deletionRevision===t.deletionRevision)}log(t,s){this.options.logger?.("[native-cleanup] job state changed",{job:t.jobId.slice(0,12),provider:t.agentType,state:t.state,result:s,attempts:t.attempts})}read(t){const s=this.filePath();if(!r.existsSync(s))return this.emptyFile();const i=JSON.parse(r.readFileSync(s,"utf8"));if(i.schemaVersion!==d||i.daemonInstallationId!==this.installationId()||!i.jobs||typeof i.jobs!="object"||Array.isArray(i.jobs))throw new Error("invalid native cleanup outbox");let e=!1;for(const[o,n]of Object.entries(i.jobs)){if(o!==n.jobId||n.schemaVersion!==d||n.daemonInstallationId!==this.installationId()||!n.sessionId||!n.agentType||!n.sourceSessionKey||!Number.isSafeInteger(n.deletionRevision)||n.deletionRevision<1||!Number.isSafeInteger(n.attempts)||n.attempts<0||!u.has(n.state)&&n.state!=="pending"&&n.state!=="running"&&n.state!=="retry_wait")throw new Error("invalid native cleanup job");t&&n.state==="running"&&(n.state="pending",e=!0)}return e&&this.write(i),i}write(t){v(this.filePath(),t)}emptyFile(){return{schemaVersion:d,daemonInstallationId:this.installationId(),jobs:{}}}jobId(t,s,i){return l.createHash("sha256").update(`${this.installationId()}\0${t.deletionRevision}\0${s}\0${i}`).digest("hex")}installationId(){return this.options.daemonInstallationId??h()}rootDir(){return this.options.rootDir??m()}filePath(){return p.join(this.rootDir(),"native-session-cleanup-outbox.json")}now(){return this.options.now?.()??new Date}}function v(a,t){const s=p.dirname(a);r.mkdirSync(s,{recursive:!0,mode:448});const i=`${a}.tmp-${process.pid}-${l.randomBytes(6).toString("hex")}`;let e;try{if(e=r.openSync(i,"wx",384),r.writeFileSync(e,`${JSON.stringify(t,null,2)}
2
+ `,"utf8"),r.fsyncSync(e),r.closeSync(e),e=void 0,r.renameSync(i,a),process.platform!=="win32"){const o=r.openSync(s,"r");try{r.fsyncSync(o)}finally{r.closeSync(o)}}}finally{e!==void 0&&r.closeSync(e);try{r.unlinkSync(i)}catch{}}}export{x as NativeSessionCleanupOutbox};
@@ -1,327 +1,6 @@
1
- // @arch docs/features/native-session-deletion-sync.md#四provider-能力基线
2
- // @test src/__tests__/native-session-lifecycle.test.ts
3
- import { createInterface } from 'node:readline';
4
- import fs from 'node:fs';
5
- import { isAvailableBuiltinAgentType } from '@shennian/wire';
6
- import { buildAgentProcessEnv } from '../agent-env.js';
7
- import { resolveBuiltinCommand, spawnAgentCommand, } from '../agents/command-spec.js';
8
- const OUTPUT_LIMIT = 64 * 1024;
9
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
- const OPENCODE_SESSION_PATTERN = /^ses_[A-Za-z0-9_-]{6,191}$/;
11
- function appendLimited(current, chunk) {
12
- if (current.length >= OUTPUT_LIMIT)
13
- return current;
14
- return `${current}${String(chunk)}`.slice(0, OUTPUT_LIMIT);
15
- }
16
- export const runNativeLifecycleCommand = async (agentType, args, timeoutMs = 10_000, cwd) => {
17
- if (!isAvailableBuiltinAgentType(agentType)) {
18
- return {
19
- code: null,
20
- stdout: '',
21
- stderr: '',
22
- timedOut: false,
23
- spawnErrorCode: 'command_unavailable',
24
- };
25
- }
26
- const spec = resolveBuiltinCommand(agentType);
27
- if (!spec) {
28
- return {
29
- code: null,
30
- stdout: '',
31
- stderr: '',
32
- timedOut: false,
33
- spawnErrorCode: 'command_unavailable',
34
- };
35
- }
36
- return await new Promise((resolve) => {
37
- let stdout = '';
38
- let stderr = '';
39
- let settled = false;
40
- let timedOut = false;
41
- const proc = spawnAgentCommand(spec, args, {
42
- stdio: ['ignore', 'pipe', 'pipe'],
43
- env: buildAgentProcessEnv({ NO_COLOR: '1' }),
44
- ...(cwd ? { cwd } : {}),
45
- });
46
- const finish = (result) => {
47
- if (settled)
48
- return;
49
- settled = true;
50
- if (timer)
51
- clearTimeout(timer);
52
- resolve({ ...result, stdout, stderr, timedOut });
53
- };
54
- proc.stdout?.on('data', (chunk) => {
55
- stdout = appendLimited(stdout, chunk);
56
- });
57
- proc.stderr?.on('data', (chunk) => {
58
- stderr = appendLimited(stderr, chunk);
59
- });
60
- proc.on('error', (error) => {
61
- finish({ code: null, spawnErrorCode: error.code ?? 'spawn_failed' });
62
- });
63
- proc.on('close', (code) => finish({ code }));
64
- const timer = setTimeout(() => {
65
- timedOut = true;
66
- proc.kill('SIGTERM');
67
- setTimeout(() => proc.kill('SIGKILL'), 1_000).unref?.();
68
- }, timeoutMs);
69
- timer.unref?.();
70
- });
71
- };
72
- function safeOutput(result) {
73
- return `${result.stdout}\n${result.stderr}`.toLowerCase();
74
- }
75
- function classifyCommandResult(result, success) {
76
- if (result.spawnErrorCode === 'command_unavailable' || result.spawnErrorCode === 'ENOENT') {
77
- return { outcome: 'unsupported' };
78
- }
79
- if (result.timedOut)
80
- return { outcome: 'transient', errorCode: 'command_timeout' };
81
- const output = safeOutput(result);
82
- if (/not found|does not exist|no session|unknown session|already (?:deleted|archived)/.test(output)) {
83
- return { outcome: 'already_absent' };
84
- }
85
- if (/busy|locked|writer|in use|current active session|active turn|temporar|try again|timed out|timeout/.test(output)) {
86
- return { outcome: 'transient', errorCode: 'native_busy' };
87
- }
88
- if (/invalid.*(?:uuid|session|id)|malformed/.test(output)) {
89
- return { outcome: 'invalid_identity' };
90
- }
91
- if (/permission denied|unauthenticated|authentication|forbidden/.test(output)) {
92
- return { outcome: 'permanent', errorCode: 'native_auth_required' };
93
- }
94
- if (result.code === 0)
95
- return { outcome: success };
96
- return { outcome: 'transient', errorCode: 'native_command_failed' };
97
- }
98
- async function commandAdvertises(runner, agentType, args, pattern) {
99
- const result = await runner(agentType, args);
100
- return !result.timedOut && result.code === 0 && pattern.test(`${result.stdout}\n${result.stderr}`);
101
- }
102
- async function codexAppServerArchiveWithSpec(spec, threadId) {
103
- return await new Promise((resolve) => {
104
- let proc = null;
105
- let settled = false;
106
- let nextId = 1;
107
- let overallTimer = null;
108
- const pending = new Map();
109
- const finish = (result) => {
110
- if (settled)
111
- return;
112
- settled = true;
113
- if (overallTimer)
114
- clearTimeout(overallTimer);
115
- for (const request of pending.values())
116
- request.reject(new Error('app_server_stopped'));
117
- pending.clear();
118
- proc?.kill('SIGTERM');
119
- resolve(result);
120
- };
121
- const request = (method, params) => {
122
- if (!proc?.stdin)
123
- return Promise.reject(new Error('app_server_unavailable'));
124
- const id = nextId++;
125
- return new Promise((requestResolve, requestReject) => {
126
- pending.set(id, { resolve: requestResolve, reject: requestReject });
127
- proc.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
128
- });
129
- };
130
- try {
131
- proc = spawnAgentCommand(spec, ['app-server', '--listen', 'stdio://'], {
132
- stdio: ['pipe', 'pipe', 'ignore'],
133
- env: buildAgentProcessEnv({ NO_COLOR: '1' }),
134
- });
135
- }
136
- catch {
137
- finish({ outcome: 'transient', errorCode: 'app_server_start_failed' });
138
- return;
139
- }
140
- const lines = createInterface({ input: proc.stdout });
141
- lines.on('line', (line) => {
142
- if (!line.trim())
143
- return;
144
- let message;
145
- try {
146
- message = JSON.parse(line);
147
- }
148
- catch {
149
- return;
150
- }
151
- if (message.id == null)
152
- return;
153
- const waiter = pending.get(message.id);
154
- if (!waiter)
155
- return;
156
- pending.delete(message.id);
157
- if (message.error) {
158
- const error = new Error(message.error.message ?? 'app_server_error');
159
- error.rpcCode = message.error.code;
160
- waiter.reject(error);
161
- }
162
- else {
163
- waiter.resolve(message.result);
164
- }
165
- });
166
- proc.on('error', () => finish({ outcome: 'transient', errorCode: 'app_server_start_failed' }));
167
- proc.on('close', () => {
168
- if (!settled)
169
- finish({ outcome: 'transient', errorCode: 'app_server_stopped' });
170
- });
171
- overallTimer = setTimeout(() => finish({ outcome: 'transient', errorCode: 'app_server_timeout' }), 12_000);
172
- overallTimer.unref?.();
173
- void (async () => {
174
- try {
175
- await request('initialize', {
176
- clientInfo: { name: 'shennian-native-cleanup', title: 'Shennian', version: '1' },
177
- capabilities: { experimentalApi: true },
178
- });
179
- await request('thread/archive', { threadId });
180
- finish({ outcome: 'archived' });
181
- }
182
- catch (error) {
183
- const rpc = error;
184
- const message = rpc.message.toLowerCase();
185
- if (/not found|does not exist|already archived|no rollout/.test(message)) {
186
- finish({ outcome: 'already_absent' });
187
- }
188
- else if (/busy|locked|writer|active turn|in use/.test(message)) {
189
- finish({ outcome: 'transient', errorCode: 'native_busy' });
190
- }
191
- else if (rpc.rpcCode === -32601 || /method not found|unsupported/.test(message)) {
192
- finish({ outcome: 'unsupported' });
193
- }
194
- else {
195
- finish({ outcome: 'transient', errorCode: 'app_server_failed' });
196
- }
197
- }
198
- })();
199
- });
200
- }
201
- export class NativeSessionLifecycleRegistry {
202
- options;
203
- capabilities = new Map();
204
- constructor(options = {}) {
205
- this.options = options;
206
- }
207
- async execute(agentType, sourceSessionKey, context = {}) {
208
- const adapter = this.adapter(agentType);
209
- const capability = await this.probe(agentType, adapter);
210
- if (capability.canArchive && adapter.archive)
211
- return await adapter.archive(sourceSessionKey);
212
- if (capability.canDelete && adapter.delete) {
213
- if (agentType === 'gemini' && !context.workDir)
214
- return { outcome: 'invalid_identity' };
215
- if ((agentType === 'gemini' || agentType === 'opencode') && context.workDir) {
216
- try {
217
- if (!fs.statSync(context.workDir).isDirectory()) {
218
- return { outcome: 'permanent', errorCode: 'workdir_unavailable' };
219
- }
220
- }
221
- catch {
222
- return { outcome: 'permanent', errorCode: 'workdir_unavailable' };
223
- }
224
- }
225
- return await this.deleteWithContext(adapter, sourceSessionKey, context);
226
- }
227
- return { outcome: 'unsupported' };
228
- }
229
- async deleteWithContext(adapter, sourceSessionKey, context) {
230
- if (!adapter.delete)
231
- return { outcome: 'unsupported' };
232
- return await adapter.delete(sourceSessionKey, context);
233
- }
234
- clearCapabilityCache() {
235
- this.capabilities.clear();
236
- }
237
- probe(agentType, adapter) {
238
- const existing = this.capabilities.get(agentType);
239
- if (existing)
240
- return existing;
241
- const pending = adapter.probe().catch(() => ({ canArchive: false, canDelete: false }));
242
- this.capabilities.set(agentType, pending);
243
- return pending;
244
- }
245
- adapter(agentType) {
246
- const runner = this.options.runner ?? runNativeLifecycleCommand;
247
- if (agentType === 'codex') {
248
- return {
249
- probe: async () => ({
250
- canArchive: await commandAdvertises(runner, 'codex', ['--help'], /\barchive\b/),
251
- canDelete: false,
252
- }),
253
- archive: async (sourceSessionKey) => {
254
- if (!UUID_PATTERN.test(sourceSessionKey))
255
- return { outcome: 'invalid_identity' };
256
- const custom = this.options.codexArchive;
257
- const spec = resolveBuiltinCommand('codex');
258
- let appServerResult = { outcome: 'unsupported' };
259
- if (custom)
260
- appServerResult = await custom(sourceSessionKey);
261
- else if (spec)
262
- appServerResult = await codexAppServerArchiveWithSpec(spec, sourceSessionKey);
263
- if (appServerResult.outcome === 'archived' ||
264
- appServerResult.outcome === 'already_absent' ||
265
- appServerResult.outcome === 'invalid_identity' ||
266
- (appServerResult.outcome === 'transient' && appServerResult.errorCode === 'native_busy'))
267
- return appServerResult;
268
- return classifyCommandResult(await runner('codex', ['archive', sourceSessionKey], 15_000), 'archived');
269
- },
270
- };
271
- }
272
- if (agentType === 'gemini') {
273
- return {
274
- probe: async () => ({
275
- canArchive: false,
276
- canDelete: await commandAdvertises(runner, 'gemini', ['--help'], /--delete-session\b/),
277
- }),
278
- delete: async (sourceSessionKey, context) => {
279
- if (!UUID_PATTERN.test(sourceSessionKey))
280
- return { outcome: 'invalid_identity' };
281
- const result = await runner('gemini', ['--delete-session', sourceSessionKey], 15_000, context?.workDir);
282
- if (/invalid session identifier/i.test(`${result.stdout}\n${result.stderr}`)) {
283
- return { outcome: 'already_absent' };
284
- }
285
- return classifyCommandResult(result, 'deleted');
286
- },
287
- };
288
- }
289
- if (agentType === 'opencode') {
290
- return {
291
- probe: async () => ({
292
- canArchive: false,
293
- canDelete: await commandAdvertises(runner, 'opencode', ['session', 'delete', '--help'], /session delete\s+<sessionID>/i),
294
- }),
295
- delete: async (sourceSessionKey, context) => {
296
- if (!OPENCODE_SESSION_PATTERN.test(sourceSessionKey)) {
297
- return { outcome: 'invalid_identity' };
298
- }
299
- return classifyCommandResult(await runner('opencode', ['session', 'delete', sourceSessionKey], 15_000, context?.workDir), 'deleted');
300
- },
301
- };
302
- }
303
- if (agentType === 'cursor') {
304
- return {
305
- probe: async () => {
306
- const result = await runner('cursor', ['--help']);
307
- const output = `${result.stdout}\n${result.stderr}`;
308
- return {
309
- canArchive: result.code === 0 && /\barchive-chat\b/.test(output),
310
- canDelete: result.code === 0 && /\bdelete-chat\b/.test(output),
311
- };
312
- },
313
- archive: async (sourceSessionKey) => {
314
- if (!UUID_PATTERN.test(sourceSessionKey))
315
- return { outcome: 'invalid_identity' };
316
- return classifyCommandResult(await runner('cursor', ['archive-chat', sourceSessionKey], 15_000), 'archived');
317
- },
318
- delete: async (sourceSessionKey) => {
319
- if (!UUID_PATTERN.test(sourceSessionKey))
320
- return { outcome: 'invalid_identity' };
321
- return classifyCommandResult(await runner('cursor', ['delete-chat', sourceSessionKey], 15_000), 'deleted');
322
- },
323
- };
324
- }
325
- return { probe: async () => ({ canArchive: false, canDelete: false }) };
326
- }
327
- }
1
+ import{createInterface as E}from"node:readline";import D from"node:fs";import{isAvailableBuiltinAgentType as g}from"@shennian/wire";import{buildAgentProcessEnv as w}from"../agent-env.js";import{resolveBuiltinCommand as b,spawnAgentCommand as y}from"../agents/command-spec.js";const C=64*1024,h=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,k=/^ses_[A-Za-z0-9_-]{6,191}$/;function A(n,r){return n.length>=C?n:`${n}${String(r)}`.slice(0,C)}const x=async(n,r,i=1e4,e)=>{if(!g(n))return{code:null,stdout:"",stderr:"",timedOut:!1,spawnErrorCode:"command_unavailable"};const t=b(n);return t?await new Promise(c=>{let s="",l="",d=!1,m=!1;const p=y(t,r,{stdio:["ignore","pipe","pipe"],env:w({NO_COLOR:"1"}),...e?{cwd:e}:{}}),u=o=>{d||(d=!0,a&&clearTimeout(a),c({...o,stdout:s,stderr:l,timedOut:m}))};p.stdout?.on("data",o=>{s=A(s,o)}),p.stderr?.on("data",o=>{l=A(l,o)}),p.on("error",o=>{u({code:null,spawnErrorCode:o.code??"spawn_failed"})}),p.on("close",o=>u({code:o}));const a=setTimeout(()=>{m=!0,p.kill("SIGTERM"),setTimeout(()=>p.kill("SIGKILL"),1e3).unref?.()},i);a.unref?.()}):{code:null,stdout:"",stderr:"",timedOut:!1,spawnErrorCode:"command_unavailable"}};function $(n){return`${n.stdout}
2
+ ${n.stderr}`.toLowerCase()}function f(n,r){if(n.spawnErrorCode==="command_unavailable"||n.spawnErrorCode==="ENOENT")return{outcome:"unsupported"};if(n.timedOut)return{outcome:"transient",errorCode:"command_timeout"};const i=$(n);return/not found|does not exist|no session|unknown session|already (?:deleted|archived)/.test(i)?{outcome:"already_absent"}:/busy|locked|writer|in use|current active session|active turn|temporar|try again|timed out|timeout/.test(i)?{outcome:"transient",errorCode:"native_busy"}:/invalid.*(?:uuid|session|id)|malformed/.test(i)?{outcome:"invalid_identity"}:/permission denied|unauthenticated|authentication|forbidden/.test(i)?{outcome:"permanent",errorCode:"native_auth_required"}:n.code===0?{outcome:r}:{outcome:"transient",errorCode:"native_command_failed"}}async function _(n,r,i,e){const t=await n(r,i);return!t.timedOut&&t.code===0&&e.test(`${t.stdout}
3
+ ${t.stderr}`)}async function I(n,r){return await new Promise(i=>{let e=null,t=!1,c=1,s=null;const l=new Map,d=u=>{if(!t){t=!0,s&&clearTimeout(s);for(const a of l.values())a.reject(new Error("app_server_stopped"));l.clear(),e?.kill("SIGTERM"),i(u)}},m=(u,a)=>{if(!e?.stdin)return Promise.reject(new Error("app_server_unavailable"));const o=c++;return new Promise((v,O)=>{l.set(o,{resolve:v,reject:O}),e.stdin.write(`${JSON.stringify({id:o,method:u,params:a})}
4
+ `)})};try{e=y(n,["app-server","--listen","stdio://"],{stdio:["pipe","pipe","ignore"],env:w({NO_COLOR:"1"})})}catch{d({outcome:"transient",errorCode:"app_server_start_failed"});return}E({input:e.stdout}).on("line",u=>{if(!u.trim())return;let a;try{a=JSON.parse(u)}catch{return}if(a.id==null)return;const o=l.get(a.id);if(o)if(l.delete(a.id),a.error){const v=new Error(a.error.message??"app_server_error");v.rpcCode=a.error.code,o.reject(v)}else o.resolve(a.result)}),e.on("error",()=>d({outcome:"transient",errorCode:"app_server_start_failed"})),e.on("close",()=>{t||d({outcome:"transient",errorCode:"app_server_stopped"})}),s=setTimeout(()=>d({outcome:"transient",errorCode:"app_server_timeout"}),12e3),s.unref?.(),(async()=>{try{await m("initialize",{clientInfo:{name:"shennian-native-cleanup",title:"Shennian",version:"1"},capabilities:{experimentalApi:!0}}),await m("thread/archive",{threadId:r}),d({outcome:"archived"})}catch(u){const a=u,o=a.message.toLowerCase();/not found|does not exist|already archived|no rollout/.test(o)?d({outcome:"already_absent"}):/busy|locked|writer|active turn|in use/.test(o)?d({outcome:"transient",errorCode:"native_busy"}):a.rpcCode===-32601||/method not found|unsupported/.test(o)?d({outcome:"unsupported"}):d({outcome:"transient",errorCode:"app_server_failed"})}})()})}class S{options;capabilities=new Map;constructor(r={}){this.options=r}async execute(r,i,e={}){const t=this.adapter(r),c=await this.probe(r,t);if(c.canArchive&&t.archive)return await t.archive(i);if(c.canDelete&&t.delete){if(r==="gemini"&&!e.workDir)return{outcome:"invalid_identity"};if((r==="gemini"||r==="opencode")&&e.workDir)try{if(!D.statSync(e.workDir).isDirectory())return{outcome:"permanent",errorCode:"workdir_unavailable"}}catch{return{outcome:"permanent",errorCode:"workdir_unavailable"}}return await this.deleteWithContext(t,i,e)}return{outcome:"unsupported"}}async deleteWithContext(r,i,e){return r.delete?await r.delete(i,e):{outcome:"unsupported"}}clearCapabilityCache(){this.capabilities.clear()}probe(r,i){const e=this.capabilities.get(r);if(e)return e;const t=i.probe().catch(()=>({canArchive:!1,canDelete:!1}));return this.capabilities.set(r,t),t}adapter(r){const i=this.options.runner??x;return r==="codex"?{probe:async()=>({canArchive:await _(i,"codex",["--help"],/\barchive\b/),canDelete:!1}),archive:async e=>{if(!h.test(e))return{outcome:"invalid_identity"};const t=this.options.codexArchive,c=b("codex");let s={outcome:"unsupported"};return t?s=await t(e):c&&(s=await I(c,e)),s.outcome==="archived"||s.outcome==="already_absent"||s.outcome==="invalid_identity"||s.outcome==="transient"&&s.errorCode==="native_busy"?s:f(await i("codex",["archive",e],15e3),"archived")}}:r==="gemini"?{probe:async()=>({canArchive:!1,canDelete:await _(i,"gemini",["--help"],/--delete-session\b/)}),delete:async(e,t)=>{if(!h.test(e))return{outcome:"invalid_identity"};const c=await i("gemini",["--delete-session",e],15e3,t?.workDir);return/invalid session identifier/i.test(`${c.stdout}
5
+ ${c.stderr}`)?{outcome:"already_absent"}:f(c,"deleted")}}:r==="opencode"?{probe:async()=>({canArchive:!1,canDelete:await _(i,"opencode",["session","delete","--help"],/session delete\s+<sessionID>/i)}),delete:async(e,t)=>k.test(e)?f(await i("opencode",["session","delete",e],15e3,t?.workDir),"deleted"):{outcome:"invalid_identity"}}:r==="cursor"?{probe:async()=>{const e=await i("cursor",["--help"]),t=`${e.stdout}
6
+ ${e.stderr}`;return{canArchive:e.code===0&&/\barchive-chat\b/.test(t),canDelete:e.code===0&&/\bdelete-chat\b/.test(t)}},archive:async e=>h.test(e)?f(await i("cursor",["archive-chat",e],15e3),"archived"):{outcome:"invalid_identity"},delete:async e=>h.test(e)?f(await i("cursor",["delete-chat",e],15e3),"deleted"):{outcome:"invalid_identity"}}:{probe:async()=>({canArchive:!1,canDelete:!1})}}}export{S as NativeSessionLifecycleRegistry,x as runNativeLifecycleCommand};
@@ -24,6 +24,7 @@ export declare class TerminalQueuedChatDispatchError extends Error {
24
24
  export declare class ChatQueueManager {
25
25
  private opts;
26
26
  private draining;
27
+ private inFlightMessageIds;
27
28
  private prepareManagedRoomTurn;
28
29
  constructor(opts: {
29
30
  getRuntime: () => SessionManagerRuntime;
@@ -42,6 +43,13 @@ export declare class ChatQueueManager {
42
43
  handleDelete(req: ReqFrame): Promise<void>;
43
44
  noteTerminal(sessionId: string): void;
44
45
  purgeSession(sessionId: string): void;
46
+ /**
47
+ * Drop Room messages that have not crossed the Provider boundary yet. A
48
+ * paused Binding must not replay messages collected while it was paused when
49
+ * the owner resumes it later. The currently dispatched batch is excluded: it
50
+ * already crossed that boundary and is no longer cancellable as queue work.
51
+ */
52
+ discardPendingManagedRoomMessages(sessionId: string): number;
45
53
  private drainIdleQueues;
46
54
  private drainNext;
47
55
  private removeBatch;