@assethub/cli 0.1.36 → 0.1.38

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.
@@ -42,9 +42,9 @@ export const sessionSaveDisclosure = (scope, client = 'claude') => {
42
42
  ? `\`assethub hooks uninstall --client ${client}\` from that folder`
43
43
  : `\`assethub hooks uninstall --client ${client} --global\``;
44
44
  return (`Session saving adds SessionStart, Stop, PreCompact and SessionEnd hooks to ${where} ` +
45
- 'is then copied to ~/.assethub/sessions/ when it ends ' +
45
+ 'is then copied to ~/.assethub/sessions/ while you work and when it ends ' +
46
46
  '(the conversation only: your prompts, the agent\'s replies and tool calls, with recognised secrets masked, readable only by you; copies older than 30 days are deleted), ' +
47
- 'and uploaded to AssetHub as a coding-agent session only you can read (internal accounts only for now): the masked transcript, ' +
47
+ 'and uploaded to AssetHub, every 8 minutes while you work (ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN) and when it ends, as a coding-agent session only you can read (internal accounts only for now): the masked transcript, ' +
48
48
  'your prompts, the agent\'s replies, its tool calls and images from the project. Uploads that fail are retried in the background ' +
49
49
  'when a later session starts or ends. Secret masking is best effort. ' +
50
50
  'Opt a project out with .assethub/no-session-save, or turn it off with ASSETHUB_SESSION_SAVE=off / ASSETHUB_SESSION_UPLOAD=off; ' +
@@ -56,6 +56,14 @@ export declare const SWEEP_LIMIT = 3;
56
56
  * only be uploaded once abandoned (ABANDONED_AFTER_MS quiet, then a new session).
57
57
  */
58
58
  export declare const WORKING_UPLOAD_INTERVAL_MS: number;
59
+ /** The shortest interval ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN may set. */
60
+ export declare const MIN_WORKING_UPLOAD_INTERVAL_MS: number;
61
+ /**
62
+ * ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN, in minutes, else WORKING_UPLOAD_INTERVAL_MS.
63
+ * Each upload sends a new snapshot of the whole transcript, so it is never
64
+ * shorter than a minute; a value that is not a positive number is ignored.
65
+ */
66
+ export declare const workingUploadIntervalMs: (env: NodeJS.ProcessEnv) => number;
59
67
  /**
60
68
  * Copy + redact the transcript Claude Code keeps at `meta.transcriptPath`, and
61
69
  * collect its images. Runs once per SessionEnd and before an upload, never per
@@ -1 +1 @@
1
- {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/hooks/save.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAC,UAAU,EAAE,SAAS,EAAgB,WAAW,EAAC,MAAM,YAAY,CAAA;AAChF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,aAAa,CAAA;AAE9C,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,eAAe,QAAmB,CAAA;AAC/C,eAAO,MAAM,sBAAsB,KAAK,CAAA;AASxC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5D,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACpE,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;CACjC,CAAA;AAYD,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,KAAG,IACgC,CAAA;AAE3G,qEAAqE;AACrE,eAAO,MAAM,0BAA0B,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,KAAG,IACP,CAAA;AAE3E,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,IAC+B,CAAA;AAErF,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,CAAC,UAAU,KAAG,MAAM,GAAG,IAM/D,CAAA;AAED,4FAA4F;AAC5F,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,KAAK,IAAI,EACT,MAAM,MAAM,EACZ,MAAM,MAAM,KACX,OAAO,CAAC,MAAM,CAwBhB,CAAA;AAgFD,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,QAAqB,CAAA;AACpD,wEAAwE;AACxE,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,QAAiB,CAAA;AAMxD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,MAAM,EAClB,UAAS;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAM,KAC/D,OAAO,CAAC,WAAW,GAAG,IAAI,CA6C5B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,KAAK,IAAI,EACT,mBAAmB,MAAM,KACxB,OAAO,CAAC,MAAM,CAmBhB,CAAA;AAED,eAAO,MAAM,WAAW,GAAU,UAAS,kBAAuB,KAAG,OAAO,CAAC,iBAAiB,CAgG7F,CAAA"}
1
+ {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/hooks/save.ts"],"names":[],"mappings":"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"}
@@ -162,7 +162,24 @@ export const SWEEP_LIMIT = 3;
162
162
  * stay open for days and rarely reach SessionEnd, so without this they would
163
163
  * only be uploaded once abandoned (ABANDONED_AFTER_MS quiet, then a new session).
164
164
  */
165
- export const WORKING_UPLOAD_INTERVAL_MS = 20 * 60 * 1000;
165
+ export const WORKING_UPLOAD_INTERVAL_MS = 8 * 60 * 1000;
166
+ /** The shortest interval ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN may set. */
167
+ export const MIN_WORKING_UPLOAD_INTERVAL_MS = 60 * 1000;
168
+ /**
169
+ * ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN, in minutes, else WORKING_UPLOAD_INTERVAL_MS.
170
+ * Each upload sends a new snapshot of the whole transcript, so it is never
171
+ * shorter than a minute; a value that is not a positive number is ignored.
172
+ */
173
+ export const workingUploadIntervalMs = (env) => {
174
+ const raw = env.ASSETHUB_SESSION_UPLOAD_INTERVAL_MIN?.trim();
175
+ if (!raw)
176
+ return WORKING_UPLOAD_INTERVAL_MS;
177
+ const ms = Number(raw) * 60 * 1000;
178
+ // Finite after scaling too: a huge value would otherwise stop uploads for good.
179
+ if (!Number.isFinite(ms) || ms <= 0)
180
+ return WORKING_UPLOAD_INTERVAL_MS;
181
+ return Math.max(ms, MIN_WORKING_UPLOAD_INTERVAL_MS);
182
+ };
166
183
  const WORKING_UPLOAD_EVENTS = new Set(['Stop', 'PreCompact']);
167
184
  /** Events that copy and redact the transcript now; the rest only mark it changed. */
168
185
  const FULL_SAVE_EVENTS = new Set(['SessionEnd', 'Manual']);
@@ -323,11 +340,20 @@ export const saveSession = async (options = {}) => {
323
340
  const result = { sessionDir, event };
324
341
  if (uploadOn && options.startWorkingUpload && WORKING_UPLOAD_EVENTS.has(event)) {
325
342
  const latest = await readMeta(sessionDir);
326
- // Counts uploads any path started (an earlier Stop, SessionEnd, a sweep).
327
- const lastStart = Math.max(Date.parse(latest?.lastWorkingUploadAt ?? '') || 0, Date.parse(latest?.lastUploadAttemptAt ?? '') || 0);
328
- if (latest && now.getTime() - lastStart >= WORKING_UPLOAD_INTERVAL_MS) {
329
- // Stamped before spawning, so the next few turns do not start more.
330
- await updateMeta(sessionDir, latest, current => ({ ...current, lastWorkingUploadAt: now.toISOString() }));
343
+ const interval = workingUploadIntervalMs(env);
344
+ let claimed = false;
345
+ // Checked and stamped under the meta lock, before spawning, so neither the
346
+ // next few turns nor a hook running beside this one start another.
347
+ if (latest)
348
+ await updateMeta(sessionDir, latest, current => {
349
+ // Counts uploads any path started (an earlier Stop, SessionEnd, a sweep).
350
+ const lastStart = Math.max(Date.parse(current.lastWorkingUploadAt ?? '') || 0, Date.parse(current.lastUploadAttemptAt ?? '') || 0);
351
+ if (now.getTime() - lastStart < interval)
352
+ return current;
353
+ claimed = true;
354
+ return { ...current, lastWorkingUploadAt: now.toISOString() };
355
+ });
356
+ if (claimed) {
331
357
  options.startWorkingUpload(sessionDir, options.upload?.profile);
332
358
  result.uploadStarted = true;
333
359
  }
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/hooks/upload.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAc,aAAa,EAAC,MAAM,YAAY,CAAA;AAG1D,eAAO,MAAM,+BAA+B,kCAAkC,CAAA;AAQ9E,eAAO,MAAM,iBAAiB,QAAS,CAAA;AACvC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,QAAc,CAAA;AAC/C,uFAAuF;AACvF,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAE9C,MAAM,MAAM,UAAU,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAEhF,MAAM,MAAM,aAAa,GAAG;IAC1B,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC,KACvD,OAAO,CAAC,UAAU,GAAG,IAAI,CA2B3B,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,YAAY,GAAI,gBAAgB,MAAM,KAAG,MACmB,CAAA;AAmBzE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,UAAU,EAChB,YAAW,OAAO,KAAyC,KAC1D,OAAO,CAAC,OAAO,CAgBjB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,MAAM,EAClB,UAAS,aAAkB,KAC1B,OAAO,CAAC,aAAa,CAmBvB,CAAA;AAoHD;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,UAAS,aAAa,GAAG;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAM,KAC/D,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAC,EAAE,CAkBrD,CAAA"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/hooks/upload.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAc,aAAa,EAAC,MAAM,YAAY,CAAA;AAG1D,eAAO,MAAM,+BAA+B,kCAAkC,CAAA;AAc9E,eAAO,MAAM,iBAAiB,QAAS,CAAA;AACvC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,QAAc,CAAA;AAC/C,uFAAuF;AACvF,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAE9C,MAAM,MAAM,UAAU,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAEhF,MAAM,MAAM,aAAa,GAAG;IAC1B,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC,KACvD,OAAO,CAAC,UAAU,GAAG,IAAI,CA2B3B,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,YAAY,GAAI,gBAAgB,MAAM,KAAG,MACmB,CAAA;AAmBzE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,UAAU,EAChB,YAAW,OAAO,KAAyC,KAC1D,OAAO,CAAC,OAAO,CAgBjB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,MAAM,EAClB,UAAS,aAAkB,KAC1B,OAAO,CAAC,aAAa,CAmBvB,CAAA;AAqID;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,UAAS,aAAa,GAAG;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAM,KAC/D,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAC,EAAE,CAkBrD,CAAA"}
@@ -6,7 +6,7 @@
6
6
  import { readFile, utimes } from 'node:fs/promises';
7
7
  import { basename, join } from 'node:path';
8
8
  import { buildRunUploadPlan } from '../runsUpload/buildRunUpload.js';
9
- import { controlRunUploadUrls } from '../runsUpload/controlEndpoints.js';
9
+ import { CONTROL_ERROR_CODES, controlRunUploadUrls } from '../runsUpload/controlEndpoints.js';
10
10
  import { readGraphFolder } from '../runsUpload/graphFolder.js';
11
11
  import { uploadRun } from '../runsUpload/uploadRun.js';
12
12
  import { RunUploadError } from '../runsUpload/runUploadError.js';
@@ -23,6 +23,12 @@ const canvasIdOf = (value) => {
23
23
  return Number.isSafeInteger(id) && id > 0 ? id : null;
24
24
  };
25
25
  const DAY_MS = 24 * 60 * 60 * 1000;
26
+ /**
27
+ * Tries per upload to get past revisions the server already holds that this
28
+ * machine has no record of (its ~/.assethub was reset, or another copy of the
29
+ * session uploaded). Each try jumps twice as far, so 8 cover 255 revisions.
30
+ */
31
+ const MAX_REV_RECOVERIES = 8;
26
32
  export const UPLOAD_TIMEOUT_MS = 20_000;
27
33
  /** A session's upload lock older than this belongs to a crashed upload. */
28
34
  export const UPLOAD_LOCK_STALE_MS = 15 * 60_000;
@@ -171,6 +177,9 @@ const uploadSessionUnlocked = async (sessionDir, options) => {
171
177
  if (meta.dirty || meta.transcriptFormat !== TRANSCRIPT_FORMAT)
172
178
  meta = (await materializeSession(sessionDir, { env: options.env })) ?? meta;
173
179
  let level = meta.compactLevel ?? 0;
180
+ // The next rev is above this too: revs the server turned away in this upload.
181
+ let revFloor = -1;
182
+ let recoveries = 0;
174
183
  try {
175
184
  const baseFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
176
185
  const timeoutMs = options.timeoutMs ?? UPLOAD_TIMEOUT_MS;
@@ -182,7 +191,7 @@ const uploadSessionUnlocked = async (sessionDir, options) => {
182
191
  // been published, and re-sending its rev with new content is a permanent
183
192
  // conflict. The server accepts gaps, so skipping an unsent rev is harmless.
184
193
  const latest = (await readMeta(sessionDir)) ?? meta;
185
- const rev = Math.max(latest.uploadedRev ?? -1, latest.attemptedRev ?? -1) + 1;
194
+ const rev = Math.max(latest.uploadedRev ?? -1, latest.attemptedRev ?? -1, revFloor) + 1;
186
195
  let plan;
187
196
  try {
188
197
  const folder = await readGraphFolder(await buildSessionGraphFolder(sessionDir, { compactLevel: level }));
@@ -204,17 +213,34 @@ const uploadSessionUnlocked = async (sessionDir, options) => {
204
213
  throw error;
205
214
  }
206
215
  await record({ attemptedRev: rev, compactLevel: level });
207
- const result = await uploadRun({
208
- plan,
209
- baseUrl: auth.baseUrl,
210
- apiKey: auth.apiKey,
211
- workspaceId: auth.workspaceId,
212
- fetchImpl,
213
- urls: controlRunUploadUrls(auth.baseUrl, CODING_AGENT_SESSION_API_PREFIX),
214
- registrationExtra: {
215
- session: { client: meta.client, sessionId: meta.sessionId, canvasId: canvasIdOf(meta.canvasId) },
216
- },
217
- });
216
+ let result;
217
+ try {
218
+ result = await uploadRun({
219
+ plan,
220
+ baseUrl: auth.baseUrl,
221
+ apiKey: auth.apiKey,
222
+ workspaceId: auth.workspaceId,
223
+ fetchImpl,
224
+ urls: controlRunUploadUrls(auth.baseUrl, CODING_AGENT_SESSION_API_PREFIX),
225
+ registrationExtra: {
226
+ session: { client: meta.client, sessionId: meta.sessionId, canvasId: canvasIdOf(meta.canvasId) },
227
+ },
228
+ });
229
+ }
230
+ catch (error) {
231
+ if (!(error instanceof RunUploadError && error.code === CONTROL_ERROR_CODES.snapshotConflict))
232
+ throw error;
233
+ result = null;
234
+ }
235
+ // The server holds this rev with other content (conflict) or a newer one
236
+ // (stale), so this snapshot was not stored: jump past it and send again.
237
+ if (!result || result.status === 'stale') {
238
+ if (recoveries >= MAX_REV_RECOVERIES)
239
+ throw new Error(`The server already holds revision ${rev} or later of this session.`);
240
+ revFloor = rev + 2 ** recoveries - 1;
241
+ recoveries += 1;
242
+ continue;
243
+ }
218
244
  await record({
219
245
  status: 'uploaded',
220
246
  graphId: result.graphId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {