@yu_robotics/remote-cli 1.6.158 → 1.6.165
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/README.md +10 -3
- package/dist/client/MessageHandler.d.ts +4 -5
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +102 -9
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +15 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/session.d.ts +17 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +37 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/delegation/GitCheckpoint.d.ts +1 -0
- package/dist/delegation/GitCheckpoint.d.ts.map +1 -1
- package/dist/delegation/GitCheckpoint.js +15 -4
- package/dist/delegation/GitCheckpoint.js.map +1 -1
- package/dist/executor/IExecutor.d.ts +21 -0
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.d.ts +26 -1
- package/dist/executor/PiExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.js +256 -15
- package/dist/executor/PiExecutor.js.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/maintenance/release-notes.json +1 -1
- package/dist/session/SessionCatalog.d.ts +76 -0
- package/dist/session/SessionCatalog.d.ts.map +1 -0
- package/dist/session/SessionCatalog.js +197 -0
- package/dist/session/SessionCatalog.js.map +1 -0
- package/dist/session/SessionControl.d.ts +56 -0
- package/dist/session/SessionControl.d.ts.map +1 -0
- package/dist/session/SessionControl.js +262 -0
- package/dist/session/SessionControl.js.map +1 -0
- package/dist/session/SessionLocalInfo.d.ts +48 -0
- package/dist/session/SessionLocalInfo.d.ts.map +1 -0
- package/dist/session/SessionLocalInfo.js +145 -0
- package/dist/session/SessionLocalInfo.js.map +1 -0
- package/dist/session/SessionMenu.d.ts +102 -0
- package/dist/session/SessionMenu.d.ts.map +1 -0
- package/dist/session/SessionMenu.js +144 -0
- package/dist/session/SessionMenu.js.map +1 -0
- package/dist/session/SessionResumeService.d.ts +38 -0
- package/dist/session/SessionResumeService.d.ts.map +1 -0
- package/dist/session/SessionResumeService.js +147 -0
- package/dist/session/SessionResumeService.js.map +1 -0
- package/dist/session/adapters/ClaudeSessionCatalog.d.ts +10 -0
- package/dist/session/adapters/ClaudeSessionCatalog.d.ts.map +1 -0
- package/dist/session/adapters/ClaudeSessionCatalog.js +156 -0
- package/dist/session/adapters/ClaudeSessionCatalog.js.map +1 -0
- package/dist/session/adapters/CodexSessionCatalog.d.ts +9 -0
- package/dist/session/adapters/CodexSessionCatalog.d.ts.map +1 -0
- package/dist/session/adapters/CodexSessionCatalog.js +214 -0
- package/dist/session/adapters/CodexSessionCatalog.js.map +1 -0
- package/dist/session/adapters/PiSessionCatalog.d.ts +52 -0
- package/dist/session/adapters/PiSessionCatalog.d.ts.map +1 -0
- package/dist/session/adapters/PiSessionCatalog.js +252 -0
- package/dist/session/adapters/PiSessionCatalog.js.map +1 -0
- package/dist/session/adapters/index.d.ts +13 -0
- package/dist/session/adapters/index.d.ts.map +1 -0
- package/dist/session/adapters/index.js +17 -0
- package/dist/session/adapters/index.js.map +1 -0
- package/dist/types/config.d.ts +11 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,12 +37,23 @@ exports.PiExecutor = void 0;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const os = __importStar(require("os"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
40
41
|
const ExecutionMetadata_1 = require("./ExecutionMetadata");
|
|
41
42
|
const PiClient_1 = require("./pi/PiClient");
|
|
42
43
|
const contract_1 = require("../delegation/contract");
|
|
43
44
|
const WorkingDirectory_1 = require("../utils/WorkingDirectory");
|
|
44
45
|
const Activity_1 = require("./Activity");
|
|
45
46
|
const PiTypes_1 = require("./pi/PiTypes");
|
|
47
|
+
const MISSING_ADOPTED_ERROR = 'The connected Pi session is no longer available on this computer. Send /session detach to start a fresh conversation, or /resume to connect another session.';
|
|
48
|
+
function sameFilePath(left, right) {
|
|
49
|
+
const canonical = (value) => { try {
|
|
50
|
+
return fs.realpathSync.native(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return path.resolve(value);
|
|
54
|
+
} };
|
|
55
|
+
return canonical(left) === canonical(right);
|
|
56
|
+
}
|
|
46
57
|
const DEFAULT_TURN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
47
58
|
const COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
48
59
|
/** Grace period to distinguish immediate slash/extension commands from a real agent turn. */
|
|
@@ -176,6 +187,15 @@ class PiExecutor {
|
|
|
176
187
|
unsubscribe = null;
|
|
177
188
|
sessionId = null;
|
|
178
189
|
sessionFile = null;
|
|
190
|
+
/** Fresh foreground identity, distinct from the stable remote thread. */
|
|
191
|
+
freshSessionId = (0, crypto_1.randomUUID)();
|
|
192
|
+
/** Adopted native history is user-owned: never deleted, never renamed. */
|
|
193
|
+
adopted = false;
|
|
194
|
+
nativeSessionMutation = false;
|
|
195
|
+
nativeSessionMutationCancelled = false;
|
|
196
|
+
nativeSessionMutationDone = null;
|
|
197
|
+
nativeSessionCleanup = 0;
|
|
198
|
+
preparingTurn = null;
|
|
179
199
|
activeTurn = null;
|
|
180
200
|
activityTracker = new Activity_1.ActivityTracker();
|
|
181
201
|
pendingUi = null;
|
|
@@ -185,7 +205,7 @@ class PiExecutor {
|
|
|
185
205
|
async configureDelegation(connection) {
|
|
186
206
|
if ((0, contract_1.sameConnection)(this.delegation, connection))
|
|
187
207
|
return;
|
|
188
|
-
if (this.
|
|
208
|
+
if (this.isBusy())
|
|
189
209
|
throw new Error('Cannot change delegation while Pi is busy');
|
|
190
210
|
await this.recycleClient();
|
|
191
211
|
this.delegation = connection;
|
|
@@ -227,24 +247,42 @@ class PiExecutor {
|
|
|
227
247
|
async execute(prompt, options = {}) {
|
|
228
248
|
if (this.destroyed)
|
|
229
249
|
throw new Error('Executor has been destroyed');
|
|
230
|
-
if (this.
|
|
250
|
+
if (this.isBusy()) {
|
|
231
251
|
return { success: false, error: 'Pi thread is already running a command' };
|
|
232
252
|
}
|
|
233
253
|
const trimmed = prompt.trim();
|
|
234
254
|
if (trimmed === '/skills' || trimmed === '/skill') {
|
|
235
255
|
return this.listSkills();
|
|
236
256
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (validationError)
|
|
240
|
-
return { success: false, error: validationError };
|
|
241
|
-
}
|
|
257
|
+
const preparation = { cancelled: false };
|
|
258
|
+
this.preparingTurn = preparation;
|
|
242
259
|
try {
|
|
260
|
+
// Each native Pi process caches a branch. Reload adopted history before
|
|
261
|
+
// every remote turn; the user must keep other writers idle.
|
|
262
|
+
if (this.adopted)
|
|
263
|
+
await this.recycleClient();
|
|
264
|
+
if (preparation.cancelled || this.destroyed)
|
|
265
|
+
return { success: false, error: 'Pi command cancelled before startup' };
|
|
243
266
|
await this.ensureClient();
|
|
267
|
+
if (preparation.cancelled || this.destroyed) {
|
|
268
|
+
await this.recycleClient();
|
|
269
|
+
return { success: false, error: 'Pi command cancelled before dispatch' };
|
|
270
|
+
}
|
|
271
|
+
if (trimmed.startsWith('/')) {
|
|
272
|
+
const validationError = await this.validateSlashCommand(trimmed);
|
|
273
|
+
if (validationError)
|
|
274
|
+
return { success: false, error: validationError };
|
|
275
|
+
}
|
|
276
|
+
if (preparation.cancelled || this.destroyed)
|
|
277
|
+
return { success: false, error: 'Pi command cancelled before dispatch' };
|
|
244
278
|
}
|
|
245
279
|
catch (error) {
|
|
246
280
|
return { success: false, error: this.friendlyError(error) };
|
|
247
281
|
}
|
|
282
|
+
finally {
|
|
283
|
+
if (this.preparingTurn === preparation)
|
|
284
|
+
this.preparingTurn = null;
|
|
285
|
+
}
|
|
248
286
|
return new Promise((resolve) => {
|
|
249
287
|
const active = {
|
|
250
288
|
options,
|
|
@@ -325,7 +363,7 @@ class PiExecutor {
|
|
|
325
363
|
return this.currentWorkingDirectory;
|
|
326
364
|
}
|
|
327
365
|
isBusy() {
|
|
328
|
-
return this.activeTurn !== null;
|
|
366
|
+
return this.activeTurn !== null || this.preparingTurn !== null || this.nativeSessionMutation || this.nativeSessionCleanup > 0;
|
|
329
367
|
}
|
|
330
368
|
async setWorkingDirectory(targetPath) {
|
|
331
369
|
const resolved = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
|
|
@@ -343,6 +381,10 @@ class PiExecutor {
|
|
|
343
381
|
resetContext() {
|
|
344
382
|
if (this.destroyed)
|
|
345
383
|
return;
|
|
384
|
+
if (this.nativeSessionMutation)
|
|
385
|
+
throw new Error('Cannot clear Pi context while a session connection is changing');
|
|
386
|
+
if (this.preparingTurn)
|
|
387
|
+
this.preparingTurn.cancelled = true;
|
|
346
388
|
this.cancelPendingUi(true);
|
|
347
389
|
if (this.activeTurn) {
|
|
348
390
|
this.completeActive({
|
|
@@ -355,6 +397,17 @@ class PiExecutor {
|
|
|
355
397
|
this.replaceClient();
|
|
356
398
|
}
|
|
357
399
|
async abort() {
|
|
400
|
+
const mutation = this.nativeSessionMutationDone;
|
|
401
|
+
if (mutation) {
|
|
402
|
+
this.nativeSessionMutationCancelled = true;
|
|
403
|
+
await this.stopNativeSessionMutation(mutation);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (this.preparingTurn) {
|
|
407
|
+
this.preparingTurn.cancelled = true;
|
|
408
|
+
await this.recycleClient();
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
358
411
|
if (!this.client.isRunning())
|
|
359
412
|
return false;
|
|
360
413
|
const active = this.activeTurn;
|
|
@@ -387,13 +440,36 @@ class PiExecutor {
|
|
|
387
440
|
}
|
|
388
441
|
async destroy() {
|
|
389
442
|
this.destroyed = true;
|
|
443
|
+
this.nativeSessionMutationCancelled = true;
|
|
444
|
+
if (this.preparingTurn)
|
|
445
|
+
this.preparingTurn.cancelled = true;
|
|
390
446
|
this.cancelPendingUi(true);
|
|
391
447
|
if (this.activeTurn) {
|
|
392
448
|
this.completeActive({ success: false, error: 'Executor destroyed' });
|
|
393
449
|
}
|
|
394
450
|
this.unsubscribe?.();
|
|
395
451
|
this.unsubscribe = null;
|
|
396
|
-
|
|
452
|
+
const mutation = this.nativeSessionMutationDone;
|
|
453
|
+
if (mutation)
|
|
454
|
+
await this.stopNativeSessionMutation(mutation);
|
|
455
|
+
else
|
|
456
|
+
await this.recycleClient();
|
|
457
|
+
}
|
|
458
|
+
/** Abort native context changes without RPC requests that could restart a stopped client. */
|
|
459
|
+
async stopNativeSessionMutation(mutation) {
|
|
460
|
+
this.nativeSessionCleanup += 1;
|
|
461
|
+
try {
|
|
462
|
+
// Interrupt startup first, then await rollback before the final exit check.
|
|
463
|
+
try {
|
|
464
|
+
await this.recycleClient();
|
|
465
|
+
}
|
|
466
|
+
catch { /* confirmed cleanup is required below */ }
|
|
467
|
+
await mutation;
|
|
468
|
+
await this.recycleClient();
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
this.nativeSessionCleanup -= 1;
|
|
472
|
+
}
|
|
397
473
|
}
|
|
398
474
|
/**
|
|
399
475
|
* `recycleClient` waits for the Pi transport's stop operation. Check every
|
|
@@ -448,7 +524,7 @@ class PiExecutor {
|
|
|
448
524
|
return this.compactWhenFull(onStream);
|
|
449
525
|
}
|
|
450
526
|
async compactWhenFull(onStream) {
|
|
451
|
-
if (this.
|
|
527
|
+
if (this.isBusy()) {
|
|
452
528
|
return { success: false, error: 'Cannot compact while a Pi command is running' };
|
|
453
529
|
}
|
|
454
530
|
try {
|
|
@@ -655,9 +731,11 @@ class PiExecutor {
|
|
|
655
731
|
async deleteThreadData(threadId) {
|
|
656
732
|
const pointerPath = path.join(path.dirname(this.sessionFilePath), `${threadId}.json`);
|
|
657
733
|
let sessionFile = this.sessionFile;
|
|
734
|
+
let adopted = threadId === this.threadId && this.adopted;
|
|
658
735
|
try {
|
|
659
736
|
const stored = JSON.parse(fs.readFileSync(pointerPath, 'utf8'));
|
|
660
737
|
sessionFile = stored.sessionFile ?? sessionFile;
|
|
738
|
+
adopted = adopted || stored.adopted === true;
|
|
661
739
|
}
|
|
662
740
|
catch {
|
|
663
741
|
// Pointer may already be gone.
|
|
@@ -666,7 +744,7 @@ class PiExecutor {
|
|
|
666
744
|
fs.unlinkSync(pointerPath);
|
|
667
745
|
}
|
|
668
746
|
catch { /* ignore */ }
|
|
669
|
-
if (sessionFile) {
|
|
747
|
+
if (sessionFile && !adopted && !this.isForegroundSession(threadId)) {
|
|
670
748
|
try {
|
|
671
749
|
fs.unlinkSync(sessionFile);
|
|
672
750
|
}
|
|
@@ -675,6 +753,7 @@ class PiExecutor {
|
|
|
675
753
|
if (threadId === this.threadId) {
|
|
676
754
|
this.sessionId = null;
|
|
677
755
|
this.sessionFile = null;
|
|
756
|
+
this.adopted = false;
|
|
678
757
|
}
|
|
679
758
|
}
|
|
680
759
|
async listSkills() {
|
|
@@ -715,6 +794,8 @@ class PiExecutor {
|
|
|
715
794
|
async ensureClient() {
|
|
716
795
|
if (this.destroyed)
|
|
717
796
|
throw new Error('Executor has been destroyed');
|
|
797
|
+
if (this.adopted && (!this.sessionFile || !fs.existsSync(this.sessionFile)))
|
|
798
|
+
throw new Error(MISSING_ADOPTED_ERROR);
|
|
718
799
|
(0, WorkingDirectory_1.assertWorkingDirectoryExists)(this.currentWorkingDirectory);
|
|
719
800
|
if (this.retiredClients.size > 0) {
|
|
720
801
|
await Promise.all([...this.retiredClients].map(client => this.stopRetiredClient(client)));
|
|
@@ -727,9 +808,150 @@ class PiExecutor {
|
|
|
727
808
|
this.reportedEffort = undefined;
|
|
728
809
|
this.client.updateLaunch?.(this.buildLaunch());
|
|
729
810
|
await this.client.start();
|
|
730
|
-
|
|
811
|
+
if (!this.adopted) {
|
|
812
|
+
await this.refreshSessionPointer();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
// Adopted sessions never follow a silently substituted identity after a restart.
|
|
816
|
+
const mismatch = await this.checkAdoptedIdentity({ sessionId: this.sessionId, sessionFile: this.sessionFile });
|
|
817
|
+
if (mismatch) {
|
|
818
|
+
await this.client.stop().catch(() => undefined);
|
|
819
|
+
throw new Error(`${mismatch} ${MISSING_ADOPTED_ERROR}`);
|
|
820
|
+
}
|
|
731
821
|
}
|
|
732
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Connect this thread to an existing native Pi session file. Verification uses
|
|
825
|
+
* RPC `get_state` only: no prompt is sent. Pi may append ordinary metadata
|
|
826
|
+
* entries (for example a thinking-level record) but keeps prior history intact.
|
|
827
|
+
*/
|
|
828
|
+
async adoptNativeSession(target) {
|
|
829
|
+
const fail = (error) => ({ success: false, error });
|
|
830
|
+
if (this.destroyed)
|
|
831
|
+
return fail('Executor has been destroyed.');
|
|
832
|
+
if (this.isBusy())
|
|
833
|
+
return fail('Wait for the running Pi task to finish before connecting a session.');
|
|
834
|
+
if (!fs.existsSync(target.sessionFile))
|
|
835
|
+
return fail('The selected Pi session is no longer available. No session was changed.');
|
|
836
|
+
return this.mutateNativeSession(() => this.adoptIdleNativeSession(target));
|
|
837
|
+
}
|
|
838
|
+
async mutateNativeSession(operation) {
|
|
839
|
+
this.nativeSessionMutation = true;
|
|
840
|
+
this.nativeSessionMutationCancelled = false;
|
|
841
|
+
let finish;
|
|
842
|
+
this.nativeSessionMutationDone = new Promise(resolve => { finish = resolve; });
|
|
843
|
+
try {
|
|
844
|
+
return await operation();
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
this.nativeSessionMutation = false;
|
|
848
|
+
this.nativeSessionMutationDone = null;
|
|
849
|
+
finish();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async adoptIdleNativeSession(target) {
|
|
853
|
+
const fail = (error) => ({ success: false, error });
|
|
854
|
+
const previous = { sessionId: this.sessionId, sessionFile: this.sessionFile, adopted: this.adopted,
|
|
855
|
+
reportedModel: this.reportedModel, reportedEffort: this.reportedEffort };
|
|
856
|
+
try {
|
|
857
|
+
await this.recycleClient();
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
return fail(`Could not confirm that the current Pi process stopped (${this.friendlyError(error)}). No session was changed.`);
|
|
861
|
+
}
|
|
862
|
+
if (this.destroyed || this.nativeSessionMutationCancelled)
|
|
863
|
+
return fail('Pi session connection was cancelled. No session was connected.');
|
|
864
|
+
this.sessionId = target.sessionId;
|
|
865
|
+
this.sessionFile = target.sessionFile;
|
|
866
|
+
this.adopted = true;
|
|
867
|
+
let error;
|
|
868
|
+
try {
|
|
869
|
+
(0, WorkingDirectory_1.assertWorkingDirectoryExists)(this.currentWorkingDirectory);
|
|
870
|
+
this.reportedModel = undefined;
|
|
871
|
+
this.reportedEffort = undefined;
|
|
872
|
+
this.client.updateLaunch?.(this.buildLaunch());
|
|
873
|
+
await this.client.start();
|
|
874
|
+
// PiClient.request() can start a process; never call it after cancellation.
|
|
875
|
+
if (!this.destroyed && !this.nativeSessionMutationCancelled)
|
|
876
|
+
error = await this.checkAdoptedIdentity(target);
|
|
877
|
+
if (this.destroyed || this.nativeSessionMutationCancelled)
|
|
878
|
+
error = 'Pi session connection was cancelled.';
|
|
879
|
+
if (!error && !this.savePointer())
|
|
880
|
+
error = 'Could not persist the Pi session connection.';
|
|
881
|
+
}
|
|
882
|
+
catch (startError) {
|
|
883
|
+
error = this.friendlyError(startError);
|
|
884
|
+
}
|
|
885
|
+
if (!error) {
|
|
886
|
+
return { success: true };
|
|
887
|
+
}
|
|
888
|
+
let stopped = true;
|
|
889
|
+
try {
|
|
890
|
+
await this.client.stop();
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
stopped = false;
|
|
894
|
+
}
|
|
895
|
+
Object.assign(this, previous);
|
|
896
|
+
// An unconfirmed stop is retired and retried before the previous session starts again.
|
|
897
|
+
if (stopped)
|
|
898
|
+
this.client.updateLaunch?.(this.buildLaunch());
|
|
899
|
+
else
|
|
900
|
+
this.replaceClient();
|
|
901
|
+
return { success: false, error: `${error} The previous conversation is kept.${stopped ? '' : ' Selected Pi process exit could not be confirmed; the session remains reserved until recovery.'}`,
|
|
902
|
+
...(!stopped ? { recoveryRequired: true } : {}) };
|
|
903
|
+
}
|
|
904
|
+
/** Forget the adopted session without deleting it; the next turn starts a fresh conversation. */
|
|
905
|
+
async detachNativeSession() {
|
|
906
|
+
if (this.destroyed)
|
|
907
|
+
return { success: false, error: 'Executor has been destroyed.' };
|
|
908
|
+
if (!this.adopted)
|
|
909
|
+
return { success: false, error: 'This thread is not connected to a native Pi session.' };
|
|
910
|
+
if (this.isBusy())
|
|
911
|
+
return { success: false, error: 'Wait for the running Pi task to finish (or send /abort) before detaching the session.' };
|
|
912
|
+
return this.mutateNativeSession(() => this.detachIdleNativeSession());
|
|
913
|
+
}
|
|
914
|
+
async detachIdleNativeSession() {
|
|
915
|
+
const previous = { sessionId: this.sessionId, sessionFile: this.sessionFile, adopted: this.adopted };
|
|
916
|
+
try {
|
|
917
|
+
await this.recycleClient();
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
return { success: false, error: `Could not confirm that the Pi process stopped (${this.friendlyError(error)}). The session is still connected; try again.` };
|
|
921
|
+
}
|
|
922
|
+
if (this.destroyed || this.nativeSessionMutationCancelled)
|
|
923
|
+
return { success: false, error: 'Pi session detach was cancelled. The session is still connected.' };
|
|
924
|
+
this.adopted = false;
|
|
925
|
+
this.sessionFile = null;
|
|
926
|
+
this.sessionId = (0, crypto_1.randomUUID)();
|
|
927
|
+
if (!this.savePointer()) {
|
|
928
|
+
Object.assign(this, previous);
|
|
929
|
+
this.client.updateLaunch?.(this.buildLaunch());
|
|
930
|
+
return { success: false, error: 'Could not persist the Pi session detach. The session is still connected; try again.' };
|
|
931
|
+
}
|
|
932
|
+
this.client.updateLaunch?.(this.buildLaunch());
|
|
933
|
+
return { success: true };
|
|
934
|
+
}
|
|
935
|
+
/** Returns an error description when Pi did not load exactly the expected session. */
|
|
936
|
+
async checkAdoptedIdentity(expected) {
|
|
937
|
+
const response = await this.client.request({ type: 'get_state' });
|
|
938
|
+
const data = response.success ? response.data : undefined;
|
|
939
|
+
if (!data)
|
|
940
|
+
return 'Pi did not report its session state.';
|
|
941
|
+
if (data.sessionId !== expected.sessionId || typeof data.sessionFile !== 'string'
|
|
942
|
+
|| !sameFilePath(data.sessionFile, expected.sessionFile)) {
|
|
943
|
+
return 'Pi opened a different session than the one selected.';
|
|
944
|
+
}
|
|
945
|
+
if (data.isStreaming === true)
|
|
946
|
+
return 'Pi reported that the session is already running a turn.';
|
|
947
|
+
const model = data.model;
|
|
948
|
+
if (typeof model?.id === 'string' && model.id.trim()) {
|
|
949
|
+
this.reportedModel = typeof model.provider === 'string' && model.provider ? `${model.provider}/${model.id}` : model.id;
|
|
950
|
+
}
|
|
951
|
+
if (typeof data.thinkingLevel === 'string' && data.thinkingLevel.trim())
|
|
952
|
+
this.reportedEffort = data.thinkingLevel;
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
733
955
|
async recycleClient() {
|
|
734
956
|
await Promise.all([
|
|
735
957
|
this.client.stop(),
|
|
@@ -754,10 +976,15 @@ class PiExecutor {
|
|
|
754
976
|
console.warn('[PiExecutor] Failed to stop previous Pi RPC process', error);
|
|
755
977
|
});
|
|
756
978
|
}
|
|
979
|
+
isForegroundSession(threadId = this.threadId) {
|
|
980
|
+
return !this.delegationWorker && !/^(?:remote-cli-)?(?:delegate-lane-|settings-meta-)/.test(threadId ?? '');
|
|
981
|
+
}
|
|
757
982
|
forgetSession() {
|
|
758
983
|
const sessionFile = this.sessionFile;
|
|
984
|
+
const preserveHistory = this.adopted || this.isForegroundSession();
|
|
759
985
|
this.clearPointer();
|
|
760
|
-
|
|
986
|
+
// Reset changes the foreground association, never deletes resumable history.
|
|
987
|
+
if (sessionFile && !preserveHistory) {
|
|
761
988
|
try {
|
|
762
989
|
fs.unlinkSync(sessionFile);
|
|
763
990
|
}
|
|
@@ -766,14 +993,21 @@ class PiExecutor {
|
|
|
766
993
|
}
|
|
767
994
|
buildLaunch() {
|
|
768
995
|
const pointerFile = this.sessionFile && fs.existsSync(this.sessionFile) ? this.sessionFile : undefined;
|
|
996
|
+
const sessionId = this.sessionId ?? (this.isForegroundSession() ? this.freshSessionId : this.threadId);
|
|
769
997
|
return {
|
|
770
998
|
command: this.piCommand,
|
|
771
999
|
cwd: this.currentWorkingDirectory,
|
|
772
1000
|
approveProject: this.autoApprove,
|
|
773
1001
|
sessionDir: this.sessionStoreDir,
|
|
774
|
-
sessionId
|
|
1002
|
+
sessionId,
|
|
775
1003
|
sessionFile: pointerFile,
|
|
776
|
-
|
|
1004
|
+
// Name fresh foreground sessions by native identity, not their reusable thread.
|
|
1005
|
+
// Never rename a persisted file on reopen, including locally assigned names.
|
|
1006
|
+
sessionName: this.threadId && !this.adopted
|
|
1007
|
+
? this.isForegroundSession()
|
|
1008
|
+
? !pointerFile ? `remote-cli-${sessionId}` : undefined
|
|
1009
|
+
: `remote-cli-${this.threadId}`
|
|
1010
|
+
: undefined,
|
|
777
1011
|
provider: this.provider,
|
|
778
1012
|
model: this.model,
|
|
779
1013
|
thinking: this.effort,
|
|
@@ -1017,6 +1251,8 @@ class PiExecutor {
|
|
|
1017
1251
|
this.sessionId = stored.id;
|
|
1018
1252
|
if (stored.sessionFile)
|
|
1019
1253
|
this.sessionFile = stored.sessionFile;
|
|
1254
|
+
this.adopted = stored.adopted === true;
|
|
1255
|
+
// Legacy released flags no longer gate adopted sessions.
|
|
1020
1256
|
}
|
|
1021
1257
|
catch {
|
|
1022
1258
|
// First run for this thread.
|
|
@@ -1027,11 +1263,13 @@ class PiExecutor {
|
|
|
1027
1263
|
id: this.sessionId ?? undefined,
|
|
1028
1264
|
sessionFile: this.sessionFile ?? undefined,
|
|
1029
1265
|
cwd: this.currentWorkingDirectory,
|
|
1266
|
+
...(this.adopted ? { adopted: true } : {}),
|
|
1030
1267
|
};
|
|
1031
1268
|
const temporaryPath = `${this.sessionFilePath}.${process.pid}.tmp`;
|
|
1032
1269
|
try {
|
|
1033
1270
|
fs.writeFileSync(temporaryPath, JSON.stringify(pointer), { encoding: 'utf8', mode: 0o600 });
|
|
1034
1271
|
fs.renameSync(temporaryPath, this.sessionFilePath);
|
|
1272
|
+
return true;
|
|
1035
1273
|
}
|
|
1036
1274
|
catch (error) {
|
|
1037
1275
|
try {
|
|
@@ -1039,11 +1277,14 @@ class PiExecutor {
|
|
|
1039
1277
|
}
|
|
1040
1278
|
catch { /* ignore */ }
|
|
1041
1279
|
console.warn('[PiExecutor] Failed to save Pi session pointer', error);
|
|
1280
|
+
return false;
|
|
1042
1281
|
}
|
|
1043
1282
|
}
|
|
1044
1283
|
clearPointer() {
|
|
1045
1284
|
this.sessionId = null;
|
|
1046
1285
|
this.sessionFile = null;
|
|
1286
|
+
this.adopted = false;
|
|
1287
|
+
this.freshSessionId = (0, crypto_1.randomUUID)();
|
|
1047
1288
|
try {
|
|
1048
1289
|
fs.unlinkSync(this.sessionFilePath);
|
|
1049
1290
|
}
|