@phenx-inc/ctlsurf 0.3.5 → 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
@@ -9949,12 +9949,6 @@ class WorkerWsClient {
9949
9949
  this.send({ type: "terminal_resize", cols, rows });
9950
9950
  }
9951
9951
  sendChatLog(entry) {
9952
- log$3("[worker-ws] chat_log DEBUG", {
9953
- type: entry.type,
9954
- chars: entry.content.length,
9955
- lines: entry.content.split("\n").length,
9956
- preview: entry.content.slice(0, 500)
9957
- });
9958
9952
  this.send({ type: "chat_log", entry });
9959
9953
  }
9960
9954
  doConnect() {
@@ -10166,55 +10160,68 @@ class TimeTracker {
10166
10160
  if (this.sessions.has(tabId)) {
10167
10161
  await this.endSession(tabId);
10168
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;
10169
10201
  try {
10170
- const blockId = await this.ensureDatastore(cwd);
10171
- if (!blockId) {
10172
- log$2(`No "${AGENT_DATASTORE_PAGE_TITLE}" page found for ${cwd} — tracking disabled for this session`);
10173
- return;
10174
- }
10175
- const startedAt = Date.now();
10176
- const sessionUuid = require$$1.randomUUID();
10202
+ const blockId = await this.ensureDatastore(s.cwd);
10203
+ if (!blockId) return false;
10177
10204
  const row = await this.api.addRow(blockId, {
10178
- Started: formatStarted(startedAt),
10179
- "Active Time": 0,
10180
- "Last Updated": new Date(startedAt).toISOString(),
10181
- 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,
10182
10209
  Worker: os.hostname(),
10183
- Session: sessionUuid,
10210
+ Session: s.sessionUuid,
10184
10211
  Notes: ""
10185
10212
  });
10186
10213
  const rowId = row?.id;
10187
10214
  if (!rowId) {
10188
- log$2("addRow returned no id; aborting tracking", row);
10189
- return;
10215
+ log$2(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
10216
+ return false;
10190
10217
  }
10191
- const state = {
10192
- blockId,
10193
- rowId,
10194
- cwd,
10195
- agentName,
10196
- idleTimeoutMin,
10197
- startedAt,
10198
- lastActivity: startedAt,
10199
- activeMs: 0,
10200
- idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1e3,
10201
- firstCheckpointTimer: null,
10202
- checkpointTimer: null,
10203
- ended: false
10204
- };
10205
- state.firstCheckpointTimer = setTimeout(() => {
10206
- void this.checkpoint(tabId);
10207
- const live = this.sessions.get(tabId);
10208
- if (live && !live.ended) {
10209
- live.checkpointTimer = setInterval(() => {
10210
- void this.checkpoint(tabId);
10211
- }, CHECKPOINT_INTERVAL_MS);
10212
- }
10213
- }, FIRST_CHECKPOINT_DELAY_MS);
10214
- this.sessions.set(tabId, state);
10215
- 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;
10216
10222
  } catch (err) {
10217
- log$2(`startSession failed: ${err?.message || err}`);
10223
+ log$2(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
10224
+ return false;
10218
10225
  }
10219
10226
  }
10220
10227
  isTracking(tabId) {
@@ -10254,14 +10261,21 @@ class TimeTracker {
10254
10261
  async endSession(tabId) {
10255
10262
  const s = this.sessions.get(tabId);
10256
10263
  if (!s || s.ended) return;
10257
- s.ended = true;
10258
10264
  if (s.firstCheckpointTimer) clearTimeout(s.firstCheckpointTimer);
10259
10265
  if (s.checkpointTimer) clearInterval(s.checkpointTimer);
10260
10266
  try {
10261
- 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
+ }
10262
10275
  } catch (err) {
10263
10276
  log$2(`endSession write failed: ${err?.message || err}`);
10264
10277
  }
10278
+ s.ended = true;
10265
10279
  this.sessions.delete(tabId);
10266
10280
  }
10267
10281
  async endAll() {
@@ -10271,13 +10285,16 @@ class TimeTracker {
10271
10285
  async checkpoint(tabId) {
10272
10286
  const s = this.sessions.get(tabId);
10273
10287
  if (!s || s.ended) return;
10288
+ if (!s.blockId || !s.rowId) {
10289
+ if (!await this.tryResolve(tabId)) return;
10290
+ }
10274
10291
  try {
10275
10292
  await this.writeRow(s, Date.now());
10276
10293
  } catch (err) {
10277
10294
  log$2(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
10278
10295
  setTimeout(() => {
10279
10296
  const live = this.sessions.get(tabId);
10280
- if (!live || live.ended) return;
10297
+ if (!live || live.ended || !live.blockId || !live.rowId) return;
10281
10298
  this.writeRow(live, Date.now()).catch((err2) => {
10282
10299
  log$2(`checkpoint retry failed: ${err2?.message || err2}`);
10283
10300
  });
@@ -10285,6 +10302,7 @@ class TimeTracker {
10285
10302
  }
10286
10303
  }
10287
10304
  async writeRow(s, _endTimeMs) {
10305
+ if (!s.blockId || !s.rowId) return;
10288
10306
  const activeMin = Math.round(s.activeMs / 6e4);
10289
10307
  await this.api.updateRow(s.blockId, s.rowId, {
10290
10308
  "Active Time": activeMin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.3.5",
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,
@@ -135,12 +135,6 @@ export class WorkerWsClient {
135
135
  }
136
136
 
137
137
  sendChatLog(entry: { type: string; content: string; ts?: string }): void {
138
- log('[worker-ws] chat_log DEBUG', {
139
- type: entry.type,
140
- chars: entry.content.length,
141
- lines: entry.content.split('\n').length,
142
- preview: entry.content.slice(0, 500),
143
- })
144
138
  this.send({ type: 'chat_log', entry })
145
139
  }
146
140