@phenx-inc/ctlsurf 0.3.6 → 0.3.7

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/out/main/index.js CHANGED
@@ -10160,55 +10160,68 @@ class TimeTracker {
10160
10160
  if (this.sessions.has(tabId)) {
10161
10161
  await this.endSession(tabId);
10162
10162
  }
10163
+ const startedAt = Date.now();
10164
+ const state = {
10165
+ blockId: null,
10166
+ rowId: null,
10167
+ sessionUuid: require$$1.randomUUID(),
10168
+ cwd,
10169
+ agentName,
10170
+ idleTimeoutMin,
10171
+ startedAt,
10172
+ lastActivity: startedAt,
10173
+ activeMs: 0,
10174
+ idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1e3,
10175
+ firstCheckpointTimer: null,
10176
+ checkpointTimer: null,
10177
+ ended: false
10178
+ };
10179
+ this.sessions.set(tabId, state);
10180
+ await this.tryResolve(tabId);
10181
+ state.firstCheckpointTimer = setTimeout(() => {
10182
+ void this.checkpoint(tabId);
10183
+ const live = this.sessions.get(tabId);
10184
+ if (live && !live.ended) {
10185
+ live.checkpointTimer = setInterval(() => {
10186
+ void this.checkpoint(tabId);
10187
+ }, CHECKPOINT_INTERVAL_MS);
10188
+ }
10189
+ }, FIRST_CHECKPOINT_DELAY_MS);
10190
+ const pending = !state.blockId || !state.rowId;
10191
+ log$2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? " (pending datastore — will retry on each checkpoint)" : ""}`);
10192
+ }
10193
+ /** Attempts to locate (or create) the datastore + add the session row.
10194
+ * Returns true once the session is resolved (blockId + rowId set).
10195
+ * Safe to call repeatedly: re-running while pending will keep retrying;
10196
+ * once resolved it's a no-op. */
10197
+ async tryResolve(tabId) {
10198
+ const s = this.sessions.get(tabId);
10199
+ if (!s) return false;
10200
+ if (s.blockId && s.rowId) return true;
10163
10201
  try {
10164
- const blockId = await this.ensureDatastore(cwd);
10165
- if (!blockId) {
10166
- log$2(`No "${AGENT_DATASTORE_PAGE_TITLE}" page found for ${cwd} — tracking disabled for this session`);
10167
- return;
10168
- }
10169
- const startedAt = Date.now();
10170
- const sessionUuid = require$$1.randomUUID();
10202
+ const blockId = await this.ensureDatastore(s.cwd);
10203
+ if (!blockId) return false;
10171
10204
  const row = await this.api.addRow(blockId, {
10172
- Started: formatStarted(startedAt),
10173
- "Active Time": 0,
10174
- "Last Updated": new Date(startedAt).toISOString(),
10175
- Agent: agentName,
10205
+ Started: formatStarted(s.startedAt),
10206
+ "Active Time": Math.round(s.activeMs / 6e4),
10207
+ "Last Updated": (/* @__PURE__ */ new Date()).toISOString(),
10208
+ Agent: s.agentName,
10176
10209
  Worker: os.hostname(),
10177
- Session: sessionUuid,
10210
+ Session: s.sessionUuid,
10178
10211
  Notes: ""
10179
10212
  });
10180
10213
  const rowId = row?.id;
10181
10214
  if (!rowId) {
10182
- log$2("addRow returned no id; aborting tracking", row);
10183
- return;
10215
+ log$2(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
10216
+ return false;
10184
10217
  }
10185
- const state = {
10186
- blockId,
10187
- rowId,
10188
- cwd,
10189
- agentName,
10190
- idleTimeoutMin,
10191
- startedAt,
10192
- lastActivity: startedAt,
10193
- activeMs: 0,
10194
- idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1e3,
10195
- firstCheckpointTimer: null,
10196
- checkpointTimer: null,
10197
- ended: false
10198
- };
10199
- state.firstCheckpointTimer = setTimeout(() => {
10200
- void this.checkpoint(tabId);
10201
- const live = this.sessions.get(tabId);
10202
- if (live && !live.ended) {
10203
- live.checkpointTimer = setInterval(() => {
10204
- void this.checkpoint(tabId);
10205
- }, CHECKPOINT_INTERVAL_MS);
10206
- }
10207
- }, FIRST_CHECKPOINT_DELAY_MS);
10208
- this.sessions.set(tabId, state);
10209
- log$2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}`);
10218
+ s.blockId = blockId;
10219
+ s.rowId = rowId;
10220
+ log$2(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
10221
+ return true;
10210
10222
  } catch (err) {
10211
- log$2(`startSession failed: ${err?.message || err}`);
10223
+ log$2(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
10224
+ return false;
10212
10225
  }
10213
10226
  }
