@awak-app/simy-cli 0.7.37 → 0.7.39
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/README.md +55 -5
- package/package.json +2 -1
- package/src/auto-update.js +25 -13
- package/src/claude-desktop/bridge.js +156 -0
- package/src/claude-desktop/extension.js +84 -0
- package/src/claude-desktop/server.js +86 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +240 -24
- package/src/codex-plugins/simy/hooks/mention-router.mjs +42 -0
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
- package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
- package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
- package/src/codex-plugins/simy/skills/continue/SKILL.md +8 -3
- package/src/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +48 -18
- package/src/index.js +10 -0
- package/src/runner.js +28 -26
- package/src/session-supervision/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- package/src/session-supervision/catalog-refresh.js +16 -0
- package/src/session-supervision/claude-native-agent-contract.js +62 -0
- package/src/session-supervision/claude-outcome-review.js +7 -6
- package/src/session-supervision/claude-sessions.js +137 -76
- package/src/session-supervision/codex-app-server.js +3 -3
- package/src/session-supervision/codex-children.js +2 -2
- package/src/session-supervision/conversation-context.js +58 -26
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +25 -12
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/checkers.js +39 -1
- package/src/sqm/claude-plugin.js +15 -1
- package/src/sqm/claude-simy-agent.md +11 -0
- package/src/sqm/delivery-assessment.js +78 -0
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +27 -12
- package/src/sqm/workflow-onboarding.js +2 -1
- package/src/update-detachment/runtime.js +2 -1
|
@@ -6,7 +6,8 @@ import { evidenceExcerpt } from '../codex-plugins/simy/hooks/purpose-evidence.mj
|
|
|
6
6
|
|
|
7
7
|
// Read only original file predicates in the authorized task directory. These
|
|
8
8
|
// are current independent controller reads, not worker claims or authored proof.
|
|
9
|
-
export async function captureAcceptanceReads(cwd, criteria, { signal } = {}) {
|
|
9
|
+
export async function captureAcceptanceReads(cwd, criteria, { signal, excerptLimit = 4000, publicTaskSourcePaths = [] } = {}) {
|
|
10
|
+
if (!Number.isSafeInteger(excerptLimit) || excerptLimit < 1 || excerptLimit > 32768) throw new Error('Acceptance excerpt bound is invalid.');
|
|
10
11
|
const root = await realpath(cwd), rows = [], seen = new Set(); let retained = 0;
|
|
11
12
|
for (const criterion of criteria) {
|
|
12
13
|
signal?.throwIfAborted();
|
|
@@ -22,8 +23,16 @@ export async function captureAcceptanceReads(cwd, criteria, { signal } = {}) {
|
|
|
22
23
|
if (bytesRead > 32768) continue;
|
|
23
24
|
const body = data.subarray(0, bytesRead); retained += bytesRead;
|
|
24
25
|
const digest = createHash('sha256').update(body).digest('hex');
|
|
26
|
+
// Host-only, explicit source-read permission for a native task reviewer.
|
|
27
|
+
// The same original Claude already has Read access to these public-task
|
|
28
|
+
// source paths. Business identifiers such as Token are source, not a
|
|
29
|
+
// credential claim. Generic artifact reads retain their redaction policy.
|
|
30
|
+
const sourceRead = publicTaskSourcePaths.some(file => resolve(root, file) === path);
|
|
31
|
+
const sourceText = bytesRead ? body.toString('utf8') : JSON.stringify({ byteLength: 0, content: '' });
|
|
32
|
+
const observation = sourceRead ? { text: sourceText.slice(0, excerptLimit), truncated: sourceText.length > excerptLimit,
|
|
33
|
+
redacted: false, policy: 'host_authorized_public_task_source' } : evidenceExcerpt(sourceText, excerptLimit);
|
|
25
34
|
rows.push({ id: `file:${target}`, source: 'controller_file_read', status: 'observed', at: Date.now(), digest,
|
|
26
|
-
recipe: { type: 'file_read', path: target }, observation
|
|
35
|
+
recipe: { type: 'file_read', path: target }, observation });
|
|
27
36
|
} catch { signal?.throwIfAborted(); /* Missing/large/unreadable files stay unverified; tool investigation remains available. */ }
|
|
28
37
|
finally { await handle?.close(); }
|
|
29
38
|
}
|
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { analyzeContinuation, CONTINUATION_ANALYSIS_VERSION } from './continuation-analysis.js';
|
|
6
6
|
import { providerGoalEvidence } from './conversation-context.js';
|
|
7
|
+
import { liveSessionWindow, supervisedSession } from './discovery-window.js';
|
|
7
8
|
|
|
8
9
|
const BACKFILL_LIMIT = 10;
|
|
9
10
|
const MAX_CACHE_ENTRIES = 2000;
|
|
@@ -276,11 +277,17 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
276
277
|
});
|
|
277
278
|
startDrain();
|
|
278
279
|
},
|
|
279
|
-
async update(sessions) {
|
|
280
|
+
async update(sessions, { priorityKeys = new Set() } = {}) {
|
|
280
281
|
await exclusive(async () => {
|
|
281
282
|
if (closed || suspended) return;
|
|
283
|
+
// The complete catalog remains navigable. Historical metadata outside
|
|
284
|
+
// this window must not churn the bounded cache or schedule paid work.
|
|
285
|
+
// Preserve queued/manual jobs and explicitly supervised conversations;
|
|
286
|
+
// removal, changed authority and OFF still use the incoming snapshot.
|
|
287
|
+
const window = liveSessionWindow(sessions, { priority: candidate => priorityKeys.has(keyOf(candidate)) || supervisedSession(candidate) ||
|
|
288
|
+
jobs.has(keyOf(candidate)) || ['queued', 'analyzing'].includes(entries.get(keyOf(candidate))?.result?.state) });
|
|
282
289
|
candidates.clear();
|
|
283
|
-
for (const candidate of
|
|
290
|
+
for (const candidate of window) candidates.set(keyOf(candidate), observedCandidate(candidate));
|
|
284
291
|
const sorted = [...candidates.values()].sort((a, b) => activityOf(b) - activityOf(a) || keyOf(a).localeCompare(keyOf(b)));
|
|
285
292
|
const backfill = document.initialized ? new Set() : new Set(sorted.slice(0, BACKFILL_LIMIT).map(keyOf));
|
|
286
293
|
let dirty = !document.initialized;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// History is a navigation index, not a real-time subscription. Keep ordinary
|
|
2
|
+
// status/preference refreshes cheap; an explicit Refresh still reloads history.
|
|
3
|
+
export function createCatalogRefreshPolicy({ now = Date.now, intervalMs = 300_000 } = {}) {
|
|
4
|
+
let completed = null, scope = null, revision = 0;
|
|
5
|
+
return {
|
|
6
|
+
invalidate() { completed = null; revision++; },
|
|
7
|
+
begin(nextScope) {
|
|
8
|
+
if (scope !== nextScope) { scope = nextScope; completed = null; revision++; }
|
|
9
|
+
const generation = revision;
|
|
10
|
+
const recentOnly = completed !== null && now() - completed < intervalMs;
|
|
11
|
+
return { recentOnly, complete(success) {
|
|
12
|
+
if (!recentOnly && success && generation === revision) completed = now();
|
|
13
|
+
} };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
|
|
3
|
+
// Observed foreground-only Agent contract. Omission is never evidence of
|
|
4
|
+
// foreground execution when the provider exposes background or unknown options.
|
|
5
|
+
const FOREGROUND_SCHEMA = {
|
|
6
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object',
|
|
7
|
+
properties: { description: { type: 'string' }, prompt: { type: 'string' },
|
|
8
|
+
subagent_type: { type: 'string' }, isolation: { type: 'string', enum: ['worktree', 'remote'] } },
|
|
9
|
+
required: ['description', 'prompt'], additionalProperties: false,
|
|
10
|
+
};
|
|
11
|
+
const withoutDescriptions = value => Array.isArray(value) ? value.map(withoutDescriptions) :
|
|
12
|
+
value && typeof value === 'object' ? Object.fromEntries(Object.entries(value)
|
|
13
|
+
.filter(([key, item]) => key !== 'description' || typeof item !== 'string')
|
|
14
|
+
.map(([key, item]) => [key, withoutDescriptions(item)])) : value;
|
|
15
|
+
|
|
16
|
+
export function isForegroundOnlyAgentSchema(schema) {
|
|
17
|
+
return isDeepStrictEqual(withoutDescriptions(schema), FOREGROUND_SCHEMA);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isForegroundNativeReviewInput(toolName, args, observedSchema = null) {
|
|
21
|
+
if (!['Agent', 'Task'].includes(toolName) || !args || typeof args !== 'object' || Array.isArray(args) ||
|
|
22
|
+
typeof args.prompt !== 'string' || typeof args.subagent_type !== 'string' ||
|
|
23
|
+
Object.keys(args).some(key => !['description', 'prompt', 'subagent_type', 'run_in_background'].includes(key)) ||
|
|
24
|
+
Object.hasOwn(args, 'description') && typeof args.description !== 'string') return false;
|
|
25
|
+
if (Object.hasOwn(args, 'run_in_background')) return args.run_in_background === false;
|
|
26
|
+
// The observed Agent schema cannot establish an omitted flag for legacy Task.
|
|
27
|
+
return toolName === 'Agent' && typeof args.description === 'string' && isForegroundOnlyAgentSchema(observedSchema);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Callers supply rows read from the exact owned, stable parent transcript,
|
|
31
|
+
// never a worker-provided schema. A schema first seen after the call proves nothing.
|
|
32
|
+
export function foregroundAgentContract(rows, candidate, beforeIndex = rows.length) {
|
|
33
|
+
const collector = createForegroundAgentContractCollector(candidate);
|
|
34
|
+
for (const row of rows.slice(0, beforeIndex)) collector.add(row);
|
|
35
|
+
return collector.finish();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Keep the prior schema and version agreement without retaining transcript
|
|
39
|
+
// bodies. A snapshot taken before a call cannot be authorized by a later shape.
|
|
40
|
+
export function createForegroundAgentContractCollector(candidate) {
|
|
41
|
+
let shape = null, shapes = 0, version, mixedVersions = false;
|
|
42
|
+
return {
|
|
43
|
+
add(row) {
|
|
44
|
+
if (row.type === 'api-request-shape') { shapes++; if (shapes === 1) shape = row; }
|
|
45
|
+
if (row.version !== undefined) {
|
|
46
|
+
mixedVersions ||= version !== undefined && version !== row.version;
|
|
47
|
+
version = row.version;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
finish() {
|
|
51
|
+
const row = shape;
|
|
52
|
+
if (shapes !== 1 || mixedVersions || version !== row.version || row.sessionId !== candidate.session || row.isSidechain === true ||
|
|
53
|
+
row.cwd !== undefined && row.cwd !== candidate.cwd ||
|
|
54
|
+
!/^[a-f0-9]{64}$/.test(row.shapeHash || '') || !/^\d+\.\d+\.\d+$/.test(row.version || '') ||
|
|
55
|
+
!Array.isArray(row.shape?.tools)) return null;
|
|
56
|
+
const agents = row.shape.tools.filter(tool => tool.name === 'Agent');
|
|
57
|
+
if (agents.length !== 1 || !isForegroundOnlyAgentSchema(agents[0].input_schema)) return null;
|
|
58
|
+
return { schema: agents[0].input_schema, shapeHash: row.shapeHash,
|
|
59
|
+
session: candidate.session, cwd: candidate.cwd, version: row.version ?? null };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -7,7 +7,7 @@ import { basename, relative, resolve } from 'node:path';
|
|
|
7
7
|
import { runStructuredProviderReview } from '../sqm/review-worker.js';
|
|
8
8
|
import { captureAcceptanceReads } from './acceptance-reads.js';
|
|
9
9
|
import { readClaudeConversation, readClaudeNativeReview } from './claude-sessions.js';
|
|
10
|
-
import {
|
|
10
|
+
import { independentReviewRequestContext, workspaceIdentity } from './purpose-verification.js';
|
|
11
11
|
import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
12
12
|
import { liveOutcomeReviewEligible, settleLiveOutcomeReview, readRecord, taskDirectory, hasPendingUserStop, providerLinkState, processIdentity } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
13
13
|
|
|
@@ -52,7 +52,7 @@ export async function claudeReviewBudgetBoundary(record, { platform = process.pl
|
|
|
52
52
|
// original Claude process waits for its CLI child and retains its writer lease.
|
|
53
53
|
// No second controller/session resumes or writes the task workspace.
|
|
54
54
|
export async function reviewClaudeCheckpoint(record, { root, review = runStructuredProviderReview,
|
|
55
|
-
readContext = readClaudeConversation, readNativeReview = readClaudeNativeReview, onFailure = () => {}, captureReads = captureAcceptanceReads, inspectBudget = claudeReviewBudgetBoundary, signal, executionTimeoutMs = 120000 } = {}) {
|
|
55
|
+
readContext = readClaudeConversation, readNativeReview = readClaudeNativeReview, onFailure = () => {}, captureReads = captureAcceptanceReads, fingerprintWorkspace = workspaceIdentity, inspectBudget = claudeReviewBudgetBoundary, signal, executionTimeoutMs = 120000 } = {}) {
|
|
56
56
|
if (!liveOutcomeReviewEligible(record)) return record;
|
|
57
57
|
const snapshot = structuredClone(record), cwd = taskDirectory(record), criteria = effectiveAcceptanceCriteria(record.run.managed);
|
|
58
58
|
const abort = new AbortController();
|
|
@@ -77,17 +77,18 @@ export async function reviewClaudeCheckpoint(record, { root, review = runStructu
|
|
|
77
77
|
summary: 'Independent review was not dispatched: the original Claude process budget arguments could not be verified. Acceptance remains unverified; inspect the original launch configuration without changing its budget.',
|
|
78
78
|
findings: [], source: 'controller_review_unavailable', checkedAt: Date.now() } });
|
|
79
79
|
const inventory = [];
|
|
80
|
-
const before = await
|
|
80
|
+
const before = await fingerprintWorkspace(cwd, undefined, inventory, { signal: combined, inventoryLimit: 1000 });
|
|
81
81
|
const reads = await captureReads(cwd, criteria, { signal: combined });
|
|
82
82
|
// This executor cannot independently inspect unnamed artifacts or services.
|
|
83
83
|
// Never turn absent/truncated input into a successful semantic judgment.
|
|
84
84
|
if (!reads.length || reads.some(row => row.observation.truncated || row.observation.redacted)) return await unavailable('Current outcome observations are absent or incomplete.');
|
|
85
85
|
if (reads.some(row => !inventory.some(file => file.type === 'file' && file.path === relative(cwd, resolve(cwd, row.recipe.path)) && file.digest === row.digest)))
|
|
86
86
|
return await unavailable('Controller observations do not match the reviewed workspace revision; reread the actual outcome.');
|
|
87
|
-
const context = await readContext(record, { signal: combined, userOnly: true });
|
|
87
|
+
const context = await readContext(record, { signal: combined, userOnly: true, reviewUserContext: true });
|
|
88
88
|
if (!context?.messages?.some(row => row.role === 'user') || context.incomplete || context.truncated) return await unavailable('Original user context could not be read completely; inspect the original request before retrying review.');
|
|
89
89
|
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
90
|
-
const requests =
|
|
90
|
+
const requests = independentReviewRequestContext(context);
|
|
91
|
+
if (requests.incomplete || requests.truncated || requests.redacted) return await unavailable('Original user requests exceed complete review coverage or contain redacted context; acceptance remains unverified.');
|
|
91
92
|
if (budget === 'worker_cap' || budget === 'native_only') {
|
|
92
93
|
const contextDigest = createHash('sha256').update(JSON.stringify(requests.messages.filter(row => row.text?.trim()))).digest('hex');
|
|
93
94
|
const pending = record.run.managed.nativeOutcomeReview;
|
|
@@ -111,7 +112,7 @@ export async function reviewClaudeCheckpoint(record, { root, review = runStructu
|
|
|
111
112
|
}
|
|
112
113
|
const result = await review('claude', {
|
|
113
114
|
system: CLAUDE_REVIEW_SYSTEM,
|
|
114
|
-
prompt: JSON.stringify({ objective: record.run.objective, requests
|
|
115
|
+
prompt: JSON.stringify({ objective: record.run.objective, requests, criteria, workspaceInventory: { paths: inventory.map(row => row.path), truncated: inventory.truncated === true }, observations: reads }),
|
|
115
116
|
output_schema: CLAUDE_REVIEW_SCHEMA
|
|
116
117
|
}, { signal: combined, executionTimeoutMs, subscriptionOnly: true, beforeDispatch: async () => {
|
|
117
118
|
if (!await current()) throw new Error('Review target changed');
|
|
@@ -5,7 +5,8 @@ import { lstat, open, readdir, realpath } from 'node:fs/promises';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
|
-
import { boundConversation, CONVERSATION_LIMITS, textContent } from './conversation-context.js';
|
|
8
|
+
import { boundConversation, CONVERSATION_LIMITS, createConversationCollector, textContent } from './conversation-context.js';
|
|
9
|
+
import { createForegroundAgentContractCollector, isForegroundNativeReviewInput } from './claude-native-agent-contract.js';
|
|
9
10
|
import { sessionTitle } from './session-titles.js';
|
|
10
11
|
import { RECENT_SESSION_LIMIT, recentSessionWindow } from './discovery-window.js';
|
|
11
12
|
import { observeClaudeDesktopLineage, readClaudeDesktopLineage } from './claude-desktop-lineage.js';
|
|
@@ -152,7 +153,8 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
152
153
|
const candidate = candidateMetadata(id, metadata, opened.mtimeMs, observedAt);
|
|
153
154
|
signal?.throwIfAborted();
|
|
154
155
|
if (includeStatistics) candidate.statistics = await statisticsFromHandle(handle, opened, 'claude', id, { signal });
|
|
155
|
-
if (includeContext) candidate.context = await conversationFromHandle(handle, opened, id, metadata.cwd,
|
|
156
|
+
if (includeContext) candidate.context = await conversationFromHandle(handle, opened, id, metadata.cwd, {
|
|
157
|
+
userOnly: ['user', 'review_user'].includes(includeContext), reviewUserContext: includeContext === 'review_user', signal });
|
|
156
158
|
if (humanRequest) candidate.humanBoundary = await humanBoundaryFromHandle(handle, opened, path, id, metadata.cwd, humanRequest, signal);
|
|
157
159
|
if (includeBoundary) {
|
|
158
160
|
candidate.transcriptPath = path;
|
|
@@ -309,29 +311,47 @@ const humanFileFingerprint = info => humanFileIdentity(info) === null ? null : c
|
|
|
309
311
|
// cooperative cancellation, exact byte offsets and a digest of every read byte.
|
|
310
312
|
// Whole-history and human-cursor readers retain their separate semantics.
|
|
311
313
|
async function scanTranscriptLines(handle, opened, { maxBytes, maxLineBytes = 4 * 1024 * 1024,
|
|
312
|
-
maxRows = 250000, signal, onLine, onChunk = () => {} }) {
|
|
313
|
-
if (!opened.size || opened.size > maxBytes) return null;
|
|
314
|
+
maxRows = 250000, start = 0, allowIncomplete = false, signal, onLine, onChunk = () => {} }) {
|
|
315
|
+
if (!opened.size || opened.size - start > maxBytes) return null;
|
|
314
316
|
const hash = createHash('sha256'), buffer = Buffer.alloc(64 * 1024);
|
|
315
|
-
let offset =
|
|
317
|
+
let offset = start, fragments = [], pendingBytes = 0, rows = 0, lineOffset = start, discard = start > 0, incomplete = false;
|
|
316
318
|
while (offset < opened.size) {
|
|
317
319
|
signal?.throwIfAborted();
|
|
318
320
|
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, opened.size - offset), offset);
|
|
319
321
|
if (!bytesRead) return null;
|
|
320
|
-
|
|
322
|
+
let chunk = buffer.subarray(0, bytesRead);
|
|
321
323
|
hash.update(chunk); onChunk(chunk, offset); offset += bytesRead;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
if (discard) {
|
|
325
|
+
const end = chunk.indexOf(10);
|
|
326
|
+
if (end === -1) continue;
|
|
327
|
+
chunk = chunk.subarray(end + 1); lineOffset = offset - chunk.length; discard = false;
|
|
328
|
+
}
|
|
329
|
+
let position = 0;
|
|
330
|
+
while (position < chunk.length) {
|
|
325
331
|
signal?.throwIfAborted();
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
if (
|
|
332
|
+
const newline = chunk.indexOf(10, position);
|
|
333
|
+
const part = chunk.subarray(position, newline === -1 ? chunk.length : newline);
|
|
334
|
+
const length = pendingBytes + part.length;
|
|
335
|
+
if (length > maxLineBytes) {
|
|
336
|
+
if (!allowIncomplete) return null;
|
|
337
|
+
incomplete = true; fragments = []; pendingBytes = 0;
|
|
338
|
+
if (newline === -1) { discard = true; break; }
|
|
339
|
+
} else if (newline === -1) {
|
|
340
|
+
// Copy only fragments crossing reads; concatenate once at the newline.
|
|
341
|
+
// Repeatedly concatenating a multi-MiB line made scanning quadratic.
|
|
342
|
+
fragments.push(Buffer.from(part)); pendingBytes = length;
|
|
343
|
+
break;
|
|
344
|
+
} else {
|
|
345
|
+
if (++rows > maxRows) return null;
|
|
346
|
+
const line = fragments.length ? Buffer.concat([...fragments, part], length) : part;
|
|
347
|
+
fragments = []; pendingBytes = 0;
|
|
348
|
+
if (line.length && onLine(line, lineOffset, rows - 1) === false) return null;
|
|
349
|
+
}
|
|
350
|
+
position = newline + 1; lineOffset = offset - chunk.length + position;
|
|
330
351
|
}
|
|
331
|
-
if (pending.length > maxLineBytes) return null;
|
|
332
352
|
}
|
|
333
|
-
if (
|
|
334
|
-
return { digest: hash.digest('hex'), byteLength: offset, rows };
|
|
353
|
+
if (pendingBytes) { if (!allowIncomplete) return null; incomplete = true; }
|
|
354
|
+
return { digest: hash.digest('hex'), byteLength: offset, rows, incomplete };
|
|
335
355
|
}
|
|
336
356
|
|
|
337
357
|
async function humanBoundaryFromHandle(handle, opened, path, session, cwd, request, signal) {
|
|
@@ -410,42 +430,42 @@ export function unchangedClaudeHumanBoundary(proof) {
|
|
|
410
430
|
} catch { return false; }
|
|
411
431
|
}
|
|
412
432
|
|
|
413
|
-
async function conversationFromHandle(handle, opened, session, cwd, userOnly = false) {
|
|
433
|
+
async function conversationFromHandle(handle, opened, session, cwd, { userOnly = false, reviewUserContext = false, signal } = {}) {
|
|
414
434
|
// Native review receipts can make a short user conversation exceed the usual
|
|
415
435
|
// display tail. Bound the file separately and filter before bounding prose.
|
|
416
436
|
const bytes = userOnly ? 64 * 1024 * 1024 : CONVERSATION_LIMITS.bytes;
|
|
417
437
|
const offset = Math.max(0, opened.size - bytes);
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
438
|
+
const collector = createConversationCollector({ reviewUserContext });
|
|
439
|
+
let incomplete = false;
|
|
440
|
+
const scanned = await scanTranscriptLines(handle, opened, { start: offset, maxBytes: bytes, maxLineBytes: bytes, maxRows: Infinity, allowIncomplete: true, signal,
|
|
441
|
+
onLine(line, start, index) {
|
|
442
|
+
const encoded = line.toString('utf8');
|
|
443
|
+
if (!encoded.trim()) return;
|
|
444
|
+
let item;
|
|
445
|
+
try { item = JSON.parse(encoded); } catch { incomplete = true; return; }
|
|
446
|
+
if (unscopedFileHistory(item)) return;
|
|
447
|
+
if (item.sessionId !== session || item.isSidechain === true) { incomplete = true; return; }
|
|
448
|
+
if (!['user', 'assistant'].includes(item.type) || item.isMeta === true) return;
|
|
449
|
+
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd) || !within(cwd, resolve(item.cwd))) { incomplete = true; return; }
|
|
450
|
+
const text = textContent(item.message?.content);
|
|
451
|
+
// User tool_result envelopes are not a new human request. A missing
|
|
452
|
+
// human message/attachment-only request cannot silently reuse an old goal.
|
|
453
|
+
if (item.type === 'user' && !text.trim() && !item.message?.content?.every?.(part => part?.type === 'tool_result')) incomplete = true;
|
|
454
|
+
if (reviewUserContext && item.type === 'user' && Array.isArray(item.message?.content) &&
|
|
455
|
+
item.message.content.some(part => !['text', 'input_text', 'output_text', 'tool_result'].includes(part?.type))) incomplete = true;
|
|
456
|
+
if (userOnly && item.type !== 'user') return;
|
|
457
|
+
collector.add({ role: item.type, turnRef: typeof item.uuid === 'string' ? item.uuid : `line:${offset}:${index}`, text });
|
|
458
|
+
} });
|
|
440
459
|
const after = await handle.stat();
|
|
441
|
-
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) incomplete = true;
|
|
442
|
-
return
|
|
460
|
+
if (!scanned || scanned.incomplete || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) incomplete = true;
|
|
461
|
+
return collector.finish({ truncated: offset > 0, incomplete });
|
|
443
462
|
}
|
|
444
463
|
|
|
445
|
-
export async function readClaudeConversation(candidate, { home, signal, userOnly = false } = {}) {
|
|
464
|
+
export async function readClaudeConversation(candidate, { home, signal, userOnly = false, reviewUserContext = false } = {}) {
|
|
465
|
+
if (reviewUserContext && !userOnly) throw new Error('Review context must contain only original user requests.');
|
|
446
466
|
if (signal?.aborted) return boundConversation(null);
|
|
447
467
|
if (!UUID.test(candidate?.session || '') || typeof candidate?.cwd !== 'string') return boundConversation(null);
|
|
448
|
-
const matches = await collect({ home, session: candidate.session, includeContext: userOnly ? 'user' : true, signal });
|
|
468
|
+
const matches = await collect({ home, session: candidate.session, includeContext: reviewUserContext ? 'review_user' : userOnly ? 'user' : true, signal });
|
|
449
469
|
const selected = matches.filter(item => item.cwd === candidate.cwd);
|
|
450
470
|
if (signal?.aborted || selected.length !== 1) return boundConversation(null);
|
|
451
471
|
if (Number.isFinite(candidate.lastActivityAt) && selected[0].lastActivityAt !== candidate.lastActivityAt) {
|
|
@@ -497,49 +517,90 @@ export async function readClaudeStatistics(candidate, { home, signal } = {}) {
|
|
|
497
517
|
// Consume only an exact native Agent invocation in the original session. A
|
|
498
518
|
// worker checkpoint, forwarded quote, resumed child or tool-using child is not
|
|
499
519
|
// an independent verdict. Native Claude accounts this child in its own budget.
|
|
500
|
-
|
|
520
|
+
async function scanNativeRows(path, visit, { signal, expectedDigest } = {}) {
|
|
521
|
+
signal?.throwIfAborted();
|
|
522
|
+
if (await realpath(path) !== path) throw new Error('Native transcript link changed');
|
|
523
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
524
|
+
try {
|
|
525
|
+
const before = await handle.stat();
|
|
526
|
+
if (!before.isFile()) throw new Error('Native transcript unavailable');
|
|
527
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
528
|
+
const scanned = await scanTranscriptLines(handle, before, { maxBytes: MAX_COMPLETE_TRANSCRIPT_BYTES, signal,
|
|
529
|
+
onLine: line => visit(JSON.parse(decoder.decode(line))) });
|
|
530
|
+
const after = await handle.stat();
|
|
531
|
+
const pathAfter = await lstat(path);
|
|
532
|
+
if (await realpath(path) !== path || pathAfter.isSymbolicLink() || pathAfter.ino !== before.ino || pathAfter.dev !== before.dev ||
|
|
533
|
+
before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs ||
|
|
534
|
+
pathAfter.size !== before.size || pathAfter.mtimeMs !== before.mtimeMs || pathAfter.ctimeMs !== before.ctimeMs || !scanned ||
|
|
535
|
+
expectedDigest && scanned.digest !== expectedDigest) throw new Error('Native transcript changed');
|
|
536
|
+
return { snapshot: [before.dev, before.ino, before.size, before.mtimeMs, before.ctimeMs].join(':'), digest: scanned.digest };
|
|
537
|
+
} finally { await handle.close(); }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function nativeParentTranscript(candidate, { home, signal }, visit) {
|
|
501
541
|
const matches = (await collect({ home, session: candidate.session, includeBoundary: true, signal }))
|
|
502
542
|
.filter(row => row.cwd === candidate.cwd && row.boundary?.complete);
|
|
503
543
|
if (matches.length !== 1) return null;
|
|
504
544
|
const parentPath = matches[0].transcriptPath;
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
};
|
|
545
|
+
const scanned = await scanNativeRows(parentPath, row => {
|
|
546
|
+
if (row.sessionId !== undefined && row.sessionId !== candidate.session ||
|
|
547
|
+
['user', 'assistant'].includes(row.type) && (row.isSidechain === true || row.cwd !== candidate.cwd)) return false;
|
|
548
|
+
return visit(row);
|
|
549
|
+
}, { signal, expectedDigest: matches[0].boundary.digest });
|
|
550
|
+
return { parentPath, ...scanned };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function readClaudeNativeAgentContract(candidate, { home, signal } = {}) {
|
|
554
|
+
try {
|
|
555
|
+
const collector = createForegroundAgentContractCollector(candidate);
|
|
556
|
+
const parent = await nativeParentTranscript(candidate, { home, signal }, row => collector.add(row));
|
|
557
|
+
return parent ? collector.finish() : null;
|
|
558
|
+
} catch (error) { if (signal?.aborted) throw error; return null; }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export async function readClaudeNativeReview(candidate, request, { home, signal } = {}) {
|
|
519
562
|
try {
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
563
|
+
const calls = [], contracts = createForegroundAgentContractCollector(candidate);
|
|
564
|
+
const parent = await nativeParentTranscript(candidate, { home, signal }, row => {
|
|
565
|
+
if (row.sessionId === candidate.session && !row.isSidechain && row.type === 'assistant' && Date.parse(row.timestamp) >= request.requestedAt) {
|
|
566
|
+
for (const part of row.message?.content || []) {
|
|
567
|
+
if (part.type === 'tool_use' && ['Agent', 'Task'].includes(part.name) && part.input?.prompt === request.prompt)
|
|
568
|
+
calls.push({ call: part, contract: contracts.finish() });
|
|
569
|
+
if (calls.length > 1) return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
contracts.add(row);
|
|
573
|
+
});
|
|
574
|
+
if (!parent || calls.length !== 1) return null;
|
|
575
|
+
const { parentPath } = parent, { call, contract } = calls[0];
|
|
576
|
+
if (call.input.subagent_type !== request.agentType || !isForegroundNativeReviewInput(call.name, call.input, contract?.schema)) return null;
|
|
577
|
+
const receipts = [];
|
|
578
|
+
const receiptScan = await scanNativeRows(parentPath, row => {
|
|
579
|
+
if (row.sessionId === candidate.session && !row.isSidechain && row.type === 'user' &&
|
|
580
|
+
row.message?.content?.some?.(part => part.type === 'tool_result' && part.tool_use_id === call.id) && row.toolUseResult) receipts.push(row);
|
|
581
|
+
if (receipts.length > 1) return false;
|
|
582
|
+
}, { signal, expectedDigest: parent.digest });
|
|
583
|
+
if (receiptScan.snapshot !== parent.snapshot) return null;
|
|
529
584
|
if (receipts.length !== 1) return null;
|
|
530
585
|
const result = receipts[0].toolUseResult;
|
|
531
586
|
if (result.status !== 'completed' || result.prompt !== request.prompt || result.agentType !== request.agentType ||
|
|
532
587
|
result.totalToolUseCount !== 0 || !/^a[a-f0-9]{8,40}$/.test(result.agentId || '')) return null;
|
|
533
588
|
const childPath = join(parentPath.slice(0, -6), 'subagents', `agent-${result.agentId}.jsonl`);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
589
|
+
let users = 0, answer = null, emptyTools = false;
|
|
590
|
+
await scanNativeRows(childPath, row => {
|
|
591
|
+
if (row.sessionId !== candidate.session || row.agentId !== result.agentId || ['user', 'assistant', 'attachment'].includes(row.type) && row.isSidechain !== true) return false;
|
|
592
|
+
if (row.type === 'user' && (++users !== 1 || textContent(row.message?.content) !== request.prompt)) return false;
|
|
593
|
+
if (row.attachment?.type === 'prompt_snapshot') {
|
|
594
|
+
const snapshot = row.attachment;
|
|
595
|
+
if (snapshot.tools && snapshot.tools.length !== 0 || !snapshot.systemPrompt?.some(text => text.trim() === CLAUDE_NATIVE_REVIEW_PROMPT)) return false;
|
|
596
|
+
emptyTools ||= Array.isArray(snapshot.tools) && snapshot.tools.length === 0;
|
|
597
|
+
}
|
|
598
|
+
if (row.type === 'assistant') {
|
|
599
|
+
if (row.message?.content?.some?.(part => part.type === 'tool_use')) return false;
|
|
600
|
+
answer = textContent(row.message?.content);
|
|
601
|
+
}
|
|
602
|
+
}, { signal });
|
|
603
|
+
if (users !== 1 || !emptyTools || answer === null) return null;
|
|
543
604
|
if (answer !== textContent(result.content)) return null;
|
|
544
605
|
const output = JSON.parse(answer.trim().replace(/^```(?:json)?\s*\n([\s\S]*?)\n```$/, '$1'));
|
|
545
606
|
if (typeof output.passed !== 'boolean' || typeof output.inspectionAvailable !== 'boolean' || typeof output.summary !== 'string' || output.summary.length > 4000 ||
|
|
@@ -28,7 +28,7 @@ import { CODEX_WRITER_LOCK_MINIMUM_VERSION, compareVersions, inspectBackendExecu
|
|
|
28
28
|
import { hasPendingUserStop, readRecord, recordPath, rootDirectory, canExecuteAutonomousExternal, assessedCheckpointContract, deliveryProgressContext, manualAcceptanceFeedback, manualAcceptanceFeedbackInstruction, taskDirectory, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
29
29
|
import { boundConversation, boundProviderGoal, textContent } from './conversation-context.js';
|
|
30
30
|
import { JsonDataUrlFilter } from './json-data-url-filter.js';
|
|
31
|
-
import { RECENT_SESSION_LIMIT } from './discovery-window.js';
|
|
31
|
+
import { RECENT_SESSION_LIMIT, liveSessionWindow } from './discovery-window.js';
|
|
32
32
|
|
|
33
33
|
const REQUEST_TIMEOUT = 15_000;
|
|
34
34
|
const MAX_LINE = 256 * 1024;
|
|
@@ -307,7 +307,7 @@ export async function listCodexSessions({ client, all = false, signal, onPage, m
|
|
|
307
307
|
// reports notLoaded rather than the original writer's actual turn state.
|
|
308
308
|
// Bound this metadata fan-out independently of the analysis worker pool.
|
|
309
309
|
const priorities = new Set(prioritySessions);
|
|
310
|
-
const ordered =
|
|
310
|
+
const ordered = liveSessionWindow(sessions, { priority: item => priorities.has(item.session) });
|
|
311
311
|
let next = 0, unavailable = false;
|
|
312
312
|
for (const metadata of sessions) metadata.goalReadState = 'unavailable';
|
|
313
313
|
await Promise.all(Array.from({ length: Math.min(3, sessions.length) }, async () => {
|
|
@@ -355,7 +355,7 @@ export async function readCodexArchiveStates(client, candidates, { budgetMs = 30
|
|
|
355
355
|
const offset = (archiveProbeOffsets.get(codexHome) || 0) % Math.max(1, missing.length);
|
|
356
356
|
let next = 0;
|
|
357
357
|
await Promise.all(Array.from({ length: Math.min(3, missing.length) }, async () => {
|
|
358
|
-
while (next < missing.length && Date.now() < deadline) {
|
|
358
|
+
while (next < Math.min(missing.length, RECENT_SESSION_LIMIT) && Date.now() < deadline) {
|
|
359
359
|
const candidate = missing[(offset + next++) % missing.length];
|
|
360
360
|
try {
|
|
361
361
|
const response = await boundedClient.request('thread/read', { threadId: candidate.session, includeTurns: false });
|
|
@@ -23,14 +23,14 @@ export async function readCodexChildSessions({ parents = [], includeArchived = f
|
|
|
23
23
|
db = new DatabaseSync(path, { readOnly: true });
|
|
24
24
|
db.exec('PRAGMA query_only = ON; PRAGMA busy_timeout = 50');
|
|
25
25
|
const rows = db.prepare(`WITH RECURSIVE family(id, depth) AS (
|
|
26
|
-
SELECT id, 0 FROM threads WHERE id IN (
|
|
26
|
+
SELECT id, 0 FROM threads WHERE id IN (SELECT value FROM json_each(?))
|
|
27
27
|
UNION SELECT e.child_thread_id, f.depth + 1 FROM thread_spawn_edges e
|
|
28
28
|
JOIN family f ON f.id = e.parent_thread_id WHERE f.depth < 32
|
|
29
29
|
) SELECT t.id, t.cwd, t.name, t.source, t.model, t.updated_at, t.archived,
|
|
30
30
|
e.parent_thread_id, MIN(f.depth) AS depth
|
|
31
31
|
FROM family f JOIN threads t ON t.id = f.id
|
|
32
32
|
JOIN thread_spawn_edges e ON e.child_thread_id = t.id
|
|
33
|
-
WHERE f.depth > 0 GROUP BY t.id ORDER BY depth, t.updated_at DESC LIMIT 2000`).all(
|
|
33
|
+
WHERE f.depth > 0 GROUP BY t.id ORDER BY depth, t.updated_at DESC LIMIT 2000`).all(JSON.stringify(roots));
|
|
34
34
|
const candidates = [];
|
|
35
35
|
for (const row of rows) {
|
|
36
36
|
let source;
|