@rallycry/conveyor-agent 10.0.6 → 10.2.0

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _project_shared from '@project/shared';
2
- import { AgentSessionServiceMethods, AgentMode, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
2
+ import { RunnerMode, AgentSessionServiceMethods, AgentMode, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
3
3
  export * from '@project/shared';
4
4
  import { ChildProcess } from 'node:child_process';
5
5
 
@@ -7,7 +7,7 @@ interface AgentConnectionConfig {
7
7
  apiUrl: string;
8
8
  taskToken: string;
9
9
  sessionId: string;
10
- runnerMode?: "task" | "pm" | "code-review";
10
+ runnerMode?: RunnerMode;
11
11
  }
12
12
  interface IncomingMessage {
13
13
  content: string;
@@ -66,7 +66,17 @@ declare class AgentConnection {
66
66
  get connected(): boolean;
67
67
  private static readonly CALL_CONNECT_WAIT_MS;
68
68
  private static readonly CALL_ACK_TIMEOUT_MS;
69
+ private static readonly SOCKET_RECYCLE_BASE_MS;
70
+ private static readonly SOCKET_RECYCLE_JITTER_MS;
71
+ private static readonly SOCKET_RECYCLE_BUSY_POLL_MS;
72
+ private recycleTimer;
73
+ private pendingCalls;
69
74
  call<M extends keyof AgentSessionServiceMethods>(method: M, payload: AgentSessionServiceMethods[M]["payload"]): Promise<AgentSessionServiceMethods[M]["response"]>;
75
+ /** (Re)arm the recycle timer — called on every successful (re)connect. */
76
+ private scheduleSocketRecycle;
77
+ private clearSocketRecycle;
78
+ private armRecycleTimer;
79
+ private attemptSocketRecycle;
70
80
  /** Resolve once `socket` reports connected, or reject after `timeoutMs`. */
71
81
  private waitForConnected;
72
82
  /** Emit an RPC and resolve on ack, rejecting if no ack arrives in time. */
@@ -109,6 +119,13 @@ declare class AgentConnection {
109
119
  cols: number;
110
120
  rows: number;
111
121
  }): void;
122
+ /**
123
+ * Report that the interactive CLI process for this session has died and no
124
+ * respawn is imminent (fire-and-forget). The server clears the scrollback
125
+ * ring and broadcasts pty:ended so clients hide the Connected-TUI tab. Old
126
+ * servers that don't know the method reject harmlessly.
127
+ */
128
+ sendPtyEnded(): void;
112
129
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
113
130
  onPtyInput(handler: (data: string) => void): () => void;
114
131
  /** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
@@ -210,7 +227,7 @@ declare class ModeController {
210
227
  private _pendingModeRestart;
211
228
  private _runnerMode;
212
229
  private _isAuto;
213
- constructor(initialMode: AgentMode, runnerMode?: "task" | "pm" | "code-review", isAuto?: boolean);
230
+ constructor(initialMode: AgentMode, runnerMode?: RunnerMode, isAuto?: boolean);
214
231
  get mode(): AgentMode;
215
232
  get isAuto(): boolean;
216
233
  get hasExitedPlanMode(): boolean;
@@ -231,7 +248,21 @@ declare class ModeController {
231
248
  applyServerMode(agentMode: AgentMode | null | undefined, isAuto: boolean | undefined): void;
232
249
  /** Resolve the initial mode based on task context */
233
250
  resolveInitialMode(context: ModeTaskContext): AgentMode;
234
- /** Check if auto-mode can bypass planning (task already has plan + properties) */
251
+ /**
252
+ * A FRESH auto task runs the read-only plan turn first (`--permission-mode plan`):
253
+ * resolveInitialMode leaves it in `auto` with `_hasExitedPlanMode = false`, and
254
+ * once the agent finishes it calls ExitPlanMode → building. Only tasks that have
255
+ * already progressed past the fresh stage bypass planning and build immediately
256
+ * with `--dangerously-skip-permissions`:
257
+ *
258
+ * - a build already underway (status past `Planning`/`Open`),
259
+ * - a PR already open (safety net if status lags),
260
+ * - a release (booted `InProgress`), or
261
+ * - a pod restart mid-build (DB `agentMode` stays `"auto"` but status advanced).
262
+ *
263
+ * Runtime Build-after-Discovery never reaches this gate as `auto` — the server's
264
+ * `resolveModeToSend` rewrites it to `"building"` for any non-`Planning` task.
265
+ */
235
266
  canBypassPlanning(context: ModeTaskContext): boolean;
236
267
  /** Handle mode change from server */
237
268
  handleModeChange(newMode: AgentMode): ModeAction;
@@ -260,6 +291,11 @@ interface LifecycleConfig {
260
291
  * persistent state (local, workspace, GitHub Codespaces) that don't lose
261
292
  * in-flight work on a crash. */
262
293
  gitFlushIntervalMs: number;
294
+ /** Claude key usage sample interval (default: 5 min). Polls the Anthropic
295
+ * OAuth usage endpoint and reports session/weekly rate-limit utilization for
296
+ * the running subscription key. Deliberately NOT the heartbeat cadence — the
297
+ * endpoint is rate-limited. Set to `0` to disable the timer entirely. */
298
+ usageSampleIntervalMs: number;
263
299
  }
264
300
  interface LifecycleCallbacks {
265
301
  onHeartbeat: () => void;
@@ -267,6 +303,7 @@ interface LifecycleCallbacks {
267
303
  onDormantTimeout: () => void;
268
304
  onTokenRefresh: () => void;
269
305
  onGitFlush: () => void;
306
+ onUsageSample: () => void;
270
307
  }
271
308
  declare class Lifecycle {
272
309
  readonly config: LifecycleConfig;
@@ -277,6 +314,7 @@ declare class Lifecycle {
277
314
  private idleCheckInterval;
278
315
  private dormantTimer;
279
316
  private gitFlushTimer;
317
+ private usageSampleTimer;
280
318
  constructor(config: LifecycleConfig, callbacks: LifecycleCallbacks);
281
319
  startHeartbeat(): void;
282
320
  stopHeartbeat(): void;
@@ -284,6 +322,8 @@ declare class Lifecycle {
284
322
  stopTokenRefresh(): void;
285
323
  startGitFlush(): void;
286
324
  stopGitFlush(): void;
325
+ startUsageSample(): void;
326
+ stopUsageSample(): void;
287
327
  startIdleTimer(): void;
288
328
  cancelIdleTimer(): void;
289
329
  /** Start (or restart) the dormant timer.
@@ -302,7 +342,7 @@ declare class Lifecycle {
302
342
  interface SessionRunnerConfig {
303
343
  connection: AgentConnectionConfig;
304
344
  agentMode?: AgentMode;
305
- runnerMode?: "task" | "pm" | "code-review";
345
+ runnerMode?: RunnerMode;
306
346
  isAuto?: boolean;
307
347
  workspaceDir: string;
308
348
  model?: string;
@@ -415,11 +455,25 @@ declare class SessionRunner {
415
455
  injectMessage(msg: IncomingMessage): void;
416
456
  /** Run queryBridge.execute, swallowing abort errors from stop/softStop. */
417
457
  private executeQuery;
458
+ /**
459
+ * A human typed into the parked, idle Connected-TUI (keep-alive PTY). Wake the
460
+ * core loop with a sentinel so it drains a passive turn. No-op once stopped.
461
+ */
462
+ private onPassiveTuiActivity;
463
+ /** Run queryBridge.executePassive, swallowing abort errors from stop/softStop. */
464
+ private executePassive;
418
465
  /** Periodic best-effort WIP commit + push during normal agent execution.
419
466
  * Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
420
467
  * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
421
468
  * clean tree. Guarded so two ticks can't overlap. Never throws. */
422
469
  private periodicGitFlush;
470
+ /** Sample the running Claude subscription key's rate-limit utilization from the
471
+ * Anthropic OAuth usage endpoint and report it as `rate_limit_update` events.
472
+ * The API (`persistRateLimitSnapshot`) attributes them to the key this session
473
+ * launched under, keeping User Settings + the PtY-tab usage widget fresh and
474
+ * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
475
+ * pod has no OAuth token (e.g. API-key projects). */
476
+ private sampleAndReportKeyUsage;
423
477
  /** PUT the WorkPreservation snapshot tar to the bundle's capability URL
424
478
  * when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
425
479
  private uploadGcsSnapshotIfConfigured;
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  unshallowRepo,
22
22
  updateRemoteToken,
23
23
  uploadSnapshotToGcs
24
- } from "./chunk-37ZIDX6S.js";
24
+ } from "./chunk-EDEMOFUN.js";
25
25
 
26
26
  // src/runner/worktree.ts
27
27
  import { execSync } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "10.0.6",
3
+ "version": "10.2.0",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -217,10 +217,16 @@ if [ "${SESSION_MODE}" = "review" ]; then
217
217
  # branch), so a role check can't identify them — gating on role=reader here
218
218
  # is what let review pods fall through to the default task runner and boot
219
219
  # the parent's task agent in discovery mode. The agent CLI's RunnerMode is
220
- # spelled "code-review" (cli.ts validates task|pm|code-review); the bare
220
+ # spelled "code-review" (cli.ts validates task|pm|code-review|adhoc); the bare
221
221
  # "review" is the agentMode/tag axis, NOT a runner mode — passing it here
222
222
  # made the agent exit "Invalid CONVEYOR_MODE" and crash-loop every 10s.
223
223
  export CONVEYOR_MODE="code-review"
224
+ elif [ "${SESSION_MODE}" = "adhoc" ]; then
225
+ # Task-less USER SCRATCH pod (Sessions view): the agent runs an interactive
226
+ # `claude` TUI relayed to the web terminal — no autonomous loop, no task. Must
227
+ # be checked BEFORE the project branch below: an adhoc session is also
228
+ # task-less with a projectId claim, but it must NOT boot the pm project runner.
229
+ export CONVEYOR_MODE="adhoc"
224
230
  elif [ -z "${CONVEYOR_TASK_ID:-}" ] && [ -n "${CONVEYOR_PROJECT_ID_CLAIM}" ]; then
225
231
  # Task-less project pod: the agent boots the project runner in pm mode
226
232
  # (see conveyor-agent setup/project-identity.ts — pm is required).
@@ -352,20 +358,21 @@ fi
352
358
  GIT_READY_MARKER="/workspaces/.conveyor-git-ready"
353
359
  GIT_FAILED_MARKER="/workspaces/.conveyor-git-failed"
354
360
 
355
- clean_repo_before_assignment_checkout() {
356
- # The baked/pooled repo is not user-owned until this git gate succeeds. Clean
357
- # stale image-generated dirt so assignment checkout can move to the requested
358
- # branch/ref instead of being blocked by tracked files like dependency stamps.
361
+ reset_tracked_repo_changes_before_assignment_checkout() {
362
+ # The baked/pooled repo is not user-owned until this git gate succeeds. Reset
363
+ # stale tracked image-generated dirt so assignment checkout can move to the
364
+ # requested branch/ref instead of being blocked by files like dependency
365
+ # stamps. Do not `git clean`: untracked prebake artifacts may be intentional.
366
+ # The assignment checkouts additionally pass -f: an UNTRACKED bake artifact
367
+ # can collide with a path the TARGET ref tracks (e.g. a dependency stamp
368
+ # committed on an older PR branch), which this reset cannot clear — checkout
369
+ # then refuses with "untracked working tree files would be overwritten".
370
+ # Forcing is safe here for the same not-user-owned reason.
359
371
  if ! _reset_out=$(git -C repo reset --hard HEAD 2>&1); then
360
372
  echo "[pool] ERROR: failed to clean tracked repo changes before checkout: ${_reset_out}" >&2
361
373
  printf '%s' "pre-checkout reset failed" > "$GIT_FAILED_MARKER"
362
374
  return 1
363
375
  fi
364
- if ! _clean_out=$(git -C repo clean -ffd 2>&1); then
365
- echo "[pool] ERROR: failed to clean untracked repo files before checkout: ${_clean_out}" >&2
366
- printf '%s' "pre-checkout clean failed" > "$GIT_FAILED_MARKER"
367
- return 1
368
- fi
369
376
  return 0
370
377
  }
371
378
 
@@ -391,7 +398,7 @@ sync_task_branch_to_repo() {
391
398
  echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
392
399
  fi
393
400
 
394
- if ! clean_repo_before_assignment_checkout; then
401
+ if ! reset_tracked_repo_changes_before_assignment_checkout; then
395
402
  return 1
396
403
  fi
397
404
 
@@ -402,7 +409,7 @@ sync_task_branch_to_repo() {
402
409
  printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
403
410
  return 1
404
411
  fi
405
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
412
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
406
413
  echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
407
414
  printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
408
415
  return 1
@@ -417,7 +424,7 @@ sync_task_branch_to_repo() {
417
424
  if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
418
425
  echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
419
426
  # Create the branch locally from the base branch
420
- if ! _create_out=$(git -C repo checkout -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
427
+ if ! _create_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
421
428
  echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
422
429
  printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
423
430
  return 1
@@ -439,7 +446,7 @@ sync_task_branch_to_repo() {
439
446
  # Use `checkout -B` to create or reset the local branch tracking origin.
440
447
  # This leaves HEAD attached to a real branch (not detached) so `git status`,
441
448
  # `git push`, and any tool that keys off branch state work correctly.
442
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
449
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
443
450
  echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
444
451
  printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
445
452
  return 1
@@ -512,7 +519,7 @@ prepare_workspace_git() {
512
519
  printf '%s' "post-assignment fetch of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
513
520
  return 1
514
521
  fi
515
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
522
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
516
523
  echo "[pool] ERROR: post-assignment checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
517
524
  printf '%s' "post-assignment checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
518
525
  return 1