@awak-app/simy-cli 0.5.81 → 0.5.83
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/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.83";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -294,7 +294,7 @@ export function assessedCheckpointContract(decision) {
|
|
|
294
294
|
return `Return ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened",evidence:["concrete observation"]} for THIS selected action. This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Completed requires evidence, and SIMY independently checks frozen acceptance. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. missing_information requires a specific question. external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
|
|
295
295
|
}
|
|
296
296
|
export function understandingCheckpointContract(decision) {
|
|
297
|
-
return `This is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; do not edit files, call external services, or start the proposed next action. Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require review. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. For semantic conditions use manual. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. If material context is missing, set exactly one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
297
|
+
return `This is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; do not edit files, call external services, or start the proposed next action. Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require review. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. For semantic conditions use manual. If the complete expected file contents are not yet known from the original user request or observed evidence in this understanding turn, use manual for that criterion and choose a read as the next action to learn them. Never emit file_equals with null, omitted, guessed, or placeholder text; an exact file check requires a bounded concrete UTF-8 string now. Do not perform the proposed next action merely to fill the check, and do not claim that a later read or model report has independently verified a manual criterion. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. If material context is missing, set exactly one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
298
298
|
}
|
|
299
299
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
300
300
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -444,7 +444,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
444
444
|
if (liveHookAdmission) {
|
|
445
445
|
const process = previous?.process;
|
|
446
446
|
if (provider !== 'claude' || previous?.hookStartedAt !== liveHookAdmission.hookStartedAt ||
|
|
447
|
-
!(
|
|
447
|
+
!(await claudeHookChannel(previous, { session, cwd, inspectProcess })).ready || process?.pid !== liveHookAdmission.pid ||
|
|
448
448
|
process?.identity !== liveHookAdmission.identity || !process.identity ||
|
|
449
449
|
await inspectProcess(process.pid) !== process.identity) throw new Error('Claude live hook changed before enrollment; refresh the session.');
|
|
450
450
|
}
|
|
@@ -928,6 +928,29 @@ export async function processIdentity(pid) {
|
|
|
928
928
|
try { const { stdout } = await promisify(execFile)('/bin/ps', ['-p', String(pid), '-o', 'lstart=,comm='], { timeout: 500, maxBuffer: 2048 }); return text(stdout, 1000) || null; }
|
|
929
929
|
catch (e) { return e.code === 1 ? '' : null; }
|
|
930
930
|
}
|
|
931
|
+
// Channel availability lasts for the verified provider session, independently
|
|
932
|
+
// of turn/permission events. Permission and dispatch decisions remain in the loop.
|
|
933
|
+
export async function claudeHookChannel(record, { session, cwd, inspectProcess = processIdentity, platform = process.platform } = {}) {
|
|
934
|
+
const unavailable = (code, reason) => ({ ready: false, code, reason });
|
|
935
|
+
if (platform === 'win32') return unavailable('platform_unsupported', 'Claude process verification is unavailable on Windows; monitoring remains available.');
|
|
936
|
+
if (!record) return unavailable('hook_missing', 'SIMY has not observed a session-start event for this conversation. Check the SIMY plugin and hook execution in the original Claude window.');
|
|
937
|
+
if (record.provider !== 'claude' || record.session !== session) return unavailable('session_mismatch', 'The observed hook belongs to a different Claude conversation. Refresh this conversation.');
|
|
938
|
+
try { if (record.cwd !== await realpath(cwd)) return unavailable('workspace_mismatch', 'The observed Claude hook belongs to a different folder. Refresh this conversation.'); }
|
|
939
|
+
catch { return unavailable('workspace_unavailable', 'The Claude conversation folder could not be verified. Check the folder and refresh.'); }
|
|
940
|
+
if (record.hookEndedAt || record.lastEvent === 'SessionEnd') return unavailable('session_ended', 'This Claude session has ended. Return to the original conversation to continue.');
|
|
941
|
+
if (!record.hookStartedAt) return unavailable(record.hookObservedAt ? 'process_unverified' : 'hook_missing', record.hookObservedAt
|
|
942
|
+
? 'The session-start hook ran, but SIMY could not identify its Claude process. Check the hook diagnostics.'
|
|
943
|
+
: 'SIMY has not observed a session-start event for this conversation. Check the SIMY plugin and hook execution in the original Claude window.');
|
|
944
|
+
const owner = record.hookSessionProcess || record.process; // Existing verified records remain compatible.
|
|
945
|
+
if (!owner?.identity || !Number.isSafeInteger(owner.pid) || owner.pid < 2) return unavailable('process_unverified', 'The session-start hook ran, but SIMY could not identify its Claude process. Check the hook diagnostics.');
|
|
946
|
+
if (record.process?.pid !== owner.pid || record.process?.identity !== owner.identity) return unavailable('process_changed', 'A different Claude process reported this conversation. Refresh the original conversation.');
|
|
947
|
+
let identity;
|
|
948
|
+
try { identity = await inspectProcess(owner.pid); } catch { identity = null; }
|
|
949
|
+
if (identity == null) return unavailable('process_check_unavailable', 'SIMY could not check whether the original Claude process is still running. Refresh and check the local process diagnostics.');
|
|
950
|
+
if (!identity) return unavailable('process_exited', 'The original Claude process has exited. Return to the original conversation to continue.');
|
|
951
|
+
if (identity !== owner.identity) return unavailable('process_changed', 'The original Claude process identity changed. Refresh the original conversation.');
|
|
952
|
+
return { ready: true, code: 'ready', reason: 'The SIMY hook channel is available. Continuation waits for this turn to finish and respects the original permissions.' };
|
|
953
|
+
}
|
|
931
954
|
async function providerProcess(provider) {
|
|
932
955
|
if (process.platform === 'win32') return null;
|
|
933
956
|
let pid = process.ppid;
|
|
@@ -960,12 +983,22 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
960
983
|
}
|
|
961
984
|
record.observedAt = now; record.lastEvent = event;
|
|
962
985
|
record.lastNotificationType = event === 'Notification' ? text(input.notification_type, 80) : null;
|
|
963
|
-
if (identity)
|
|
986
|
+
if (identity) {
|
|
987
|
+
// Legacy records have no frozen hook owner; a new process must not inherit
|
|
988
|
+
// an old SessionStart admission through a later prompt or stop event.
|
|
989
|
+
if (event !== 'SessionStart' && record.process && !record.hookSessionProcess &&
|
|
990
|
+
(identity.pid !== record.process.pid || identity.identity !== record.process.identity)) delete record.hookStartedAt;
|
|
991
|
+
record.process = identity;
|
|
992
|
+
}
|
|
964
993
|
if (event === 'SessionStart') {
|
|
965
|
-
|
|
994
|
+
record.hookObservedAt = now;
|
|
995
|
+
delete record.hookEndedAt;
|
|
996
|
+
if (identity) { record.hookStartedAt = now; record.hookSessionProcess = { ...identity }; }
|
|
997
|
+
else { delete record.hookStartedAt; delete record.hookSessionProcess; delete record.process; }
|
|
966
998
|
const approved = record.run?.managed?.approvedAction;
|
|
967
999
|
output = { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `SIMY hooks observed this ${provider} session: ${input.session_id}. Only if the user requests SIMY Delivery Loop, use simy delivery start --provider ${provider} --session ${input.session_id} with the agreed objective and next step. Existing loop: ${record.run?.state || 'not started'}.${approved ? ` User approved exactly one ${approved.action.kind} action in scope ${approved.action.scope}: ${approved.action.summary}. Next step: ${approved.nextStep}. ${approved.dispatchedAt ? 'That approval was already dispatched; any repeated action needs a new approval.' : 'Resume the original conversation to dispatch this one action.'}` : ''} Never enable, trust, resume, or reset a loop on the user's behalf without their request.` } };
|
|
968
1000
|
}
|
|
1001
|
+
if (event === 'SessionEnd') record.hookEndedAt = now;
|
|
969
1002
|
const run = record.run;
|
|
970
1003
|
if (run && event === 'Stop' && input.turn_id && run.lastStopKey === `${input.turn_id}:${Boolean(input.stop_hook_active)}:${run.revision}`) return record;
|
|
971
1004
|
// Observation only: human prompts mark work in progress, and real provider
|
|
@@ -30,6 +30,10 @@ function candidateMetadata(id, metadata, lastActivityAt, observedAt) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Desktop writes this bookkeeping outside the session envelope. It is neither
|
|
34
|
+
// conversation identity nor message content in either bounded reader.
|
|
35
|
+
const unscopedFileHistory = item => item.type === 'file-history-snapshot' && item.sessionId === undefined;
|
|
36
|
+
|
|
33
37
|
function within(parent, child) {
|
|
34
38
|
const path = relative(parent, child);
|
|
35
39
|
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
@@ -60,6 +64,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
60
64
|
if (!line) continue;
|
|
61
65
|
let item;
|
|
62
66
|
try { item = JSON.parse(line); } catch { return null; }
|
|
67
|
+
if (unscopedFileHistory(item)) continue;
|
|
63
68
|
if (item.sessionId !== id) return null;
|
|
64
69
|
if (item.type === 'custom-title' && item.isSidechain !== true) { customTitle = sessionTitle(item.customTitle); continue; }
|
|
65
70
|
if (item.type !== 'user' && item.type !== 'assistant') continue;
|
|
@@ -199,6 +204,7 @@ async function conversationFromHandle(handle, opened, session, cwd) {
|
|
|
199
204
|
if (!line.trim()) continue;
|
|
200
205
|
let item;
|
|
201
206
|
try { item = JSON.parse(line); } catch { incomplete = true; continue; }
|
|
207
|
+
if (unscopedFileHistory(item)) continue;
|
|
202
208
|
if (item.sessionId !== session || item.isSidechain === true) { incomplete = true; continue; }
|
|
203
209
|
if (!['user', 'assistant'].includes(item.type) || item.isMeta === true) continue;
|
|
204
210
|
if (typeof item.cwd !== 'string' || resolve(item.cwd) !== cwd) { incomplete = true; continue; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { TERMINAL as TERMINAL_RUNS, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { TERMINAL as TERMINAL_RUNS, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
4
|
import { listCodexSessions, openCodexAppServer, readCodexSession, CodexAppServerError } from './codex-app-server.js';
|
|
5
5
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
6
6
|
import { originalSessionTarget } from './original-session-target.js';
|
|
@@ -44,15 +44,13 @@ async function withClient(openClient, work, signal) {
|
|
|
44
44
|
} finally { signal?.removeEventListener('abort', cancelled); await client.close(); }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return await inspectProcess(record.process.pid) === record.process.identity;
|
|
55
|
-
} catch { return false; }
|
|
47
|
+
async function liveClaudeChannel(record, metadata, inspectProcess) {
|
|
48
|
+
if (metadata?.observation?.source !== 'claude_local_index') return { ready: false, code: 'session_metadata_unavailable', reason: 'SIMY could not match this hook to the Claude conversation index. Refresh the conversation list.' };
|
|
49
|
+
return claudeHookChannel(record, { session: metadata.session, cwd: metadata.cwd, inspectProcess });
|
|
50
|
+
}
|
|
51
|
+
async function requireClaudeChannel(record, metadata, inspectProcess) {
|
|
52
|
+
const channel = await liveClaudeChannel(record, metadata, inspectProcess);
|
|
53
|
+
if (!channel.ready) throw Object.assign(new Error(channel.reason), { code: channel.code });
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
export async function discoverSessions({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
@@ -90,7 +88,13 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
90
88
|
const run = record?.run;
|
|
91
89
|
const mode = run?.managed ? run.state === 'recovery_required' && run.recoveryCause === 'session_supervision' ? 'pending_takeover' : 'managed' : run ? 'standard_loop' : record?.monitor ? 'monitor' : 'unmanaged';
|
|
92
90
|
const supported = item.provider === 'codex' && item.observation.source === 'codex_app_server' && Boolean(item.model) && !codexError;
|
|
93
|
-
const
|
|
91
|
+
const claudeChannel = item.provider === 'claude' ? claudeError
|
|
92
|
+
? { ready: false, code: 'session_metadata_unavailable', reason: claudeError }
|
|
93
|
+
: await liveClaudeChannel(record, item, inspectProcess) : null;
|
|
94
|
+
const liveHook = claudeChannel?.ready === true;
|
|
95
|
+
const continuationIssue = liveHook && run ? !run.managed
|
|
96
|
+
? { code: 'existing_loop', reason: 'This conversation already has a Delivery Loop. Use its existing controls.' }
|
|
97
|
+
: TERMINAL_RUNS.has(run.state) ? { code: 'task_settled', reason: 'This supervised task has settled. Review its existing status before continuing.' } : null : null;
|
|
94
98
|
let isRunning = item.isRunning;
|
|
95
99
|
if (record?.runtime?.process?.identity && record.cwd === item.cwd) {
|
|
96
100
|
try {
|
|
@@ -114,13 +118,12 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
114
118
|
binding: { mode, taskId: run?.managed?.taskId || record?.associationTaskId || null },
|
|
115
119
|
originalSessionTarget: originalSessionTarget(item.provider, item.session),
|
|
116
120
|
continuationMode: item.provider === 'claude' ? liveHook ? 'live_hook' : 'manual' : supported ? 'codex_app_server' : 'manual',
|
|
121
|
+
...(claudeChannel ? { continuationReasonCode: continuationIssue?.code || claudeChannel.code } : {}),
|
|
117
122
|
capabilities: { canMonitor: item.capabilities?.canMonitor ?? true,
|
|
118
123
|
canAnalyze: !run && (item.provider === 'codex' ? item.observation.source === 'codex_app_server' && !codexError : item.observation.source === 'claude_local_index' && !claudeError),
|
|
119
124
|
canEnroll: (supported || liveHook) && !run, canContinue: (supported || liveHook) && !run,
|
|
120
125
|
canAutoContinue: (supported || liveHook) && (!run || Boolean(run.managed) && !TERMINAL_RUNS.has(run.state)) },
|
|
121
|
-
reason: item.provider === 'claude' ?
|
|
122
|
-
process.platform === 'win32' ? 'Claude live hook enrollment is unavailable on Windows because provider process identity cannot be verified; monitoring remains available.' :
|
|
123
|
-
'Claude enrollment needs a live matching SIMY SessionStart hook and provider process; cold CLI takeover is unavailable.' :
|
|
126
|
+
reason: item.provider === 'claude' ? continuationIssue?.reason || claudeChannel.reason :
|
|
124
127
|
codexError || (!supported ? 'Exact model or provider metadata is unavailable.' : null),
|
|
125
128
|
};
|
|
126
129
|
};
|
|
@@ -149,8 +152,8 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
149
152
|
if (enabled && previous.run && !previous.run.managed) throw new Error('Automatic continuation is unavailable for this conversation.');
|
|
150
153
|
if (enabled && TERMINAL_RUNS.has(previous.run?.state)) throw new Error('This conversation is settled. Resume an eligible existing task after refreshing its status.');
|
|
151
154
|
if (enabled && provider === 'codex' && !previous.admission?.model) throw new Error('Automatic continuation is unavailable for this conversation.');
|
|
152
|
-
if (enabled && provider === 'claude'
|
|
153
|
-
{ session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity)
|
|
155
|
+
if (enabled && provider === 'claude') await requireClaudeChannel(previous,
|
|
156
|
+
{ session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity);
|
|
154
157
|
const epoch = previous.autoContinue === enabled && previous.autoContinueEpoch ? previous.autoContinueEpoch : randomUUID();
|
|
155
158
|
const settings = { autonomyPolicy: 'balanced', maxMinutes: 30, maxContinuations: 6, maxFailures: 3,
|
|
156
159
|
...previous.autoContinueSettings,
|
|
@@ -230,13 +233,13 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
230
233
|
if (provider === 'claude') {
|
|
231
234
|
metadata = await readClaude(session);
|
|
232
235
|
const record = await readRecord(root, provider, session);
|
|
233
|
-
|
|
236
|
+
await requireClaudeChannel(record, metadata, inspectProcess);
|
|
234
237
|
if (record.run) throw new Error('This session already has a Delivery Loop.');
|
|
235
238
|
await monitorSession({ root, provider, session, monitor: true, readClaude: async () => metadata, now });
|
|
236
239
|
// A second check narrows the time between metadata admission and the
|
|
237
240
|
// original provider's next Stop boundary. Process exit still remains a
|
|
238
241
|
// manual recovery condition; SIMY never starts claude -r.
|
|
239
|
-
|
|
242
|
+
await requireClaudeChannel(await readRecord(root, provider, session), metadata, inspectProcess);
|
|
240
243
|
} else {
|
|
241
244
|
metadata = await monitorSession({ root, provider, session, monitor: true, openClient, now });
|
|
242
245
|
if (!metadata.model) throw new Error('Codex did not expose this conversation’s model; automatic continuation is unavailable.');
|