jg-agent-hq 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2
4
+
5
+ - Resume inactive managed Codex sessions retained as loaded by native unsubscribe, preserving explicit handoff and active/uncertain ownership guards.
6
+ - Qualify Claude transcript formats 2.1.296 and 2.1.299, preserve in-workspace working-directory changes, and refresh reader projections when their interpretation changes. Keep executable qualification separate and reject workspace/symlink escapes.
7
+ - Await native-fixture cleanup after test timeouts and give the full 1,024-milestone revision workflow a focused CI budget. Add a real child-runner timeout/isolation regression.
8
+
3
9
  ## 0.5.1
4
10
 
5
11
  - Qualify Claude Code 2.1.306 history alongside 2.1.304, including transcripts spanning both versions. Continue to reject unqualified CLI and transcript versions.
package/README.md CHANGED
@@ -41,7 +41,7 @@ The controller owns its SQLite worker and native connections independently of cl
41
41
 
42
42
  The current read contracts were exercised against Codex 0.163.0 and Claude Code versions 2.1.304 and 2.1.306 on Linux. Unsupported native responses fail explicitly. `jg-agent-hq/contracts` contains browser-safe schemas and types; importing it does not import the host runtime.
43
43
 
44
- Use `--provider claude` for Claude history and usage; the default is `codex`. Claude connects lazily through its native structured CLI, with normal settings enabled and no user prompt. Usage requests explicitly disable the optional transcript-behavior scan. Its local history reader accepts only the qualified 2.1.304 and 2.1.306 versions, including transcripts containing both, keeps byte locations rather than conversation bodies in SQLite, and reports unsupported history as unavailable. File history cannot establish current execution state. See the [reader limits and freshness contract](api/README.md#claude-history).
44
+ Use `--provider claude` for Claude history and usage; the default is `codex`. Claude connects lazily through its native structured CLI, with normal settings enabled and no user prompt. Usage requests explicitly disable the optional transcript-behavior scan. Its local history reader accepts only qualified transcript versions 2.1.296, 2.1.299, 2.1.304 and 2.1.306, including transcripts spanning these versions, keeps byte locations rather than conversation bodies in SQLite, and reports unsupported history as unavailable. File history cannot establish current execution state. See the [reader limits and freshness contract](api/README.md#claude-history).
45
45
 
46
46
  Explicit actions create, name, adopt, resume and close threads; send idle messages; steer an exact running turn; queue the next message; interrupt; and answer native questions or permissions. Actions have stable UUIDs and durable receipts. Repeating the same request returns its receipt; changing that UUID's payload conflicts. A lost acknowledgement stays uncertain until the exact native response arrives or the operator makes an explicit recovery choice. HQ never resends uncertain input.
47
47
 
@@ -105,4 +105,6 @@ Tests consume built public exports. Unit tests cover application-owned decisions
105
105
 
106
106
  The website's README and `docs/website.md` define its build-once check composition, category-specific scripts and owned preview/browser lifecycle. Root and website checks must both pass for task completion when workspace configuration or canonical site content changes.
107
107
 
108
+ The runtime fixture teardown is registered with the test runner as well as the callback, so a test timeout cannot restore environment variables during the next test.
109
+
108
110
  Current local checks establish the tested host only. Release qualification must include the Node engines floor and current LTS; macOS operation requires its own runtime evidence.
package/api/README.md CHANGED
@@ -48,11 +48,11 @@ Threads expose native state, `historyState` (`available` or `unavailable`), owne
48
48
 
49
49
  Claude connects on its first read through `claude --print --input-format stream-json --output-format stream-json --verbose`. No user prompt, bare mode, permission bypass or SDK dependency is used. Runtime status reports this adapter as `idle`, `ready` or `unavailable`, with its observed CLI version. An unavailable native lane requires restarting the host after resolving the native problem.
50
50
 
51
- The qualified CLI interface has no session-history listing operation. The Claude-owned reader therefore uses direct UUID JSONL files in the native project's directory beneath `CLAUDE_CONFIG_DIR`, or the normal `.claude` directory. The CLI and each versioned transcript record must independently match a qualified version: 2.1.304 or 2.1.306. A transcript may contain both qualified versions. Other formats, encoded project keys longer than 200 characters, directory/file links, malformed records and records larger than 1 MiB report unavailable. Discovery never recurses into subdirectories, including excluded directories. Unsupported discovered sessions have `historyState: unavailable`; they do not prevent supported sessions from being listed.
51
+ The qualified CLI interface has no session-history listing operation. The Claude-owned reader therefore uses direct UUID JSONL files in the native project's directory beneath `CLAUDE_CONFIG_DIR`, or the normal `.claude` directory. The running CLI must match 2.1.304 or 2.1.306. Transcript records may independently use the qualified formats 2.1.296, 2.1.299, 2.1.304 and 2.1.306, including multiple qualified formats within one session. Qualifying a stored record format does not qualify that executable version. Other formats, encoded project keys longer than 200 characters, directory/file links, malformed records and records larger than 1 MiB report unavailable. Discovery never recurses into subdirectories, including excluded directories. Unsupported discovered sessions have `historyState: unavailable`; they do not prevent supported sessions from being listed.
52
52
 
53
- Discovery verifies the workspace recorded inside each transcript before returning its identity or title. A matching native directory name alone is insufficient because punctuation replacement can produce colliding directory keys. Foreign workspaces are omitted. If a file's workspace cannot be established, discovery reports unavailable rather than inventing an empty result.
53
+ Discovery verifies the workspace recorded inside each transcript before returning its identity or title. A matching native directory name alone is insufficient because punctuation replacement can produce colliding directory keys. Native working directories may be the selected workspace or its subdirectories, including removed subdirectories. The nearest existing path ancestor must resolve inside the workspace without excluded segments; sibling paths and symlink escapes are rejected. Foreign workspaces are omitted. If a file's workspace cannot be established, discovery reports unavailable rather than inventing an empty result.
54
54
 
55
- The existing SQLite worker incrementally records file identity, complete-line offsets, message identities and parent links. It does not save message bodies. Each worker batch inspects at most 128 directory entries and reads at most 4 MiB of new transcript bytes, one file at a time. The adapter continues batches with a host-event-loop yield between them, within a 30-second operation budget. If that budget is reached, the read reports unavailable and subsequent reads continue indexing. At most four incomplete discovery iterators remain open. Shutdown closes them. Discovery continuations use that projection without starting another directory scan; start a fresh first page to refresh discovery. Removed files are discarded from the projection after a complete refresh. No additional cache infrastructure or browser cache is used.
55
+ The existing SQLite worker incrementally records file identity, complete-line offsets, message identities and parent links. It does not save message bodies. File identity includes the reader projection revision and qualified record formats, so a reader capability change refreshes rejected or obsolete projections from native files. Each worker batch inspects at most 128 directory entries and reads at most 4 MiB of new transcript bytes, one file at a time. The adapter continues batches with a host-event-loop yield between them, within a 30-second operation budget. If that budget is reached, the read reports unavailable and subsequent reads continue indexing. At most four incomplete discovery iterators remain open. Shutdown closes them. Discovery continuations use that projection without starting another directory scan; start a fresh first page to refresh discovery. Removed files are discarded from the projection after a complete refresh. No additional cache infrastructure or browser cache is used.
56
56
 
57
57
  Current-chain membership and turn starts are indexed as metadata. Ordinary item and turn continuations seek those indexes; they do not walk the full chain per page. A changed branch rebuilds membership before exposing that revision. Repeated native message identities can replace content while retaining their parent; a changed parent for the same identity is an unsupported format.
58
58
 
@@ -84,6 +84,8 @@ Message and queue input is a closed union: `{ kind: 'text', text }` or `{ kind:
84
84
 
85
85
  Names use lowercase ASCII letters, digits and hyphens, start with a letter or digit, and have at most 32 characters. Names are unique without regard to case in one state directory. Creation and adoption resolve collisions atomically with ordinal suffixes such as `build-usage-2`, truncating the base to keep the complete name within 32 characters. Explicit rename rejects an occupied or pending name. Provider/native-session pairs remain unique. At most 32 native sessions are controlled concurrently; close idle sessions to release capacity. Native discovery remains independent of that limit. Confirm that other native CLI controllers are closed before adoption, resume or recovery. An unloaded native status cannot prove another CLI has stopped. HQ provides exclusive coordination within one canonical state directory; it does not impose a global lock on independent CLIs. Recovery is permitted only after the producing runtime has stopped and its ownership evidence has been inspected.
86
86
 
87
+ Codex may keep an idle native thread loaded after `thread/unsubscribe`. An explicitly confirmed resume may reacquire that thread only when its known HQ control is inactive and managed. Active native threads, uncertain HQ ownership and already controlled idle sessions remain blocked. Other adoption and recovery checks are unchanged.
88
+
87
89
  Receipts separate `delivery` (`queued`, `submitted`, `acknowledged`, `uncertain`, `failed`, `canceled`) from nullable native turn `completion` (`completed`, `failed`, `interrupted`). Acknowledgement means the exact native request was accepted, not that the turn or tool completed. Permission replies remain submitted after a pipe write and become acknowledged only when Codex resolves the exact native request or Claude returns a tool-result block for the exact tool-use identity. Claude cancellation expires the prompt and leaves an unacknowledged written reply uncertain. Resolution does not prove a tool ran. The receipt includes HQ/native identity, request phase, digest, timestamps, safe failure classification and optional related action. Only selected receipts expose undelivered `queuedText` and `queuedSkill`; collection reads omit both. Queued native skill selections contain a native name and nullable source path, never skill content. Both are cleared after delivery acknowledgement or cancellation.
88
90
 
89
91
  A missing acknowledgement never triggers an automatic resend. Exact late responses reconcile delivery, including completion that arrived first. Native output loss marks the affected provider controls and submitted work uncertain. A Claude process failure affects its own session; a Codex transport failure affects its Codex cohort. Restart quarantines controls from an interrupted runtime; explicit recovery preserves the HQ identity and name. Successful shutdown proves native process-group exit before marking controls inactive. Unknown creation without a native identity remains uncertain.
@@ -100,7 +102,7 @@ Prompts expose `replyActionId` as soon as a reply is admitted, so reconnecting c
100
102
 
101
103
  ### Claude native control
102
104
 
103
- Each controlled session has one persistent native `claude` process in the authorized workspace. HQ adds structured input/output, replayed user-message acknowledgements, partial-message output and `--permission-prompt-tool=stdio`. It does not set a model, replace instructions/settings, disable session persistence, bypass permissions or enable a bare mode. Native skills remain available. Control was exercised against CLI 2.1.304. Native idle initialization, session identity and matching acknowledgements establish capability; a changed version label alone does not disable compatible controls. File history retains its separate qualified-version check.
105
+ Each controlled session has one persistent native `claude` process in the authorized workspace. HQ adds structured input/output, replayed user-message acknowledgements, partial-message output and `--permission-prompt-tool=stdio`. It does not set a model, replace instructions/settings, disable session persistence, bypass permissions or enable a bare mode. Native skills remain available. Control was exercised against CLI 2.1.304 and 2.1.306. Native idle initialization, session identity and matching acknowledgements establish capability; a changed version label alone does not disable compatible controls. File history retains its separate qualified-version check.
104
106
 
105
107
  Creation chooses a native UUID without inference, then names that exact session. Resume uses native `--resume` and verifies the session-id fence on the name control before marking the session idle. The operator's handoff confirmation establishes that another CLI controller is closed; file timestamps and file-backed unknown states do not establish this. Independently running CLIs are outside HQ's state-directory coordination boundary.
106
108
 
@@ -690,9 +690,15 @@ export const createActions = (native, claude, readClaudeThread, store, roots, ep
690
690
  const target = provider === 'codex'
691
691
  ? await native.readThread(nativeId)
692
692
  : await readClaudeThread(nativeId, thread.workspaceRoot);
693
- if ((provider === 'codex'
694
- ? target.nativeState !== 'notLoaded'
695
- : target.historyState !== 'available') ||
693
+ // native unsubscribe may retain an idle thread after its managed control is closed
694
+ const canResume = provider === 'codex'
695
+ ? target.nativeState === 'notLoaded' ||
696
+ (command.kind === 'thread-resume' &&
697
+ thread.state === 'inactive' &&
698
+ thread.ownership === 'managed' &&
699
+ target.nativeState === 'idle')
700
+ : target.historyState === 'available';
701
+ if (!canResume ||
696
702
  (await resolveWorkspaceTarget(target.workspaceRoot, roots)) !== thread.workspaceRoot)
697
703
  conflict();
698
704
  prepared.nativeId = nativeId;
@@ -2,7 +2,7 @@ import { Exception } from 'error-message-utils';
2
2
  import { scheduler } from 'node:timers/promises';
3
3
  import { z } from 'zod';
4
4
  import { createClaudeRpc } from './rpc.js';
5
- import { ClaudeHistoryVersionSchema, ClaudeUsageSchema } from './types.js';
5
+ import { ClaudeHistoryCliVersionSchema, ClaudeUsageSchema } from './types.js';
6
6
  import { toClaudeUsageSnapshot } from './transformers.js';
7
7
  /**
8
8
  * Lazily connects read-only Claude capabilities; Codex needs no Claude installation.
@@ -61,7 +61,7 @@ export const createClaudeProvider = (store) => {
61
61
  async readHistory(command) {
62
62
  const deadline = Date.now() + 30_000;
63
63
  const native = await open();
64
- if (!ClaudeHistoryVersionSchema.safeParse(native.version).success)
64
+ if (!ClaudeHistoryCliVersionSchema.safeParse(native.version).success)
65
65
  throw new Exception('The Claude history is unavailable', undefined, {
66
66
  kind: 'agent-unavailable',
67
67
  });
@@ -4,9 +4,11 @@ import { Exception, extractMessage, hasErrorCode } from 'error-message-utils';
4
4
  import { z } from 'zod';
5
5
  import { HqResponseSchema, } from '../../../contracts/index.js';
6
6
  import { decodeHistoryCursor, encodeHistoryCursor, } from '../../../sessions/index.js';
7
- import { ClaudeHistoryVersionSchema } from '../types.js';
7
+ import { ClaudeHistoryRecordVersionSchema } from '../types.js';
8
8
  import { EntryRowSchema, EntrySeekSchema, FileRowSchema, RecordSchema, SeekSchema, } from './types.js';
9
- import { openTranscript, projectDirectory, toClaudeItem } from './utilities.js';
9
+ import { isClaudeWorkspaceCwd, openTranscript, projectDirectory, toClaudeItem, } from './utilities.js';
10
+ // identifies the path and record interpretation used by persisted projection entries
11
+ const HISTORY_PROJECTION_VERSION = 1;
10
12
  const MAX_RECORD_BYTES = 1_048_576;
11
13
  const MAX_REFRESH_BYTES = 4_194_304;
12
14
  const CHAIN = `WITH RECURSIVE current_chain AS (
@@ -189,8 +191,8 @@ export class ClaudeHistoryModel {
189
191
  this.__unavailable();
190
192
  const record = RecordSchema.parse(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
191
193
  if (record.uuid !== entry.uuid ||
192
- !ClaudeHistoryVersionSchema.safeParse(record.version).success ||
193
- record.cwd !== root)
194
+ !ClaudeHistoryRecordVersionSchema.safeParse(record.version).success ||
195
+ !isClaudeWorkspaceCwd(record.cwd, root))
194
196
  this.__unavailable();
195
197
  const item = toClaudeItem(record, entry.turn_id);
196
198
  const itemSize = Buffer.byteLength(JSON.stringify(item));
@@ -251,7 +253,7 @@ export class ClaudeHistoryModel {
251
253
  this.__unavailable();
252
254
  const bytes = Buffer.alloc(Math.min(stat.size, 256));
253
255
  readSync(fd, bytes, 0, bytes.length, 0);
254
- return `${stat.dev}:${stat.ino}:${stat.birthtimeMs}:${createHash('sha256').update(bytes).digest('hex')}`;
256
+ return `${HISTORY_PROJECTION_VERSION}:${ClaudeHistoryRecordVersionSchema.options.join(',')}:${stat.dev}:${stat.ino}:${stat.birthtimeMs}:${createHash('sha256').update(bytes).digest('hex')}`;
255
257
  }
256
258
  __refresh(directory, root) {
257
259
  let scan = this.__scans.get(root);
@@ -391,7 +393,7 @@ export class ClaudeHistoryModel {
391
393
  __record(root, nativeId, row, bytes, offset) {
392
394
  const record = RecordSchema.parse(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
393
395
  if (record.cwd !== undefined) {
394
- if (record.cwd !== root || record.sessionId !== nativeId) {
396
+ if (!isClaudeWorkspaceCwd(record.cwd, root) || record.sessionId !== nativeId) {
395
397
  row.workspace_state = 'foreign';
396
398
  this.__unavailable();
397
399
  }
@@ -404,16 +406,16 @@ export class ClaudeHistoryModel {
404
406
  return;
405
407
  }
406
408
  if (record.version !== undefined &&
407
- !ClaudeHistoryVersionSchema.safeParse(record.version).success)
409
+ !ClaudeHistoryRecordVersionSchema.safeParse(record.version).success)
408
410
  this.__unavailable();
409
411
  if (record.isSidechain || !record.uuid)
410
412
  return;
411
413
  if ((record.type === 'user' || record.type === 'assistant') &&
412
414
  record.message?.role !== record.type)
413
415
  this.__unavailable();
414
- if (!ClaudeHistoryVersionSchema.safeParse(record.version).success ||
416
+ if (!ClaudeHistoryRecordVersionSchema.safeParse(record.version).success ||
415
417
  record.sessionId !== nativeId ||
416
- record.cwd !== root ||
418
+ !isClaudeWorkspaceCwd(record.cwd, root) ||
417
419
  record.timestamp === undefined ||
418
420
  !Number.isFinite(Date.parse(record.timestamp)))
419
421
  this.__unavailable();
@@ -7,6 +7,11 @@ import type { IRecord } from './types.js';
7
7
  * - -1: The Claude history is unavailable
8
8
  */
9
9
  export declare const projectDirectory: (workspaceRoot: string) => string;
10
+ /**
11
+ * Verifies a native cwd within its selected workspace, including removed subdirectories.
12
+ * @returns Whether existing path ancestors remain inside the workspace without excluded segments.
13
+ */
14
+ export declare const isClaudeWorkspaceCwd: (cwd: string | undefined, workspaceRoot: string) => boolean;
10
15
  /**
11
16
  * Opens only one direct native UUID transcript, refusing links and traversal.
12
17
  * @returns The descriptor, which its caller must close.
@@ -1,7 +1,7 @@
1
1
  import { constants, lstatSync, openSync, realpathSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
- import { Exception } from 'error-message-utils';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { Exception, hasErrorCode } from 'error-message-utils';
5
5
  import { z } from 'zod';
6
6
  const EXCLUDED = new Set(['_archive', '_archives', '_backup', '_backups']);
7
7
  /**
@@ -37,6 +37,39 @@ export const projectDirectory = (workspaceRoot) => {
37
37
  });
38
38
  return directory;
39
39
  };
40
+ /**
41
+ * Verifies a native cwd within its selected workspace, including removed subdirectories.
42
+ * @returns Whether existing path ancestors remain inside the workspace without excluded segments.
43
+ */
44
+ export const isClaudeWorkspaceCwd = (cwd, workspaceRoot) => {
45
+ if (cwd === workspaceRoot)
46
+ return true;
47
+ if (!cwd ||
48
+ cwd.length > 4096 ||
49
+ !isAbsolute(cwd) ||
50
+ resolve(cwd) !== cwd ||
51
+ cwd.split(sep).some((part) => EXCLUDED.has(part)))
52
+ return false;
53
+ const child = relative(workspaceRoot, cwd);
54
+ if (isAbsolute(child) || child === '..' || child.startsWith(`..${sep}`))
55
+ return false;
56
+ let ancestor = cwd;
57
+ while (ancestor !== workspaceRoot) {
58
+ try {
59
+ const canonical = realpathSync(ancestor);
60
+ if (canonical.split(sep).some((part) => EXCLUDED.has(part)))
61
+ return false;
62
+ const resolved = relative(workspaceRoot, canonical);
63
+ return !isAbsolute(resolved) && resolved !== '..' && !resolved.startsWith(`..${sep}`);
64
+ }
65
+ catch (cause) {
66
+ if (!hasErrorCode(cause, 'ENOENT') && !hasErrorCode(cause, 'ENOTDIR'))
67
+ throw cause;
68
+ ancestor = dirname(ancestor);
69
+ }
70
+ }
71
+ return true;
72
+ };
40
73
  /**
41
74
  * Opens only one direct native UUID transcript, refusing links and traversal.
42
75
  * @returns The descriptor, which its caller must close.
@@ -1,8 +1,14 @@
1
1
  import { z } from 'zod';
2
- export declare const ClaudeHistoryVersionSchema: z.ZodEnum<{
2
+ export declare const ClaudeHistoryCliVersionSchema: z.ZodEnum<{
3
3
  "2.1.304": "2.1.304";
4
4
  "2.1.306": "2.1.306";
5
5
  }>;
6
+ export declare const ClaudeHistoryRecordVersionSchema: z.ZodEnum<{
7
+ "2.1.304": "2.1.304";
8
+ "2.1.306": "2.1.306";
9
+ "2.1.296": "2.1.296";
10
+ "2.1.299": "2.1.299";
11
+ }>;
6
12
  export declare const ClaudeInitializeSchema: z.ZodObject<{
7
13
  claude_code_version: z.ZodString;
8
14
  commands: z.ZodOptional<z.ZodUnknown>;
@@ -1,6 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  // only explicitly qualified CLI and transcript versions may use the local reader
3
- export const ClaudeHistoryVersionSchema = z.enum(['2.1.304', '2.1.306']);
3
+ export const ClaudeHistoryCliVersionSchema = z.enum(['2.1.304', '2.1.306']);
4
+ export const ClaudeHistoryRecordVersionSchema = z.enum([
5
+ '2.1.296',
6
+ '2.1.299',
7
+ ...ClaudeHistoryCliVersionSchema.options,
8
+ ]);
4
9
  export const ClaudeInitializeSchema = z.object({
5
10
  claude_code_version: z.string().min(1).max(128),
6
11
  commands: z.unknown().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jg-agent-hq",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Native coding-agent workspace primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",