@awak-app/simy-cli 0.6.2 → 0.6.5
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 +6 -1
- package/package.json +3 -3
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +16 -22
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +76 -101
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +10 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +1 -1
- package/src/delivery-hooks/command.js +12 -16
- package/src/delivery-hooks/desktop.js +6 -14
- package/src/delivery-hooks/supervisor.js +7 -31
- package/src/desktop-host.js +15 -14
- package/src/session-supervision/action-association-host.js +7 -3
- package/src/session-supervision/action-association.js +2 -2
- package/src/session-supervision/association-store.js +12 -3
- package/src/session-supervision/claude-sessions.js +36 -20
- package/src/session-supervision/codex-app-server.js +8 -22
- package/src/session-supervision/discovery-window.js +15 -0
- package/src/session-supervision/handoff.js +2 -1
- package/src/session-supervision/index.js +25 -27
- package/src/session-supervision/stop-notifications.js +43 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { canReplanDelivery,
|
|
3
|
+
import { canReplanDelivery, deliveryStop, deliveryStopBlocksResume, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
4
|
import { originalSessionTarget } from '../session-supervision/original-session-target.js';
|
|
5
5
|
|
|
6
6
|
export const HOOK_SETUP = Object.freeze({
|
|
@@ -31,17 +31,11 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
31
31
|
runs: records.filter(r => r.run).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
|
|
32
32
|
provider: r.provider, session: r.session, originalSessionTarget: originalSessionTarget(r.provider, r.session),
|
|
33
33
|
objective: r.run.objective, state: r.run.state, reason: r.run.reason,
|
|
34
|
-
continuations: r.run.continuations,
|
|
35
|
-
localExecutionLimits: false,
|
|
34
|
+
continuations: r.run.continuations,
|
|
36
35
|
continuationPolicy: r.run.managed?.continuationPolicy || 'bounded',
|
|
37
36
|
startedAt: r.run.startedAt,
|
|
38
|
-
|
|
39
|
-
canChangeContinuation: false,
|
|
40
|
-
hardDeadline: null, hardMaxContinuations: null,
|
|
41
|
-
budgetWarnings: deliveryBudgetState(r.run, now).warnings,
|
|
42
|
-
budgetCapability: r.run.managed?.budgetCapability || null,
|
|
37
|
+
stopCode: deliveryStop(r.run, now)?.code ?? null,
|
|
43
38
|
outcomeProgress: r.run.managed?.outcomeProgress || null,
|
|
44
|
-
budgetExtensionHistory: r.run.budgetExtensionHistory || [],
|
|
45
39
|
needsAttention: ['blocked', 'failed', 'recovery_required', 'budget_exhausted', 'verification_pending'].includes(r.run.state), updatedAt: r.run.updatedAt,
|
|
46
40
|
managed: Boolean(r.run.managed), taskId: r.run.managed?.taskId || null, actionId: r.run.managed?.actionId || null,
|
|
47
41
|
actionDueAt: r.run.managed?.actionDueAt ?? null,
|
|
@@ -56,8 +50,6 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
56
50
|
handoff: r.run.managed?.handoff || null, threadHistory: r.run.managed?.threadHistory || [],
|
|
57
51
|
progressBudget: r.run.managed ? { minutes: r.run.managed.maxNoProgressMinutes, lastVerifiedProgressAt: r.run.managed.lastVerifiedProgressAt,
|
|
58
52
|
attempts: r.run.managed.noProgress, maxAttempts: r.run.managed.maxNoProgress } : null,
|
|
59
|
-
tokenBudget: r.run.managed?.maxTokens ? { limit: r.run.managed.maxTokens, used: r.run.managed.tokensUsed,
|
|
60
|
-
usageAvailable: r.run.managed.usageAvailable, source: 'provider_observed', dollarCost: null } : null,
|
|
61
53
|
pendingApproval: r.run.managed?.pendingApproval || null,
|
|
62
54
|
autonomyPolicy: r.run.managed?.autonomyPolicy || null, plan: r.run.managed?.plan || null,
|
|
63
55
|
decision: r.run.managed?.decision || null, decisionHistory: r.run.managed?.decisionHistory || [],
|
|
@@ -77,14 +69,14 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
77
69
|
r.run.managed.resumeChannel === 'codex_app_server' && r.run.managed.autoRecovery),
|
|
78
70
|
canResume: ['blocked', 'recovery_required', 'paused'].includes(r.run.state) && r.run.managed?.phase !== 'needs_clarification' &&
|
|
79
71
|
!r.run.managed?.pendingApproval && !r.run.managed?.dependencyWait && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status) &&
|
|
80
|
-
!
|
|
72
|
+
!deliveryStopBlocksResume(r.run, now),
|
|
81
73
|
canApprove: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
|
|
82
74
|
canReject: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
|
|
83
75
|
canReplan: canReplanDelivery(r.run, r.provider, now),
|
|
84
76
|
canAnswer: r.run.managed?.phase === 'needs_clarification' && r.run.state === 'blocked' && Boolean(r.run.managed.understandingQuestion?.id) &&
|
|
85
|
-
!
|
|
77
|
+
!deliveryStopBlocksResume(r.run, now),
|
|
86
78
|
canResolveDependency: r.run.state === 'blocked' && r.run.blocker === 'external_dependency' && Boolean(r.run.managed?.dependencyWait?.id) &&
|
|
87
|
-
!
|
|
79
|
+
!deliveryStopBlocksResume(r.run, now) && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status),
|
|
88
80
|
canSetPolicy: Boolean(r.run.managed?.autonomyPolicy) && !['completed', 'reported_done', 'stopped', 'failed', 'budget_exhausted'].includes(r.run.state),
|
|
89
81
|
})),
|
|
90
82
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
|
-
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop,
|
|
4
|
+
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
5
5
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
6
6
|
import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
7
7
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
8
8
|
|
|
9
9
|
const MAX_OUTPUT = 64 * 1024;
|
|
10
10
|
const SAFE_PROVIDER_CODES = new Set(['identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
11
|
-
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval'
|
|
11
|
+
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval']);
|
|
12
12
|
function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
|
|
13
13
|
function providerFailureReason(code) {
|
|
14
14
|
if (code === 'permission_restore') return 'Codex sandbox restoration could not be confirmed; inspect the original conversation before resuming.';
|
|
@@ -40,9 +40,8 @@ async function assertActiveLease(root, provider, session, leaseId, signal, curre
|
|
|
40
40
|
if (signal?.aborted || current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId) {
|
|
41
41
|
throw new Error('Managed recovery lease changed before provider dispatch.');
|
|
42
42
|
}
|
|
43
|
-
if (currentTime() >= managedDispatchDeadline(current.run)) throw new Error('Managed recovery deadline expired before provider dispatch.');
|
|
44
43
|
if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
|
|
45
|
-
if (managedBudgetReason(current.run, currentTime())) throw new Error('Managed
|
|
44
|
+
if (managedBudgetReason(current.run, currentTime())) throw new Error('Managed task stopped before provider dispatch.');
|
|
46
45
|
const managed = current.run.managed;
|
|
47
46
|
const approvedWait = managed.decision?.outcome === 'wait' && managed.approvedAction?.id === managed.decision.id && !managed.approvedAction.dispatchedAt;
|
|
48
47
|
if (managed.autonomyPolicy && ((managed.decision?.outcome === 'wait' && !approvedWait) || managed.resumeLease.decisionId !== managed.decision?.id)) throw new Error('Assessed action changed before provider dispatch.');
|
|
@@ -80,13 +79,11 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
80
79
|
lastDependencyResolution: run.managed.lastDependencyResolution,
|
|
81
80
|
remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
|
|
82
81
|
currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
|
|
83
|
-
|
|
84
|
-
localExecutionLimits: false } };
|
|
82
|
+
progress: deliveryProgressContext(run) };
|
|
85
83
|
const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
|
|
86
84
|
const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
|
|
87
85
|
const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
|
|
88
86
|
if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
|
|
89
|
-
if (currentTime() >= managedDispatchDeadline(run)) throw new Error('Managed recovery deadline expired before provider dispatch.');
|
|
90
87
|
await beforeDispatch();
|
|
91
88
|
if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
|
|
92
89
|
|
|
@@ -196,9 +193,9 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
196
193
|
const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(live.managed.decision.action?.kind) &&
|
|
197
194
|
live.managed.approvedAction?.id === live.managed.decision.id && !live.managed.approvedAction.dispatchedAt;
|
|
198
195
|
if (!live?.managed?.autoRecovery || live.state !== 'recovery_required' || !allowedCause || live.managed.pendingApproval || live.managed.autonomyPolicy && (!live.managed.decision || live.managed.decision.outcome === 'wait' && !approved) || appChannel && live.managed.nextAttemptAt > currentTime()) return current;
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
live.state =
|
|
196
|
+
const stop = deliveryStop(live, currentTime());
|
|
197
|
+
if (stop) {
|
|
198
|
+
live.state = stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
|
|
202
199
|
}
|
|
203
200
|
live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
|
|
204
201
|
owner: { pid: process.pid, identity: ownerIdentity } };
|
|
@@ -217,7 +214,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
217
214
|
let result;
|
|
218
215
|
let resource;
|
|
219
216
|
let lastActivityAt = currentTime();
|
|
220
|
-
let usage = null;
|
|
221
217
|
let dispatched = false, waitingForTool = false;
|
|
222
218
|
const controller = new AbortController();
|
|
223
219
|
const abort = () => controller.abort();
|
|
@@ -262,10 +258,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
262
258
|
const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
|
|
263
259
|
result = await runner(claimed, { signal: controller.signal, root, leaseId, currentTime, leaseMonitored: true,
|
|
264
260
|
onActivity: observation => { lastActivityAt = currentTime(); waitingForTool = observation?.waitingForTool === true; },
|
|
265
|
-
onUsage: observed => {
|
|
266
|
-
usage = observed;
|
|
267
|
-
|
|
268
|
-
},
|
|
269
261
|
beforeDispatch: async providerSandbox => {
|
|
270
262
|
await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime);
|
|
271
263
|
await updateRecord(root, record.provider, record.session, current => {
|
|
@@ -298,9 +290,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
298
290
|
errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
|
|
299
291
|
finally { stopWatch(); signal?.removeEventListener('abort', abort); }
|
|
300
292
|
if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
|
|
301
|
-
if (deliveryBudgetState(claimed.run).tokenCeiling && !usage && !result.busy && dispatched) {
|
|
302
|
-
result = { ...result, usageUnavailable: true };
|
|
303
|
-
}
|
|
304
293
|
if (result.code === 0) {
|
|
305
294
|
try {
|
|
306
295
|
await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
|
|
@@ -327,14 +316,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
327
316
|
const live = current?.run;
|
|
328
317
|
if (live?.managed?.resumeLease?.id !== leaseId) return current;
|
|
329
318
|
delete live.managed.resumeLease;
|
|
330
|
-
delete live.managed.usageBlocked;
|
|
331
|
-
if (usage) {
|
|
332
|
-
live.managed.tokensUsed = (live.managed.tokensUsed || 0) + usage.tokens;
|
|
333
|
-
live.managed.usageAvailable = true;
|
|
334
|
-
live.managed.lastUsage = { ...usage, attemptId: leaseId };
|
|
335
|
-
} else if (!result.busy && deliveryBudgetState(live).tokenCeiling && dispatched) {
|
|
336
|
-
live.managed.usageAvailable = false; live.managed.usageBlocked = true;
|
|
337
|
-
}
|
|
338
319
|
const attempt = live.managed.executionAttempt;
|
|
339
320
|
if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
|
|
340
321
|
attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
|
|
@@ -351,13 +332,8 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
351
332
|
if (live.state !== 'auto_resuming') live.reason = `${String(live.reason || '').slice(0, 400)} ${live.managed.providerWarning}`.trim();
|
|
352
333
|
}
|
|
353
334
|
if (live.state !== 'auto_resuming') {
|
|
354
|
-
if (live.managed.usageBlocked && !['stopped', 'paused', 'completed', 'reported_done'].includes(live.state)) {
|
|
355
|
-
live.managed.autoRecovery = false;
|
|
356
|
-
live.reason = `${live.reason} Provider token usage is unavailable; further budgeted turns are blocked.`;
|
|
357
|
-
}
|
|
358
335
|
live.updatedAt = Date.now(); notifyState(live, live.updatedAt); return current;
|
|
359
336
|
}
|
|
360
|
-
live.budgetWarnings = deliveryBudgetState(live).warnings;
|
|
361
337
|
if (result.busy) {
|
|
362
338
|
if (result.accessDenied && dispatched) {
|
|
363
339
|
// The final guard rejected before the provider call. Keep its one-shot approval unused.
|
package/src/desktop-host.js
CHANGED
|
@@ -9,6 +9,7 @@ import { deliveryHookStatus, openHookSetup } from "./delivery-hooks/desktop.js";
|
|
|
9
9
|
import { controlDelivery, verifyDelivery, createRecordReader } from "./codex-plugins/simy/hooks/delivery-loop.mjs";
|
|
10
10
|
import { createManagedSupervisor } from "./delivery-hooks/supervisor.js";
|
|
11
11
|
import { createActionAssociationHost } from "./session-supervision/action-association-host.js";
|
|
12
|
+
import { createStopNotifier } from "./session-supervision/stop-notifications.js";
|
|
12
13
|
import { answerSession, continueSession, discoverSessions, enrollSession, monitorSession, trackSession, setSessionAutoContinue } from "./session-supervision/index.js";
|
|
13
14
|
import { createContinuationAnalysisManager } from "./session-supervision/analysis-manager.js";
|
|
14
15
|
import { analyzeContinuation, readContinuationContext } from "./session-supervision/continuation-analysis.js";
|
|
@@ -95,6 +96,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
95
96
|
const makeActionAssociations = () => (dependencies.createActionAssociationHost || createActionAssociationHost)({ root: join(root, "delivery-hooks"), sessionRoot: root, webOrigin, list: () => recordReader.list(), onChange: () => { if (!closed) setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
96
97
|
let actionAssociations = makeActionAssociations();
|
|
97
98
|
const statisticsCache = new Map();
|
|
99
|
+
const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
|
|
98
100
|
let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
|
|
99
101
|
const accessScope = access => JSON.stringify([access?.revision ?? null, access?.organizationId ?? null]);
|
|
100
102
|
const setAccessSnapshot = async access => {
|
|
@@ -105,7 +107,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
105
107
|
discoveryController?.abort();
|
|
106
108
|
sessionRefreshAgain = Boolean(sessionRefresh) && desktopAccessReady(currentAccess);
|
|
107
109
|
for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
|
|
108
|
-
sessionStatus = { sessions: [], providers: {}, refreshedAt: null }; sessionRefreshedAt = 0;
|
|
110
|
+
sessionStatus = { sessions: [], providers: {}, refreshedAt: null, initialLoading: true }; sessionRefreshedAt = 0;
|
|
109
111
|
await analysisManager?.update([]);
|
|
110
112
|
}
|
|
111
113
|
return currentAccess;
|
|
@@ -134,7 +136,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
134
136
|
beforeClaim: record => actionAssociations.prepareCached?.(record) || record });
|
|
135
137
|
let shuttingDown = null;
|
|
136
138
|
let commands;
|
|
137
|
-
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null };
|
|
139
|
+
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
138
140
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
139
141
|
const stopDiscovery = async ({ drain = false } = {}) => {
|
|
140
142
|
discoveryController?.abort();
|
|
@@ -158,8 +160,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
158
160
|
sessionRefreshAgain = desktopAccessReady(latestAccess);
|
|
159
161
|
return;
|
|
160
162
|
}
|
|
161
|
-
await analysisManager.update(value.sessions); sessionStatus = value; sessionRefreshedAt = Date.now(); })
|
|
162
|
-
.catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
|
|
163
|
+
await analysisManager.update(value.sessions); sessionStatus = { ...value, initialLoading: false }; sessionRefreshedAt = Date.now(); })
|
|
164
|
+
.catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, initialLoading: false, providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
|
|
163
165
|
.finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
|
|
164
166
|
if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
|
|
165
167
|
}
|
|
@@ -244,6 +246,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
244
246
|
}
|
|
245
247
|
await analysisManager.updateProviderStatuses?.(status.providers);
|
|
246
248
|
const records = await recordReader.list();
|
|
249
|
+
stopNotifier.observe(records);
|
|
247
250
|
const projection = await actionAssociations.beginProjection?.({ records });
|
|
248
251
|
status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: projection?.records || records }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
|
|
249
252
|
await actionAssociations.decorate(status.deliveryHooks, projection);
|
|
@@ -459,7 +462,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
459
462
|
if (command.force !== true) throw new Error("Confirm before stopping this task.");
|
|
460
463
|
await agent.stopDesktopTask(command.taskKey);
|
|
461
464
|
emit({ type: "result", id: command.id, ok: true });
|
|
462
|
-
void publish(); return;
|
|
465
|
+
void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
|
|
463
466
|
}
|
|
464
467
|
const access = await accessSnapshot();
|
|
465
468
|
const commandScope = accessScope(access);
|
|
@@ -479,7 +482,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
479
482
|
statisticsCache.clear(); // Only the selected conversation needs chart data.
|
|
480
483
|
statisticsCache.set(`${candidate.provider}:${candidate.session}`, statistics);
|
|
481
484
|
emit({ type: "result", id: command.id, ok: true });
|
|
482
|
-
void publish(); return;
|
|
485
|
+
void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
|
|
483
486
|
}
|
|
484
487
|
if (action === "my_ai_session") {
|
|
485
488
|
const ticket = await agent.createDesktopWebSession();
|
|
@@ -573,9 +576,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
573
576
|
else if (action === "codex_hooks" || action === "claude_hooks") {
|
|
574
577
|
const provider = action === "codex_hooks" ? "codex" : "claude";
|
|
575
578
|
await (dependencies.openHookSettings || openHookSetup)(provider);
|
|
576
|
-
} else if (["delivery_stop", "delivery_resume", "delivery_pause", "delivery_approve", "delivery_reject", "delivery_replan", "delivery_set_policy", "delivery_dependency_ready", "
|
|
579
|
+
} else if (["delivery_stop", "delivery_resume", "delivery_pause", "delivery_approve", "delivery_reject", "delivery_replan", "delivery_set_policy", "delivery_dependency_ready", "delivery_continuation_policy"].includes(action)) {
|
|
577
580
|
await controlDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
|
|
578
|
-
taskId: command.taskId, approvalId: command.approvalId, autonomyPolicy: command.autonomyPolicy,
|
|
581
|
+
taskId: command.taskId, approvalId: command.approvalId, autonomyPolicy: command.autonomyPolicy, continuationPolicy: command.continuationPolicy, waitId: command.waitId, answer: command.answer, action: action.slice("delivery_".length) });
|
|
579
582
|
} else if (action === "delivery_verify") {
|
|
580
583
|
await verifyDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
|
|
581
584
|
taskId: command.taskId, criterionId: command.criterionId });
|
|
@@ -591,7 +594,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
591
594
|
}
|
|
592
595
|
if (command.enabled) await authorizeWork(commandScope);
|
|
593
596
|
await (dependencies.setSessionAutoContinue || setSessionAutoContinue)({ root: join(root, 'delivery-hooks'),
|
|
594
|
-
provider: command.provider, session: command.session, enabled: command.enabled, continuationPolicy: command.continuationPolicy
|
|
597
|
+
provider: command.provider, session: command.session, enabled: command.enabled, continuationPolicy: command.continuationPolicy });
|
|
595
598
|
if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
|
|
596
599
|
// Acknowledge the durable preference before discovery or analysis.
|
|
597
600
|
emit({ type: "result", id: command.id, ok: true });
|
|
@@ -626,8 +629,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
626
629
|
} else if (action === "session_enroll") {
|
|
627
630
|
await (dependencies.enrollSession || enrollSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
628
631
|
session: command.session, objective: command.objective, acceptanceCriteria: command.acceptanceCriteria,
|
|
629
|
-
autonomyPolicy: command.autonomyPolicy,
|
|
630
|
-
maxContinuations: command.maxContinuations, maxFailures: command.maxFailures, maxNoProgressMinutes: command.maxNoProgressMinutes, maxTokens: command.maxTokens, continuationPolicy: command.continuationPolicy, hardMaxMinutes: command.hardMaxMinutes, hardMaxContinuations: command.hardMaxContinuations, providerTokenCeiling: command.providerTokenCeiling });
|
|
632
|
+
autonomyPolicy: command.autonomyPolicy, maxFailures: command.maxFailures, maxNoProgressMinutes: command.maxNoProgressMinutes, continuationPolicy: command.continuationPolicy });
|
|
631
633
|
await refreshSessions(true);
|
|
632
634
|
} else if (action === "session_analyze") {
|
|
633
635
|
const candidate = await selectedSession(command, { reuseDiscovery: true });
|
|
@@ -639,9 +641,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
639
641
|
await continueAnalyzedSession({ root: join(root, 'delivery-hooks'), candidate, manager,
|
|
640
642
|
readContext: dependencies.readContinuationContext || readContinuationContext,
|
|
641
643
|
enroll: async options => { await authorizeWork(commandScope); return guardedEnroll(options); }, beforeEnroll: () => authorizeWork(commandScope),
|
|
642
|
-
settings: { autonomyPolicy: command.autonomyPolicy,
|
|
643
|
-
|
|
644
|
-
maxNoProgressMinutes: command.maxNoProgressMinutes, maxTokens: command.maxTokens, continuationPolicy: command.continuationPolicy, hardMaxMinutes: command.hardMaxMinutes, hardMaxContinuations: command.hardMaxContinuations, providerTokenCeiling: command.providerTokenCeiling } });
|
|
644
|
+
settings: { autonomyPolicy: command.autonomyPolicy, maxFailures: command.maxFailures,
|
|
645
|
+
maxNoProgressMinutes: command.maxNoProgressMinutes, continuationPolicy: command.continuationPolicy } });
|
|
645
646
|
await refreshSessions(true);
|
|
646
647
|
} else if (action === "session_answer") {
|
|
647
648
|
await (dependencies.answerSession || answerSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
@@ -160,7 +160,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
160
160
|
if (closed) return;
|
|
161
161
|
const managed = record.run.managed, job = managed.actionAssociation?.job;
|
|
162
162
|
if (job && !managed.resumeLease && (!expected || TERMINAL.has(record.run.state) || ['paused', 'blocked'].includes(record.run.state) || record.run.state === 'recovery_required' && record.run.recoveryCause !== 'session_supervision' ||
|
|
163
|
-
managedBudgetReason(record.run, now()) ||
|
|
163
|
+
managedBudgetReason(record.run, now()) ||
|
|
164
164
|
managed.understandingRevision !== job.understandingRevision || record.run.revision !== job.runRevision || managed.actionAssociation.invalidated)) {
|
|
165
165
|
await cancelActionAssociation({ root, ...identity(record), expectedJobId: job.id, now: now() });
|
|
166
166
|
}
|
|
@@ -200,7 +200,10 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
200
200
|
async decorateSessions(sessions, projection) {
|
|
201
201
|
await ordinary.decorate(sessions, projection);
|
|
202
202
|
const records = projection?.byKey || new Map((await list(root)).map(record => [`${record.provider}:${record.session}`, record]));
|
|
203
|
-
const status = { runs: sessions.filter(item =>
|
|
203
|
+
const status = { runs: sessions.filter(item => {
|
|
204
|
+
const record = records.get(`${item.provider}:${item.session}`);
|
|
205
|
+
return record?.run?.managed && record.cwd === item.cwd;
|
|
206
|
+
}).map(item => {
|
|
204
207
|
const record = records.get(`${item.provider}:${item.session}`);
|
|
205
208
|
return { managed: true, taskId: record.run.managed.taskId, actionAssociation: record.run.managed.actionAssociation, candidate: item };
|
|
206
209
|
}) };
|
|
@@ -212,6 +215,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
212
215
|
const expected = projection ? projection.authRevision : await current(), record = projection
|
|
213
216
|
? projection.byKey.get(`${candidate.provider}:${candidate.session}`)
|
|
214
217
|
: await read(root, candidate.provider, candidate.session);
|
|
218
|
+
if (record?.cwd !== candidate.cwd) return null;
|
|
215
219
|
const local = record?.run?.managed?.actionAssociation || record?.actionAssociation;
|
|
216
220
|
if (!expected || projection && auth !== expected || local?.authRevision !== expected || !(candidate.actionAssociation?.canCorrect || candidate.actionAssociation?.canReadBinding)) return null;
|
|
217
221
|
const binding = candidate.actionAssociation.binding;
|
|
@@ -224,7 +228,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
224
228
|
const latest = await read(root, candidate.provider, candidate.session);
|
|
225
229
|
const scoped = latest?.run?.managed?.actionAssociation || latest?.actionAssociation;
|
|
226
230
|
if (!await valid(expected)) return false;
|
|
227
|
-
return (latest?.run?.managed?.taskId || latest?.associationTaskId) === candidate.taskId && scoped?.namespace === local.namespace && scoped?.authRevision === expected && fingerprint(scoped.binding) === fingerprint(binding);
|
|
231
|
+
return latest?.cwd === candidate.cwd && (latest?.run?.managed?.taskId || latest?.associationTaskId) === candidate.taskId && scoped?.namespace === local.namespace && scoped?.authRevision === expected && fingerprint(scoped.binding) === fingerprint(binding);
|
|
228
232
|
} };
|
|
229
233
|
|
|
230
234
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { listRecords, updateRecord, TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
2
|
+
import { listRecords, managedBudgetReason, updateRecord, TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
3
|
import { associationSignals, associationRevision } from '../codex-plugins/simy/hooks/action-association.mjs';
|
|
4
4
|
export { associationSignals, associationRevision, associationContract, validateAssociationResult } from '../codex-plugins/simy/hooks/action-association.mjs';
|
|
5
5
|
|
|
@@ -25,7 +25,7 @@ export async function queueActionAssociation({ root, provider, session, taskId,
|
|
|
25
25
|
await assertScope(validate);
|
|
26
26
|
const run = record?.run, managed = run?.managed;
|
|
27
27
|
if (!managed || managed.taskId !== taskId || run.revision !== expectedRevision) return record;
|
|
28
|
-
if (!stable(run) ||
|
|
28
|
+
if (!stable(run) || managedBudgetReason(run, now)) {
|
|
29
29
|
if (!managed.actionAssociation?.job && !managed.actionAssociation?.outbox && !managed.actionAssociation?.manualOverride) managed.actionAssociation = { ...managed.actionAssociation, status: 'waiting_boundary', retryOn: 'original_session_safe_boundary', updatedAt: now };
|
|
30
30
|
return record;
|
|
31
31
|
}
|
|
@@ -212,11 +212,20 @@ export function createOrdinaryAssociationStore({
|
|
|
212
212
|
for (const taskId of views.keys()) if (!taskIds.has(taskId) && !inFlight.has(taskId)) views.delete(taskId);
|
|
213
213
|
const pending = [];
|
|
214
214
|
for (const candidate of sessions) {
|
|
215
|
-
|
|
215
|
+
let record;
|
|
216
|
+
try {
|
|
217
|
+
record = await ensure(
|
|
216
218
|
candidate,
|
|
217
219
|
records.get(`${candidate.provider}:${candidate.session}`) || null,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// A stale workspace or unreadable local record affects only this
|
|
223
|
+
// optional association. Never rewrite its identity or stop the host.
|
|
224
|
+
candidate.actionAssociation = { status: error?.code === 'conflict' ? 'conflict' : 'unavailable',
|
|
225
|
+
binding: null, candidates: [], canCorrect: false, canReadBinding: false };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const taskId = record.run?.managed?.taskId || record.associationTaskId;
|
|
220
229
|
candidate.taskId = taskId;
|
|
221
230
|
candidate.binding = { ...candidate.binding, taskId };
|
|
222
231
|
if (record.run?.managed) continue;
|
|
@@ -5,6 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { boundConversation, CONVERSATION_LIMITS, textContent } from './conversation-context.js';
|
|
7
7
|
import { sessionTitle } from './session-titles.js';
|
|
8
|
+
import { RECENT_SESSION_LIMIT, recentSessionWindow } from './discovery-window.js';
|
|
8
9
|
|
|
9
10
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
11
|
const MAX_PROJECTS = 256;
|
|
@@ -147,14 +148,14 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
147
148
|
finally { await handle?.close(); }
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
async function collect({ home = homedir(), limit = 100, session = null, includeContext = false, cacheMetadata = false, includeStatistics = false, signal } = {}) {
|
|
151
|
+
async function collect({ home = homedir(), limit = 100, session = null, prioritySessions = [], includeContext = false, cacheMetadata = false, includeStatistics = false, signal } = {}) {
|
|
151
152
|
if (signal?.aborted) return [];
|
|
152
153
|
const root = await projectsRoot(home);
|
|
153
154
|
if (!root) return [];
|
|
154
|
-
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit,
|
|
155
|
+
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit, RECENT_SESSION_LIMIT)) : 100;
|
|
155
156
|
const observedAt = Date.now();
|
|
156
157
|
const result = new Map();
|
|
157
|
-
|
|
158
|
+
const descriptors = [], priorities = new Set(prioritySessions);
|
|
158
159
|
let projects;
|
|
159
160
|
try { projects = await readdir(root, { withFileTypes: true }); } catch { return []; }
|
|
160
161
|
for (const entry of projects.filter(item => item.isDirectory() && !item.isSymbolicLink()).slice(0, MAX_PROJECTS)) {
|
|
@@ -162,32 +163,47 @@ async function collect({ home = homedir(), limit = 100, session = null, includeC
|
|
|
162
163
|
const project = join(root, entry.name);
|
|
163
164
|
let files;
|
|
164
165
|
try { files = await readdir(project, { withFileTypes: true }); } catch { continue; }
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
const candidates = files.filter(file => file.isFile() && !file.isSymbolicLink() &&
|
|
167
|
+
file.name.endsWith('.jsonl') && UUID.test(file.name.slice(0, -'.jsonl'.length)) &&
|
|
168
|
+
(!session || file.name === `${session}.jsonl`));
|
|
169
|
+
let next = 0;
|
|
170
|
+
// Ranking uses file metadata only. Opening every transcript before slicing
|
|
171
|
+
// made a small visible catalog scan thousands of message prefixes at startup.
|
|
172
|
+
await Promise.all(Array.from({ length: Math.min(8, candidates.length) }, async () => {
|
|
173
|
+
while (next < candidates.length && !signal?.aborted) {
|
|
174
|
+
const file = candidates[next++];
|
|
175
|
+
try {
|
|
176
|
+
const info = await lstat(join(project, file.name));
|
|
177
|
+
if (info.isFile() && !info.isSymbolicLink() && info.size <= MAX_FILE_BYTES)
|
|
178
|
+
descriptors.push({ projectName: entry.name, fileName: file.name,
|
|
179
|
+
session: file.name.slice(0, -'.jsonl'.length), lastActivityAt: info.mtimeMs });
|
|
180
|
+
} catch { /* A disappearing transcript does not stop discovery. */ }
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
if (signal?.aborted) return [];
|
|
185
|
+
const selected = session ? descriptors : recentSessionWindow(descriptors, {
|
|
186
|
+
limit: maximum, priority: item => priorities.has(item.session),
|
|
187
|
+
});
|
|
188
|
+
for (const file of selected.slice(0, MAX_TRANSCRIPTS)) {
|
|
189
|
+
if (signal?.aborted) return [];
|
|
190
|
+
const candidate = await inspectTranscript(root, file.projectName, file.fileName, observedAt, includeContext, cacheMetadata, includeStatistics);
|
|
191
|
+
if (!candidate) continue;
|
|
192
|
+
const key = `${candidate.session}\0${candidate.cwd}${includeContext || includeStatistics ? `\0${file.projectName}\0${file.fileName}` : ''}`;
|
|
193
|
+
if (!result.has(key) || candidate.lastActivityAt > result.get(key).lastActivityAt) result.set(key, candidate);
|
|
178
194
|
}
|
|
179
195
|
const sorted = [...result.values()].sort((a, b) => b.lastActivityAt - a.lastActivityAt || a.session.localeCompare(b.session));
|
|
180
|
-
return session ? sorted : sorted
|
|
196
|
+
return session ? sorted : recentSessionWindow(sorted, { limit: maximum, priority: item => priorities.has(item.session) });
|
|
181
197
|
}
|
|
182
198
|
|
|
183
|
-
export async function listClaudeSessions({ home, limit, signal } = {}) {
|
|
184
|
-
return collect({ home, limit, signal, cacheMetadata: true });
|
|
199
|
+
export async function listClaudeSessions({ home, limit, prioritySessions, signal } = {}) {
|
|
200
|
+
return collect({ home, limit, prioritySessions, signal, cacheMetadata: true });
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
export async function readClaudeSession(session, { home } = {}) {
|
|
188
204
|
const id = typeof session === 'string' ? session : session?.session;
|
|
189
205
|
if (typeof id !== 'string' || !UUID.test(id)) return null;
|
|
190
|
-
const matches = await collect({ home, limit:
|
|
206
|
+
const matches = await collect({ home, limit: RECENT_SESSION_LIMIT, session: id });
|
|
191
207
|
const selected = typeof session === 'object' && typeof session.cwd === 'string'
|
|
192
208
|
? matches.filter(item => item.cwd === session.cwd) : matches;
|
|
193
209
|
return selected.length === 1 ? selected[0] : null;
|
|
@@ -2,9 +2,10 @@ import { associationContract } from '../codex-plugins/simy/hooks/action-associat
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { CODEX_WRITER_LOCK_MINIMUM_VERSION, compareVersions, inspectBackendExecutable } from '../backend-executable.js';
|
|
5
|
-
import { assessedCheckpointContract,
|
|
5
|
+
import { assessedCheckpointContract, deliveryProgressContext, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
6
6
|
import { boundConversation, boundProviderGoal, textContent } from './conversation-context.js';
|
|
7
7
|
import { JsonDataUrlFilter } from './json-data-url-filter.js';
|
|
8
|
+
import { RECENT_SESSION_LIMIT } from './discovery-window.js';
|
|
8
9
|
|
|
9
10
|
const REQUEST_TIMEOUT = 15_000;
|
|
10
11
|
const MAX_LINE = 256 * 1024;
|
|
@@ -187,14 +188,15 @@ export function codexThreadMetadata(thread) {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions =
|
|
191
|
+
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions = RECENT_SESSION_LIMIT, inspectState = false, prioritySessions = [], observationBudgetMs = 3000 } = {}) {
|
|
191
192
|
const sessions = [];
|
|
192
193
|
const windowSize = Math.min(maxSessions, maxPages * pageSize);
|
|
193
194
|
let cursor = null, listed = 0, pages = 0, batchSize = pageSize;
|
|
194
195
|
while (pages < Math.max(maxPages, Math.ceil(windowSize / batchSize)) && listed < windowSize) {
|
|
195
196
|
const limit = Math.min(batchSize, windowSize - listed);
|
|
196
197
|
let response;
|
|
197
|
-
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS,
|
|
198
|
+
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS,
|
|
199
|
+
archived: false, sortKey: 'updated_at', sortDirection: 'desc' }); }
|
|
198
200
|
catch (error) {
|
|
199
201
|
// Keep the transport's byte limit. Retry the same cursor in smaller
|
|
200
202
|
// batches rather than dropping discovery for large metadata envelopes.
|
|
@@ -394,7 +396,7 @@ export function isCodexWriterBusy(error) {
|
|
|
394
396
|
return error instanceof CodexAppServerError && error.code === -32600 && /already has an active writer/i.test(error.message);
|
|
395
397
|
}
|
|
396
398
|
|
|
397
|
-
export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {}
|
|
399
|
+
export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {} } = {}) {
|
|
398
400
|
const run = record.run, decision = run.managed?.decision;
|
|
399
401
|
if (!decision || !run.managed.autonomyPolicy) throw new CodexAppServerError('decision', 'An assessed action is required before Codex dispatch.');
|
|
400
402
|
if (!['read', 'local_edit', 'local_test'].includes(decision.action?.kind)) throw new CodexAppServerError('manual_provider_action', 'This action must be completed in the original provider conversation.');
|
|
@@ -405,7 +407,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
405
407
|
const client = await openClient({ signal });
|
|
406
408
|
let originalSandbox = null, overrideStarted = false, activeTurnId = null, turnCompleted = false, turnStart = null;
|
|
407
409
|
let primaryError = null;
|
|
408
|
-
let usage = null, lastTotal = null;
|
|
409
410
|
try {
|
|
410
411
|
const metadata = await readCodexSession(client, record.session);
|
|
411
412
|
if (await realpath(metadata.cwd) !== record.cwd || metadata.model !== run.managed.resumeModel) throw new CodexAppServerError('identity', 'Codex session workspace or model changed.');
|
|
@@ -422,8 +423,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
422
423
|
enrolledScopes: run.managed.approvalScopes,
|
|
423
424
|
lastDependencyResolution: run.managed.lastDependencyResolution,
|
|
424
425
|
...(authorizedWait ? { approvedAction: { approvalId: approved.id, action: approved.action, oneShot: true } } : {}),
|
|
425
|
-
|
|
426
|
-
localExecutionLimits: false } };
|
|
426
|
+
progress: deliveryProgressContext(run) };
|
|
427
427
|
const approvalInstruction = authorizedWait ? `SIMY recorded a matching, one-shot scoped user approval ${approved.id} for this exact selected action. The decision outcome "wait" describes its pre-approval assessment; the approved action is authorized now. Execute it once, then report what actually happened. ` : '';
|
|
428
428
|
const prompt = run.managed.phase === 'action_association' ? associationContract(run.managed.actionAssociation.job) : run.managed.phase === 'understanding' ? `Understand the ORIGINAL user request in this exact Codex conversation. This first/clarifying turn is read-only and must not perform task work. Context: ${JSON.stringify({ ...context, priorUnderstanding: run.managed.sessionUnderstanding, userAnswer: run.managed.understandingAnswer })}\n${understandingCheckpointContract(decision)} Do not write SIMY state directly.` : `Continue this existing SIMY managed task in the exact Codex conversation. Context: ${JSON.stringify(context)}\n${approvalInstruction}Execute ONLY the selected decision action. Do not perform any action from the NEXT plan in this turn. For a next local action within the enrolled task use the exact scope "task"; do not relabel external, broad, or destructive work as local. Preserve provider permissions. End with one JSON checkpoint. ${assessedCheckpointContract(decision)} Do not write SIMY state directly.`;
|
|
429
429
|
const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
|
|
@@ -439,21 +439,12 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
439
439
|
let turnId = null, completion = null, settled = false;
|
|
440
440
|
const messages = new Map();
|
|
441
441
|
const activeTools = new Set();
|
|
442
|
-
const earlyUsage = [];
|
|
443
442
|
const earlyActivity = [];
|
|
444
443
|
const observeActivity = (method, params) => {
|
|
445
444
|
if (method === 'item/started' && ['commandExecution', 'mcpToolCall', 'webSearch', 'fileChange'].includes(params.item?.type)) activeTools.add(params.item.id);
|
|
446
445
|
if (method === 'item/completed') activeTools.delete(params.item?.id);
|
|
447
446
|
onActivity({ waitingForTool: activeTools.size > 0 });
|
|
448
447
|
};
|
|
449
|
-
const observeUsage = params => {
|
|
450
|
-
const total = params.tokenUsage?.total?.totalTokens, last = params.tokenUsage?.last?.totalTokens;
|
|
451
|
-
if (!Number.isSafeInteger(total) || total < 0 || !Number.isSafeInteger(last) || last < 0 || last > total) return;
|
|
452
|
-
if (lastTotal === null) usage = { source: 'codex_app_server', tokens: last, observedAt: Date.now() };
|
|
453
|
-
else usage.tokens += Math.max(0, total - lastTotal);
|
|
454
|
-
lastTotal = Math.max(lastTotal ?? 0, total);
|
|
455
|
-
onUsage({ ...usage });
|
|
456
|
-
};
|
|
457
448
|
const cleanup = () => { unsubscribe(); signal?.removeEventListener('abort', cancelled); client.child.removeListener('close', closed); };
|
|
458
449
|
const finish = (error, value) => { if (settled) return; settled = true; cleanup(); if (error) reject(error); else resolve(value); };
|
|
459
450
|
const cancelled = () => finish(new CodexAppServerError('cancelled', 'Codex turn was cancelled.'));
|
|
@@ -474,10 +465,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
474
465
|
if (turnId) observeActivity(method, params);
|
|
475
466
|
} else if (!turnId && typeof params.turnId === 'string' && earlyActivity.length < 100) earlyActivity.push({ method, turnId: params.turnId,
|
|
476
467
|
item: { id: params.item?.id, type: params.item?.type } });
|
|
477
|
-
if (method === 'thread/tokenUsage/updated') {
|
|
478
|
-
if (turnId && params.turnId === turnId) observeUsage(params);
|
|
479
|
-
else if (!turnId && earlyUsage.length < 100) earlyUsage.push({ turnId: params.turnId, tokenUsage: params.tokenUsage });
|
|
480
|
-
}
|
|
481
468
|
if (method === 'item/completed' && params.item?.type === 'agentMessage' && typeof params.item.text === 'string' && typeof params.turnId === 'string') messages.set(params.turnId, params.item.text);
|
|
482
469
|
if (method === 'turn/completed') { completion = params; if (params.turn?.id === turnId) turnCompleted = true; complete(); }
|
|
483
470
|
});
|
|
@@ -487,14 +474,13 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
487
474
|
turnStart = client.request('turn/start', { threadId: record.session, input: [{ type: 'text', text: prompt }], cwd: record.cwd,
|
|
488
475
|
model: run.managed.resumeModel, sandboxPolicy })
|
|
489
476
|
.then(response => { turnId = response?.turn?.id; activeTurnId = turnId; if (!turnId) finish(new CodexAppServerError('turn_failed', 'Codex did not start a turn.')); else {
|
|
490
|
-
for (const params of earlyUsage) if (params.turnId === turnId) observeUsage(params);
|
|
491
477
|
for (const params of earlyActivity) if (params.turnId === turnId) observeActivity(params.method, params);
|
|
492
478
|
complete();
|
|
493
479
|
} })
|
|
494
480
|
.catch(error => finish(error));
|
|
495
481
|
});
|
|
496
482
|
const checkpoint = await done;
|
|
497
|
-
return { code: 0, checkpoint
|
|
483
|
+
return { code: 0, checkpoint };
|
|
498
484
|
} catch (error) {
|
|
499
485
|
primaryError = error;
|
|
500
486
|
throw error;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
2
|
+
|
|
3
|
+
export const RECENT_SESSION_LIMIT = 200;
|
|
4
|
+
|
|
5
|
+
// Explicit tracking and unsettled work remain visible outside recent history.
|
|
6
|
+
// Completed, untracked records are history, not an unlimited extra catalog.
|
|
7
|
+
export const retainedSession = record => Boolean(record?.autoContinue || record?.monitor || record?.tracked ||
|
|
8
|
+
record?.run && !TERMINAL.has(record.run.state));
|
|
9
|
+
|
|
10
|
+
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false } = {}) {
|
|
11
|
+
const recent = [...items].sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0) || a.session.localeCompare(b.session));
|
|
12
|
+
const retained = [], history = [];
|
|
13
|
+
for (const item of recent) (priority(item) ? retained : history).push(item);
|
|
14
|
+
return [...retained, ...history.slice(0, Math.max(0, limit - retained.length))];
|
|
15
|
+
}
|
|
@@ -12,7 +12,8 @@ function unsettled(managed) {
|
|
|
12
12
|
}
|
|
13
13
|
function exactSource(record, taskId, targetSession, now, allowStoppedPending = false) {
|
|
14
14
|
if (!record?.run?.managed || !record.admission || record.admission.session !== record.session || record.admission.cwd !== record.cwd || record.run.managed.taskId !== taskId) throw new Error('Exact managed task is unavailable.');
|
|
15
|
-
|
|
15
|
+
const stopReason = now === undefined ? null : managedBudgetReason(record.run, now);
|
|
16
|
+
if (stopReason) throw new Error(`${stopReason} Resume it in its original conversation before handing it off.`);
|
|
16
17
|
const prior = record.run.managed.handoff;
|
|
17
18
|
const transfer = prior?.status === 'received' ? null : prior;
|
|
18
19
|
if (transfer && transfer.targetSession !== targetSession) throw new Error('A different handoff is already recorded.');
|