lunel-cli 0.1.64 → 0.1.66

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.
@@ -6,6 +6,7 @@ export declare class CodexProvider implements AIProvider {
6
6
  private nextId;
7
7
  private pending;
8
8
  private sessions;
9
+ private resumedThreadIds;
9
10
  private pendingPermissionRequestIds;
10
11
  private assistantMessageIdByTurnId;
11
12
  private partTextById;
@@ -71,6 +72,13 @@ export declare class CodexProvider implements AIProvider {
71
72
  private mergeSession;
72
73
  private upsertSession;
73
74
  private ensureLocalSession;
75
+ private getTransportThreadId;
76
+ private findSessionByThreadId;
77
+ private ensureThreadResumed;
78
+ private shouldTreatAsThreadNotFound;
79
+ private ensurePromptSessionReady;
80
+ private createContinuationSession;
81
+ private handleMissingThread;
74
82
  private resolveSessionFromPayload;
75
83
  private resolveInFlightTurnId;
76
84
  private decodeMessagesFromThreadRead;
@@ -94,6 +102,7 @@ export declare class CodexProvider implements AIProvider {
94
102
  private extractItemId;
95
103
  private extractTextPayload;
96
104
  private extractThreadId;
105
+ private extractThreadCwd;
97
106
  private extractCreatedAt;
98
107
  private extractUpdatedAt;
99
108
  private readTimestamp;
@@ -115,6 +124,8 @@ export declare class CodexProvider implements AIProvider {
115
124
  private renderUnifiedDiffBody;
116
125
  private extractCanonicalPatch;
117
126
  private normalizeDisplayPath;
127
+ private normalizeDirectoryPath;
128
+ private belongsToCurrentRoot;
118
129
  private normalizedFileChangeStatus;
119
130
  private extractToolInput;
120
131
  private extractCommandExecutionInput;
package/dist/ai/codex.js CHANGED
@@ -13,6 +13,7 @@ export class CodexProvider {
13
13
  nextId = 1;
14
14
  pending = new Map();
15
15
  sessions = new Map();
16
+ resumedThreadIds = new Set();
16
17
  pendingPermissionRequestIds = new Map();
17
18
  assistantMessageIdByTurnId = new Map();
18
19
  partTextById = new Map();
@@ -51,7 +52,7 @@ export class CodexProvider {
51
52
  };
52
53
  }
53
54
  async createSession(title) {
54
- const result = await this.call("thread/start", {});
55
+ const result = await this.call("thread/start", { cwd: process.cwd() });
55
56
  const threadId = this.extractThreadId(result);
56
57
  if (!threadId) {
57
58
  throw new Error("thread/start response missing threadId");
@@ -63,6 +64,7 @@ export class CodexProvider {
63
64
  createdAt: this.extractCreatedAt(result) ?? Date.now(),
64
65
  updatedAt: this.extractUpdatedAt(result) ?? Date.now(),
65
66
  archived: false,
67
+ cwd: this.extractThreadCwd(result) ?? process.cwd(),
66
68
  });
67
69
  return { session: this.toSessionInfo(session) };
68
70
  }
@@ -77,6 +79,7 @@ export class CodexProvider {
77
79
  }
78
80
  this.reconcileSessionsWithServer(activeThreads, archivedThreads);
79
81
  const sessions = Array.from(this.sessions.values())
82
+ .filter((session) => this.belongsToCurrentRoot(session))
80
83
  .sort((a, b) => a.updatedAt - b.updatedAt)
81
84
  .map((session) => this.toSessionInfo(session));
82
85
  return { sessions };
@@ -99,8 +102,9 @@ export class CodexProvider {
99
102
  }
100
103
  async getMessages(sessionId) {
101
104
  const session = this.ensureLocalSession(sessionId);
105
+ const transportThreadId = this.getTransportThreadId(session);
102
106
  const result = await this.call("thread/read", {
103
- threadId: sessionId,
107
+ threadId: transportThreadId,
104
108
  includeTurns: true,
105
109
  });
106
110
  const threadObject = this.extractThreadObject(result);
@@ -117,6 +121,8 @@ export class CodexProvider {
117
121
  createdAt: this.extractCreatedAt(threadObject) ?? session.createdAt,
118
122
  updatedAt: this.extractUpdatedAt(threadObject) ?? session.updatedAt,
119
123
  archived: false,
124
+ transportThreadId,
125
+ cwd: this.extractThreadCwd(threadObject) ?? session.cwd,
120
126
  });
121
127
  return { messages: session.messages };
122
128
  }
@@ -125,11 +131,12 @@ export class CodexProvider {
125
131
  session.updatedAt = Date.now();
126
132
  (async () => {
127
133
  try {
134
+ let targetSession = await this.ensurePromptSessionReady(session);
128
135
  let imageUrlKey = "url";
129
136
  while (true) {
130
137
  try {
131
138
  await this.call("turn/start", {
132
- threadId: session.id,
139
+ threadId: this.getTransportThreadId(targetSession),
133
140
  input: this.makeTurnInputPayload(text, files, imageUrlKey),
134
141
  ...(model ? { model: model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}` } : {}),
135
142
  ...(agent ? { agent } : {}),
@@ -137,6 +144,11 @@ export class CodexProvider {
137
144
  break;
138
145
  }
139
146
  catch (err) {
147
+ if (this.shouldTreatAsThreadNotFound(err)) {
148
+ targetSession = await this.createContinuationSession(targetSession);
149
+ imageUrlKey = "url";
150
+ continue;
151
+ }
140
152
  if (imageUrlKey === "url"
141
153
  && files.length > 0
142
154
  && this.shouldRetryTurnStartWithImageURLField(err)) {
@@ -157,12 +169,13 @@ export class CodexProvider {
157
169
  }
158
170
  async abort(sessionId) {
159
171
  const session = this.ensureLocalSession(sessionId);
160
- const turnId = session.activeTurnId ?? await this.resolveInFlightTurnId(sessionId);
172
+ const transportThreadId = this.getTransportThreadId(session);
173
+ const turnId = session.activeTurnId ?? await this.resolveInFlightTurnId(transportThreadId);
161
174
  if (!turnId) {
162
175
  throw new Error(`Session ${sessionId} has no active interruptible Codex turn`);
163
176
  }
164
177
  session.activeTurnId = turnId;
165
- await this.call("turn/interrupt", { threadId: sessionId, turnId });
178
+ await this.call("turn/interrupt", { threadId: transportThreadId, turnId });
166
179
  return {};
167
180
  }
168
181
  async agents() {
@@ -660,8 +673,9 @@ export class CodexProvider {
660
673
  }
661
674
  async refreshSessionMetadata(sessionId) {
662
675
  const session = this.sessions.get(sessionId);
676
+ const transportThreadId = session ? this.getTransportThreadId(session) : sessionId;
663
677
  const result = await this.call("thread/read", {
664
- threadId: sessionId,
678
+ threadId: transportThreadId,
665
679
  includeTurns: false,
666
680
  });
667
681
  const threadObject = this.extractThreadObject(result);
@@ -674,6 +688,8 @@ export class CodexProvider {
674
688
  createdAt: this.extractCreatedAt(threadObject) ?? session?.createdAt ?? Date.now(),
675
689
  updatedAt: this.extractUpdatedAt(threadObject) ?? session?.updatedAt ?? Date.now(),
676
690
  archived: false,
691
+ transportThreadId,
692
+ cwd: this.extractThreadCwd(threadObject) ?? session?.cwd,
677
693
  }, true);
678
694
  }
679
695
  async fetchServerThreadsByArchiveState(archived) {
@@ -748,6 +764,7 @@ export class CodexProvider {
748
764
  createdAt: this.extractCreatedAt(obj),
749
765
  updatedAt: this.extractUpdatedAt(obj),
750
766
  archived,
767
+ cwd: this.extractThreadCwd(obj),
751
768
  };
752
769
  }
753
770
  hasNextCursor(value) {
@@ -767,6 +784,7 @@ export class CodexProvider {
767
784
  title,
768
785
  createdAt: this.extractCreatedAt(payload) ?? Date.now(),
769
786
  updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
787
+ cwd: this.extractThreadCwd(payload),
770
788
  });
771
789
  }
772
790
  reconcileSessionsWithServer(activeThreads, archivedThreads = []) {
@@ -797,6 +815,8 @@ export class CodexProvider {
797
815
  createdAt: input.createdAt ?? Date.now(),
798
816
  updatedAt: input.updatedAt ?? Date.now(),
799
817
  archived: input.archived ?? false,
818
+ transportThreadId: input.transportThreadId ?? input.id,
819
+ cwd: input.cwd,
800
820
  messages: [],
801
821
  };
802
822
  }
@@ -806,25 +826,31 @@ export class CodexProvider {
806
826
  ? Math.max(existing.updatedAt, input.updatedAt)
807
827
  : existing.updatedAt;
808
828
  existing.archived = input.archived ?? existing.archived ?? false;
829
+ existing.transportThreadId = input.transportThreadId ?? input.id ?? existing.transportThreadId ?? existing.id;
830
+ existing.cwd = input.cwd ?? existing.cwd;
809
831
  return existing;
810
832
  }
811
833
  upsertSession(input, emitUpdated = false) {
812
- const existing = this.sessions.get(input.id);
834
+ const existing = this.findSessionByThreadId(input.id);
813
835
  const before = existing
814
836
  ? {
815
837
  title: existing.title,
816
838
  createdAt: existing.createdAt,
817
839
  updatedAt: existing.updatedAt,
818
840
  archived: existing.archived ?? false,
841
+ transportThreadId: existing.transportThreadId ?? existing.id,
842
+ cwd: existing.cwd,
819
843
  }
820
844
  : null;
821
845
  const session = this.mergeSession(existing, input);
822
- this.sessions.set(input.id, session);
846
+ this.sessions.set(session.id, session);
823
847
  const changed = !before
824
848
  || before.title !== session.title
825
849
  || before.createdAt !== session.createdAt
826
850
  || before.updatedAt !== session.updatedAt
827
- || before.archived !== (session.archived ?? false);
851
+ || before.archived !== (session.archived ?? false)
852
+ || before.transportThreadId !== (session.transportThreadId ?? session.id)
853
+ || before.cwd !== session.cwd;
828
854
  if (emitUpdated && changed) {
829
855
  this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
830
856
  }
@@ -836,9 +862,102 @@ export class CodexProvider {
836
862
  return existing;
837
863
  return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
838
864
  }
865
+ getTransportThreadId(session) {
866
+ return session.transportThreadId ?? session.id;
867
+ }
868
+ findSessionByThreadId(threadId) {
869
+ const direct = this.sessions.get(threadId);
870
+ if (direct)
871
+ return direct;
872
+ for (const session of this.sessions.values()) {
873
+ if ((session.transportThreadId ?? session.id) === threadId) {
874
+ return session;
875
+ }
876
+ }
877
+ return undefined;
878
+ }
879
+ async ensureThreadResumed(threadId) {
880
+ if (!threadId || this.resumedThreadIds.has(threadId)) {
881
+ return;
882
+ }
883
+ await this.call("thread/resume", { threadId });
884
+ this.resumedThreadIds.add(threadId);
885
+ }
886
+ shouldTreatAsThreadNotFound(error) {
887
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
888
+ if (message.includes("not materialized") || message.includes("not yet materialized")) {
889
+ return false;
890
+ }
891
+ return message.includes("thread not found") || message.includes("unknown thread");
892
+ }
893
+ async ensurePromptSessionReady(session) {
894
+ const threadId = this.getTransportThreadId(session);
895
+ try {
896
+ await this.ensureThreadResumed(threadId);
897
+ return session;
898
+ }
899
+ catch (error) {
900
+ if (!this.shouldTreatAsThreadNotFound(error)) {
901
+ throw error;
902
+ }
903
+ return this.createContinuationSession(session);
904
+ }
905
+ }
906
+ async createContinuationSession(session) {
907
+ const previousThreadId = this.getTransportThreadId(session);
908
+ this.handleMissingThread(session);
909
+ const result = await this.call("thread/start", {
910
+ ...(session.cwd ? { cwd: session.cwd } : { cwd: process.cwd() }),
911
+ });
912
+ const threadId = this.extractThreadId(result);
913
+ if (!threadId) {
914
+ throw new Error("thread/start response missing threadId");
915
+ }
916
+ session.transportThreadId = threadId;
917
+ session.updatedAt = Date.now();
918
+ session.archived = false;
919
+ this.upsertSession({
920
+ id: session.id,
921
+ title: this.extractThreadTitleFromUnknown(result) ?? session.title,
922
+ createdAt: session.createdAt,
923
+ updatedAt: this.extractUpdatedAt(result) ?? Date.now(),
924
+ archived: false,
925
+ transportThreadId: threadId,
926
+ cwd: this.extractThreadCwd(result) ?? session.cwd ?? process.cwd(),
927
+ }, true);
928
+ this.resumedThreadIds.add(threadId);
929
+ const systemMessageId = `system:${crypto.randomUUID()}`;
930
+ session.messages.push({
931
+ id: systemMessageId,
932
+ role: "assistant",
933
+ parts: [{
934
+ id: `${systemMessageId}:text`,
935
+ type: "text",
936
+ text: `Continued from archived thread \`${previousThreadId}\``,
937
+ sessionID: session.id,
938
+ messageID: systemMessageId,
939
+ }],
940
+ time: Date.now(),
941
+ });
942
+ this.emitMessagePartEvent(session.id, systemMessageId, "assistant", {
943
+ id: `${systemMessageId}:text`,
944
+ type: "text",
945
+ text: `Continued from archived thread \`${previousThreadId}\``,
946
+ sessionID: session.id,
947
+ messageID: systemMessageId,
948
+ });
949
+ return session;
950
+ }
951
+ handleMissingThread(session) {
952
+ const threadId = this.getTransportThreadId(session);
953
+ this.resumedThreadIds.delete(threadId);
954
+ session.activeTurnId = undefined;
955
+ }
839
956
  resolveSessionFromPayload(payload) {
840
957
  const threadId = this.extractThreadId(payload);
841
- return threadId ? this.ensureLocalSession(threadId) : undefined;
958
+ if (!threadId)
959
+ return undefined;
960
+ return this.findSessionByThreadId(threadId) ?? this.ensureLocalSession(threadId);
842
961
  }
843
962
  async resolveInFlightTurnId(threadId) {
844
963
  const result = await this.call("thread/read", { threadId, includeTurns: true });
@@ -1211,6 +1330,19 @@ export class CodexProvider {
1211
1330
  const thread = this.asRecord(obj.thread);
1212
1331
  return this.readString(thread.id) ?? this.readString(thread.threadId) ?? this.readString(thread.thread_id);
1213
1332
  }
1333
+ extractThreadCwd(payload) {
1334
+ const obj = this.extractThreadObject(payload);
1335
+ const cwd = this.firstString(obj, [
1336
+ "cwd",
1337
+ "projectPath",
1338
+ "project_path",
1339
+ "gitWorkingDirectory",
1340
+ "git_working_directory",
1341
+ "workingDirectory",
1342
+ "working_directory",
1343
+ ]);
1344
+ return cwd ? this.normalizeDirectoryPath(cwd) : undefined;
1345
+ }
1214
1346
  extractCreatedAt(payload) {
1215
1347
  const obj = this.extractThreadObject(payload);
1216
1348
  return this.readTimestamp(obj.createdAt) ?? this.readTimestamp(obj.created_at);
@@ -1441,6 +1573,22 @@ export class CodexProvider {
1441
1573
  }
1442
1574
  return relative;
1443
1575
  }
1576
+ normalizeDirectoryPath(rawPath) {
1577
+ if (!rawPath)
1578
+ return undefined;
1579
+ const trimmed = rawPath.trim();
1580
+ if (!trimmed)
1581
+ return undefined;
1582
+ return path.resolve(trimmed);
1583
+ }
1584
+ belongsToCurrentRoot(session) {
1585
+ const sessionCwd = this.normalizeDirectoryPath(session.cwd);
1586
+ const currentRoot = this.normalizeDirectoryPath(process.cwd());
1587
+ if (!sessionCwd || !currentRoot) {
1588
+ return false;
1589
+ }
1590
+ return sessionCwd === currentRoot;
1591
+ }
1444
1592
  normalizedFileChangeStatus(itemObject, isCompleted) {
1445
1593
  const status = this.readString(itemObject.status)
1446
1594
  ?? this.readString(this.asRecord(itemObject.status).type)
package/dist/index.js CHANGED
@@ -2494,11 +2494,56 @@ async function createSessionFromManager() {
2494
2494
  return (await response.json());
2495
2495
  }
2496
2496
  async function connectToCloudSession(sessionCode) {
2497
- const response = await fetch(`${MANAGER_URL}/v1/session/${sessionCode}`);
2497
+ const response = await fetch(`${MANAGER_URL}/v1/session/resolve`, {
2498
+ method: "POST",
2499
+ headers: { "Content-Type": "application/json" },
2500
+ body: JSON.stringify({ code: sessionCode }),
2501
+ });
2498
2502
  if (!response.ok) {
2499
2503
  throw new Error(`Failed to look up cloud session ${sessionCode}: ${response.status}`);
2500
2504
  }
2501
- return (await response.json());
2505
+ const snapshot = await response.json();
2506
+ if (!snapshot.exists || !snapshot.valid || !snapshot.primary || !snapshot.resumeToken || !snapshot.code) {
2507
+ throw new Error(`Failed to look up cloud session ${sessionCode}: invalid or expired session`);
2508
+ }
2509
+ return {
2510
+ sessionId: snapshot.sessionId || "",
2511
+ code: snapshot.code,
2512
+ password: snapshot.resumeToken,
2513
+ primary: snapshot.primary,
2514
+ backup: snapshot.backup ?? null,
2515
+ state: snapshot.state,
2516
+ expiresAt: snapshot.expiresAt,
2517
+ };
2518
+ }
2519
+ async function resolveSessionByResumeToken(resumeToken) {
2520
+ const response = await fetch(`${MANAGER_URL}/v1/session/resolve`, {
2521
+ method: "POST",
2522
+ headers: { "Content-Type": "application/json" },
2523
+ body: JSON.stringify({ resumeToken }),
2524
+ });
2525
+ if (!response.ok) {
2526
+ throw new Error(`Failed to resolve session from manager: ${response.status}`);
2527
+ }
2528
+ const snapshot = await response.json();
2529
+ if (!snapshot.exists || !snapshot.valid || !snapshot.primary || !snapshot.resumeToken || !snapshot.code) {
2530
+ if (snapshot.reason === "session_finalized" || snapshot.reason === "not_found") {
2531
+ return null;
2532
+ }
2533
+ if (snapshot.state === "ended" || snapshot.state === "expired") {
2534
+ return null;
2535
+ }
2536
+ throw new Error(`Session resolve returned invalid snapshot (${snapshot.reason || "unknown"})`);
2537
+ }
2538
+ return {
2539
+ sessionId: snapshot.sessionId || "",
2540
+ code: snapshot.code,
2541
+ password: snapshot.resumeToken,
2542
+ primary: snapshot.primary,
2543
+ backup: snapshot.backup ?? null,
2544
+ state: snapshot.state,
2545
+ expiresAt: snapshot.expiresAt,
2546
+ };
2502
2547
  }
2503
2548
  function displayQR(primaryGateway, backupGateway, code) {
2504
2549
  console.log("\n");
@@ -2776,13 +2821,30 @@ async function handleConnectionDrop(reason) {
2776
2821
  gracefulShutdown();
2777
2822
  return;
2778
2823
  }
2779
- try {
2780
- await connectWebSocket();
2781
- console.log(`[reconnect] connected via ${activeGatewayUrl}`);
2782
- }
2783
- catch (err) {
2784
- console.error(`[reconnect] failed on all gateways: ${err.message}`);
2785
- gracefulShutdown();
2824
+ let attempt = 0;
2825
+ while (!shuttingDown) {
2826
+ attempt += 1;
2827
+ const base = Math.min(250 * 2 ** (attempt - 1), 30_000);
2828
+ const delayMs = Math.round(base * (0.8 + Math.random() * 0.4));
2829
+ try {
2830
+ const resolved = await resolveSessionByResumeToken(currentSessionPassword);
2831
+ if (!resolved) {
2832
+ console.error("[reconnect] session no longer exists or is finalized");
2833
+ gracefulShutdown();
2834
+ return;
2835
+ }
2836
+ currentSessionCode = resolved.code;
2837
+ currentSessionPassword = resolved.password;
2838
+ currentPrimaryGateway = normalizeGatewayUrl(resolved.primary);
2839
+ currentBackupGateway = resolved.backup ? normalizeGatewayUrl(resolved.backup) : null;
2840
+ await connectWebSocket();
2841
+ console.log(`[reconnect] connected via ${activeGatewayUrl}`);
2842
+ return;
2843
+ }
2844
+ catch (err) {
2845
+ console.error(`[reconnect] attempt ${attempt} failed: ${err.message}`);
2846
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2847
+ }
2786
2848
  }
2787
2849
  }
2788
2850
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",