10214
10227
  isTracking(tabId) {
@@ -10248,14 +10261,21 @@ class TimeTracker {
10248
10261
  async endSession(tabId) {
10249
10262
  const s = this.sessions.get(tabId);
10250
10263
  if (!s || s.ended) return;
10251
- s.ended = true;
10252
10264
  if (s.firstCheckpointTimer) clearTimeout(s.firstCheckpointTimer);
10253
10265
  if (s.checkpointTimer) clearInterval(s.checkpointTimer);
10254
10266
  try {
10255
- await this.writeRow(s, Date.now());
10267
+ if (!s.blockId || !s.rowId) {
10268
+ await this.tryResolve(tabId);
10269
+ }
10270
+ if (s.blockId && s.rowId) {
10271
+ await this.writeRow(s, Date.now());
10272
+ } else {
10273
+ log$2(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 6e4)}min not recorded`);
10274
+ }
10256
10275
  } catch (err) {
10257
10276
  log$2(`endSession write failed: ${err?.message || err}`);
10258
10277
  }
10278
+ s.ended = true;
10259
10279
  this.sessions.delete(tabId);
10260
10280
  }
10261
10281
  async endAll() {
@@ -10265,13 +10285,16 @@ class TimeTracker {
10265
10285
  async checkpoint(tabId) {
10266
10286
  const s = this.sessions.get(tabId);
10267
10287
  if (!s || s.ended) return;
10288
+ if (!s.blockId || !s.rowId) {
10289
+ if (!await this.tryResolve(tabId)) return;
10290
+ }
10268
10291
  try {
10269
10292
  await this.writeRow(s, Date.now());
10270
10293
  } catch (err) {
10271
10294
  log$2(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
10272
10295
  setTimeout(() => {
10273
10296
  const live = this.sessions.get(tabId);
10274
- if (!live || live.ended) return;
10297
+ if (!live || live.ended || !live.blockId || !live.rowId) return;
10275
10298
  this.writeRow(live, Date.now()).catch((err2) => {
10276
10299
  log$2(`checkpoint retry failed: ${err2?.message || err2}`);
10277
10300
  });
@@ -10279,6 +10302,7 @@ class TimeTracker {
10279
10302
  }
10280
10303
  }
10281
10304
  async writeRow(s, _endTimeMs) {
10305
+ if (!s.blockId || !s.rowId) return;
10282
10306
  const activeMin = Math.round(s.activeMs / 6e4);
10283
10307
  await this.api.updateRow(s.blockId, s.rowId, {
10284
10308
  "Active Time": activeMin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -30,8 +30,9 @@ function formatStarted(ms: number): string {
30
30
  }
31
31
 
32
32
  interface SessionState {
33
- blockId: string
34
- rowId: string
33
+ blockId: string | null
34
+ rowId: string | null
35
+ sessionUuid: string
35
36
  cwd: string
36
37
  agentName: string
37
38
  idleTimeoutMin: number
@@ -83,55 +84,72 @@ export class TimeTracker {
83
84
  if (this.sessions.has(tabId)) {
84
85
  await this.endSession(tabId)
85
86
  }
86
- try {
87
- const blockId = await this.ensureDatastore(cwd)
88
- if (!blockId) {
89
- log(`No "${AGENT_DATASTORE_PAGE_TITLE}" page found for ${cwd} — tracking disabled for this session`)
90
- return
87
+ const startedAt = Date.now()
88
+ const state: SessionState = {
89
+ blockId: null,
90
+ rowId: null,
91
+ sessionUuid: randomUUID(),
92
+ cwd,
93
+ agentName,
94
+ idleTimeoutMin,
95
+ startedAt,
96
+ lastActivity: startedAt,
97
+ activeMs: 0,
98
+ idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1000,
99
+ firstCheckpointTimer: null,
100
+ checkpointTimer: null,
101
+ ended: false,
102
+ }
103
+ this.sessions.set(tabId, state)
104
+
105
+ await this.tryResolve(tabId)
106
+
107
+ state.firstCheckpointTimer = setTimeout(() => {
108
+ void this.checkpoint(tabId)
109
+ const live = this.sessions.get(tabId)
110
+ if (live && !live.ended) {
111
+ live.checkpointTimer = setInterval(() => {
112
+ void this.checkpoint(tabId)
113
+ }, CHECKPOINT_INTERVAL_MS)
91
114
  }
92
- const startedAt = Date.now()
93
- const sessionUuid = randomUUID()
115
+ }, FIRST_CHECKPOINT_DELAY_MS)
116
+
117
+ const pending = !state.blockId || !state.rowId
118
+ log(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? ' (pending datastore — will retry on each checkpoint)' : ''}`)
119
+ }
120
+
121
+ /** Attempts to locate (or create) the datastore + add the session row.
122
+ * Returns true once the session is resolved (blockId + rowId set).
123
+ * Safe to call repeatedly: re-running while pending will keep retrying;
124
+ * once resolved it's a no-op. */
125
+ private async tryResolve(tabId: string): Promise<boolean> {
126
+ const s = this.sessions.get(tabId)
127
+ if (!s) return false
128
+ if (s.blockId && s.rowId) return true
129
+ try {
130
+ const blockId = await this.ensureDatastore(s.cwd)
131
+ if (!blockId) return false
94
132
  const row = await this.api.addRow(blockId, {
95
- Started: formatStarted(startedAt),
96
- 'Active Time': 0,
97
- 'Last Updated': new Date(startedAt).toISOString(),
98
- Agent: agentName,
133
+ Started: formatStarted(s.startedAt),
134
+ 'Active Time': Math.round(s.activeMs / 60000),
135
+ 'Last Updated': new Date().toISOString(),
136
+ Agent: s.agentName,
99
137
  Worker: os.hostname(),
100
- Session: sessionUuid,
138
+ Session: s.sessionUuid,
101
139
  Notes: '',
102
140
  })
103
141
  const rowId = row?.id
104
142
  if (!rowId) {
105
- log('addRow returned no id; aborting tracking', row)
106
- return
143
+ log(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`)
144
+ return false
107
145
  }
108
- const state: SessionState = {
109
- blockId,
110
- rowId,
111
- cwd,
112
- agentName,
113
- idleTimeoutMin,
114
- startedAt,
115
- lastActivity: startedAt,
116
- activeMs: 0,
117
- idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1000,
118
- firstCheckpointTimer: null,
119
- checkpointTimer: null,
120
- ended: false,
121
- }
122
- state.firstCheckpointTimer = setTimeout(() => {
123
- void this.checkpoint(tabId)
124
- const live = this.sessions.get(tabId)
125
- if (live && !live.ended) {
126
- live.checkpointTimer = setInterval(() => {
127
- void this.checkpoint(tabId)
128
- }, CHECKPOINT_INTERVAL_MS)
129
- }
130
- }, FIRST_CHECKPOINT_DELAY_MS)
131
- this.sessions.set(tabId, state)
132
- log(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}`)
146
+ s.blockId = blockId
147
+ s.rowId = rowId
148
+ log(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`)
149
+ return true
133
150
  } catch (err: any) {
134
- log(`startSession failed: ${err?.message || err}`)
151
+ log(`tryResolve failed for tab=${tabId}: ${err?.message || err}`)
152
+ return false
135
153
  }
136
154
  }
137
155
 
@@ -175,14 +193,21 @@ export class TimeTracker {
175
193
  async endSession(tabId: string): Promise<void> {
176
194
  const s = this.sessions.get(tabId)
177
195
  if (!s || s.ended) return
178
- s.ended = true
179
196
  if (s.firstCheckpointTimer) clearTimeout(s.firstCheckpointTimer)
180
197
  if (s.checkpointTimer) clearInterval(s.checkpointTimer)
181
198
  try {
182
- await this.writeRow(s, Date.now())
199
+ if (!s.blockId || !s.rowId) {
200
+ await this.tryResolve(tabId)
201
+ }
202
+ if (s.blockId && s.rowId) {
203
+ await this.writeRow(s, Date.now())
204
+ } else {
205
+ log(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 60000)}min not recorded`)
206
+ }
183
207
  } catch (err: any) {
184
208
  log(`endSession write failed: ${err?.message || err}`)
185
209
  }
210
+ s.ended = true
186
211
  this.sessions.delete(tabId)
187
212
  }
188
213
 
@@ -194,13 +219,16 @@ export class TimeTracker {
194
219
  private async checkpoint(tabId: string): Promise<void> {
195
220
  const s = this.sessions.get(tabId)
196
221
  if (!s || s.ended) return
222
+ if (!s.blockId || !s.rowId) {
223
+ if (!(await this.tryResolve(tabId))) return
224
+ }
197
225
  try {
198
226
  await this.writeRow(s, Date.now())
199
227
  } catch (err: any) {
200
228
  log(`checkpoint failed: ${err?.message || err}; retrying in 2s`)
201
229
  setTimeout(() => {
202
230
  const live = this.sessions.get(tabId)
203
- if (!live || live.ended) return
231
+ if (!live || live.ended || !live.blockId || !live.rowId) return
204
232
  this.writeRow(live, Date.now()).catch((err2: any) => {
205
233
  log(`checkpoint retry failed: ${err2?.message || err2}`)
206
234
  })
@@ -209,6 +237,7 @@ export class TimeTracker {
209
237
  }
210
238
 
211
239
  private async writeRow(s: SessionState, _endTimeMs: number): Promise<void> {
240
+ if (!s.blockId || !s.rowId) return
212
241
  const activeMin = Math.round(s.activeMs / 60000)
213
242
  await this.api.updateRow(s.blockId, s.rowId, {
214
243
  'Active Time': activeMin,