@awak-app/simy-cli 0.7.29 → 0.7.32

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.
Files changed (67) hide show
  1. package/README.md +88 -3
  2. package/package.json +2 -2
  3. package/src/agent.js +8 -2
  4. package/src/cli-contract.js +1 -1
  5. package/src/cli-observability.js +1 -1
  6. package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -3
  7. package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +7 -0
  8. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +389 -51
  9. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +6 -3
  10. package/src/codex-plugins/simy/hooks/update-dispatch-gate.mjs +126 -4
  11. package/src/codex-plugins/simy/skills/continue/SKILL.md +69 -0
  12. package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +6 -0
  13. package/src/delivery-hooks/claude-workflow.md +3 -3
  14. package/src/delivery-hooks/command.js +8 -0
  15. package/src/delivery-hooks/desktop.js +31 -9
  16. package/src/delivery-hooks/supervisor.js +3 -2
  17. package/src/desktop-access.js +4 -1
  18. package/src/desktop-host.js +494 -72
  19. package/src/native-development-report-completion.js +26 -0
  20. package/src/native-development-reports.js +15 -5
  21. package/src/orchestrator/completion-contract.js +8 -2
  22. package/src/orchestrator/contract.js +3 -1
  23. package/src/orchestrator/instruction.js +1 -1
  24. package/src/runner.js +2 -1
  25. package/src/session-supervision/action-deadline.js +18 -1
  26. package/src/session-supervision/autonomous-upgrade.js +4 -2
  27. package/src/session-supervision/claude-desktop-archive.js +57 -0
  28. package/src/session-supervision/claude-desktop-native.js +50 -0
  29. package/src/session-supervision/claude-desktop-native.jxa.js +195 -0
  30. package/src/session-supervision/claude-desktop-target.js +87 -0
  31. package/src/session-supervision/claude-owner-renewal.js +197 -0
  32. package/src/session-supervision/claude-sessions.js +143 -34
  33. package/src/session-supervision/codex-app-server.js +20 -2
  34. package/src/session-supervision/continuation-analysis.js +7 -7
  35. package/src/session-supervision/continuation-command.js +45 -0
  36. package/src/session-supervision/controller-store.js +48 -0
  37. package/src/session-supervision/conversation-presentation.js +241 -0
  38. package/src/session-supervision/decision-presentation.js +96 -35
  39. package/src/session-supervision/execution-context.js +39 -0
  40. package/src/session-supervision/index.js +36 -7
  41. package/src/session-supervision/manual-acceptance-command.js +30 -0
  42. package/src/session-supervision/notification-authority.js +9 -0
  43. package/src/session-supervision/notification-journal.js +40 -0
  44. package/src/session-supervision/operation-journal.js +42 -0
  45. package/src/session-supervision/owner-transport.js +146 -0
  46. package/src/session-supervision/purpose-recovery.js +10 -2
  47. package/src/session-supervision/remote-control.js +1 -1
  48. package/src/session-supervision/remote-entry.js +2 -0
  49. package/src/session-supervision/remote-errors.js +15 -0
  50. package/src/session-supervision/remote-hosts.js +344 -36
  51. package/src/session-supervision/remote-metadata.js +103 -0
  52. package/src/session-supervision/remote-session-list.js +2 -4
  53. package/src/session-supervision/session-archive-command.js +45 -0
  54. package/src/session-supervision/session-list.js +40 -4
  55. package/src/session-supervision/sqm-presentation.js +46 -3
  56. package/src/session-supervision/stop-notifications.js +42 -11
  57. package/src/session-supervision/work-state.js +106 -6
  58. package/src/sqm/agent-skill.js +3 -3
  59. package/src/sqm/claude-plugin.js +5 -2
  60. package/src/sqm/history-store.js +6 -0
  61. package/src/team-threads.js +4 -7
  62. package/src/update-detachment/coordinator.js +7 -1
  63. package/src/update-detachment/history-recovery.js +18 -0
  64. package/src/update-detachment/ledger-adapter.js +1 -0
  65. package/src/update-detachment/retained-paused.js +290 -0
  66. package/src/update-detachment/runtime.js +78 -15
  67. package/src/web-api.js +9 -0
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { effectiveAcceptanceCriteria } from "./codex-plugins/simy/hooks/acceptance-policy.mjs";
2
3
 
3
4
  import {
4
5
  NATIVE_DEVELOPMENT_REPORT_COMPLETIONS_PATH,
@@ -141,3 +142,28 @@ function canonicalJson(value) {
141
142
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
142
143
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
143
144
  }
145
+
146
+ // v2: completion is a machine-observed controller outcome, not a request for
147
+ // human certification or a particular acceptance-document format. This never
148
+ // promotes provider reported_done or process exit to completed.
149
+ export function projectControllerCompletion(record) {
150
+ const run = record?.run;
151
+ const acceptance = run?.managed?.acceptanceCriteria;
152
+ if (acceptance !== undefined && (!Array.isArray(acceptance) ||
153
+ acceptance.some(criterion => !criterion || typeof criterion !== 'object' || Array.isArray(criterion)))) return null;
154
+ if (effectiveAcceptanceCriteria(run?.managed).some(criterion => criterion.check?.type === 'manual' &&
155
+ !projectCriterion(criterion, run.managed.verified, run.managed.criterionObservations, 0))) return null;
156
+ const taskId = run?.managed?.taskId || run?.id;
157
+ if (run?.managed?.thread && (run.managed.thread.provider !== record.provider || run.managed.thread.id !== record.session)) return null;
158
+ const recordedChecks = Object.values(run?.managed?.verified || {}).map(item => typeof item.checkedAt === 'number' ? item.checkedAt : Date.parse(item.checkedAt)).filter(Number.isFinite);
159
+ const checkedAt = normalizeIso(run?.completedAt ?? run?.updatedAt ?? run?.checkpoint?.at ?? (recordedChecks.length ? Math.max(...recordedChecks) : undefined));
160
+ if (!['codex', 'claude'].includes(record?.provider) || !UUID.test(record?.session || '') ||
161
+ run?.state !== 'completed' || run?.checkpoint?.status !== 'completed' ||
162
+ run?.recoveryCause === 'process_exit' || !UUID.test(taskId || '') || !checkedAt ||
163
+ !Number.isSafeInteger(run.revision) || run.revision < 0) return null;
164
+ const criteria = [{ id_hash: digest('controller-outcome'), source: 'controller_result', checked_at: checkedAt,
165
+ evidence_digest: digest({ taskId, revision: run.revision, checkpoint: run.checkpoint }) }];
166
+ return { provider: record.provider, provider_session_id: record.session.toLowerCase(), task_id: taskId.toLowerCase(),
167
+ managed_revision: run.revision, checkpoint_digest: digest(run.checkpoint), acceptance_digest: digest(criteria),
168
+ verified_at: checkedAt, criteria };
169
+ }
@@ -18,14 +18,16 @@ export class NativeDevelopmentReportsError extends Error {
18
18
 
19
19
  export function normalizeNativeDevelopmentReportEnrollment(value) {
20
20
  if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse();
21
- const keys = Object.keys(value).sort();
21
+ const keys = Object.keys(value).filter(key => key !== 'host_id').sort();
22
22
  const expected = ["enabled", "provider", "provider_session_id", "request_title", "revision"];
23
23
  if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw invalidResponse();
24
24
  if (!PROVIDERS.has(value.provider) || !UUID.test(value.provider_session_id) ||
25
25
  typeof value.request_title !== "string" || value.request_title.length > 160 || CONTROL.test(value.request_title) ||
26
26
  typeof value.enabled !== "boolean" || !Number.isSafeInteger(value.revision) || value.revision < 0 ||
27
27
  (value.enabled && value.revision === 0)) throw invalidResponse();
28
+ if (value.host_id !== undefined && (typeof value.host_id !== 'string' || !/^(local|remote-ssh-discovered:[a-zA-Z0-9][a-zA-Z0-9._@-]{0,127})$/.test(value.host_id))) throw invalidResponse();
28
29
  return Object.freeze({
30
+ ...(value.host_id !== undefined ? { host_id: value.host_id } : {}),
29
31
  provider: value.provider,
30
32
  provider_session_id: value.provider_session_id.toLowerCase(),
31
33
  request_title: value.request_title,
@@ -36,7 +38,7 @@ export function normalizeNativeDevelopmentReportEnrollment(value) {
36
38
 
37
39
  export function normalizeNativeDevelopmentReportSave(value) {
38
40
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new NativeDevelopmentReportsError("Invalid development report request.", { code: "invalid_native_development_report_request" });
39
- const keys = Object.keys(value).sort();
41
+ const keys = Object.keys(value).filter(key => key !== 'host_id').sort();
40
42
  const expected = ["enabled", "expected_revision", "provider", "provider_session_id", "request_title"];
41
43
  if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new NativeDevelopmentReportsError("Invalid development report request.", { code: "invalid_native_development_report_request" });
42
44
  if (!PROVIDERS.has(value.provider) || !UUID.test(value.provider_session_id) || typeof value.enabled !== "boolean" ||
@@ -46,7 +48,9 @@ export function normalizeNativeDevelopmentReportSave(value) {
46
48
  if (requestTitle.length > 160 || CONTROL.test(requestTitle) || (value.enabled && requestTitle.length === 0)) {
47
49
  throw new NativeDevelopmentReportsError("Invalid development report title.", { code: "invalid_native_development_report_request" });
48
50
  }
51
+ if (value.host_id !== undefined && (typeof value.host_id !== 'string' || !/^(local|remote-ssh-discovered:[a-zA-Z0-9][a-zA-Z0-9._@-]{0,127})$/.test(value.host_id))) throw invalidResponse();
49
52
  return Object.freeze({
53
+ ...(value.host_id !== undefined ? { host_id: value.host_id } : {}),
50
54
  provider: value.provider,
51
55
  provider_session_id: value.provider_session_id.toLowerCase(),
52
56
  request_title: requestTitle,
@@ -90,11 +94,17 @@ export function createNativeDevelopmentReportsApi({ webOrigin, readSession, fetc
90
94
  const session = await sessionForRequest();
91
95
  let response;
92
96
  try {
93
- response = await fetchImpl(webApiUrl(path, {
97
+ const url = webApiUrl(path, {
94
98
  webOrigin, apiBaseUrl: session.api_base_url,
95
- }), {
99
+ });
100
+ // Older dev gateways forward the query but omit protocol headers. Keep
101
+ // negotiation explicit without requiring a production gateway rollout.
102
+ if (url.pathname.startsWith('/api/local-cli-gateway/')) {
103
+ url.searchParams.set('native_report_version', '2');
104
+ }
105
+ response = await fetchImpl(url, {
96
106
  method,
97
- headers: webApiHeaders({ token: session.token }, body === undefined ? {} : { "Content-Type": "application/json" }),
107
+ headers: webApiHeaders({ token: session.token }, { "x-simy-native-report-version": "2", ...(body === undefined ? {} : { "Content-Type": "application/json" }) }),
98
108
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
99
109
  signal: AbortSignal.timeout(10_000),
100
110
  cache: "no-store",
@@ -135,6 +135,10 @@ export function completionEvidenceChecks(charter, attempt) {
135
135
 
136
136
  const rows = acceptanceCriterionRows(charter);
137
137
  const criterionEvidence = normalizeCriterionEvidence(attempt?.criterion_evidence);
138
+ const outcome = attempt?.completion_result || attempt?.raw_output?.completion_result;
139
+ // The worker can describe its actual end-to-end check once. Dedicated rows
140
+ // are optional; a concrete failed row always defeats a broad success claim.
141
+ const wholeOutcome = hasUserOutcomeProof({ user_outcome: outcome });
138
142
  const known = new Set(rows.map(row => row.id));
139
143
  const checks = [check("criterion_evidence_scope", "Results belong to the requested behavior",
140
144
  criterionEvidence.every(row => known.has(row.criterion_id)), "Do not substitute an unrelated result for the request.")];
@@ -147,7 +151,7 @@ export function completionEvidenceChecks(charter, attempt) {
147
151
  `criterion_evidence_${row.id.toLowerCase()}`,
148
152
  `${row.id} has passing completion evidence`,
149
153
  Boolean(
150
- matches.length === 1 &&
154
+ matches.length === 0 ? wholeOutcome : matches.length === 1 &&
151
155
  evidence?.passed &&
152
156
  evidence.verification_method &&
153
157
  evidence.observed_result,
@@ -158,7 +162,9 @@ export function completionEvidenceChecks(charter, attempt) {
158
162
  ? `${evidence.verification_method || "method missing"}: ${
159
163
  evidence.observed_result || "observed result missing"
160
164
  }; evidence ${evidence.evidence_refs.join(", ") || "missing"}.`
161
- : `No criterion evidence was recorded for ${row.id}: ${row.statement}`,
165
+ : wholeOutcome
166
+ ? `${outcome.verification_method}: ${outcome.observed_result}`
167
+ : `The requested behavior has not been verified: ${row.statement}`,
162
168
  evidence?.evidence_refs || [],
163
169
  ),
164
170
  );
@@ -28,7 +28,9 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
28
28
  const charterContext = record(request.charter_context);
29
29
  const classifiedRisk = inheritRisk(classifyRisk(request), record(charterContext.risk));
30
30
  const risk = { ...classifiedRisk, requires_design_review: false,
31
- required_controls: classifiedRisk.required_controls.filter(control => control !== "design_review") };
31
+ required_controls: classifiedRisk.required_controls.filter(control => control !== "design_review" &&
32
+ (control !== "independent_ai_audit" || request.independent_review_required === true) &&
33
+ (control !== "human_peer_approval" || request.require_human_approval === true || charterContext.require_human_approval === true)) };
32
34
  const inheritedDesignReview = record(charterContext.design_review);
33
35
  const inheritedPromptPolicyReport = record(charterContext.prompt_policy_report);
34
36
  const inheritedEvidencePolicy = record(charterContext.evidence_policy);
@@ -27,7 +27,7 @@ export function buildCodingInstruction(
27
27
  section("work", "Read repository instructions and preserve existing user changes. Choose the smallest useful implementation. Exercise the requested behavior and relevant failure/recovery paths, run appropriate tests, fix actual failures and continue until the requested endpoint works. For UI work, use the interface and judge ease, clarity and accuracy. Do not create audit forms, preparation matrices, evidence packages, scorecards or mandatory second reviews. Do not invent work to fill a checklist. Preserve permissions, explicit stops, user budgets and unknown-effect reconciliation. Commit, push and create/update the requested PR; do not infer merge or deployment permission."),
28
28
  section("remaining", "Optional history: provider-reported data, not independent acceptance. Human guidance is included below.\n" + JSON.stringify({ previousSummary: previousAttempt?.summary, reasoning: previousAttempt?.problem_solving,
29
29
  failures: previousFindings, guidance: promptInterventions.map(row => row.prompt) })),
30
- section("result_contract", 'Finish with SIMY_RESULT_JSON: and a JSON object containing summary, branch_name, commit_sha, pr_url, pr_base_branch, tests_run, tests_passed, and criterion_evidence. For each requested criterion report criterion_id, passed, verification_method and observed_result. These describe the checks actually performed; do not invent success. Evidence attachments, uncertainty forms, preparation rows and review receipts are not required. Report an actual blocker or unfinished behavior clearly.'),
30
+ section("result_contract", 'Finish with SIMY_RESULT_JSON: and a JSON object containing summary, branch_name, commit_sha, pr_url, pr_base_branch, tests_run, tests_passed, and completion_result:{passed,verification_method,observed_result} describing the actual whole requested outcome. Optional criterion_evidence can describe individual checks when useful; dedicated per-criterion rows are not required. These describe the checks actually performed; do not invent success. Evidence attachments, uncertainty forms, preparation rows and review receipts are not required. Report an actual blocker or unfinished behavior clearly.'),
31
31
  ].join("\n\n");
32
32
  }
33
33
 
package/src/runner.js CHANGED
@@ -216,7 +216,8 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
216
216
  ? charter.expected_evidence
217
217
  : [],
218
218
  required_checks: Array.isArray(charter.required_checks) ? charter.required_checks : [],
219
- require_human_approval: charter.require_human_approval !== false,
219
+ require_human_approval: charter.require_human_approval === true,
220
+ independent_review_required: charter.independent_review_required === true,
220
221
  must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
221
222
  proposal_id: typeof charter.proposal_id === "string" ? charter.proposal_id : null,
222
223
  workflow_observation_binding:
@@ -339,5 +339,22 @@ export function createActionDeadlineService({
339
339
  async function read(context) {
340
340
  return project(await store.read(identity(context)), context);
341
341
  }
342
- return { reconcile, read, decide };
342
+ async function importSnapshot(context, snapshot) {
343
+ const current = await reconcile(context, { displayOnly: true });
344
+ const decision = snapshot?.decision;
345
+ if (JSON.stringify(snapshot?.identity) !== JSON.stringify(identity(context)) ||
346
+ snapshot.fingerprint !== current.fingerprint || !current.reminder ||
347
+ !decision || !options.some(option => option.id === decision.choice) ||
348
+ typeof decision.requestId !== 'string' || !decision.requestId ||
349
+ !Number.isSafeInteger(decision.revision) || decision.revision < 1 ||
350
+ decision.reminderKey !== current.reminder.key) return current;
351
+ const state = await transact(context, previous => {
352
+ if (!previous || previous.decision || previous.fingerprint !== snapshot.fingerprint) return previous;
353
+ return { ...previous, decision: { id: decision.requestId, requestId: decision.requestId,
354
+ choice: decision.choice, revision: decision.revision, reminderKey: previous.reminder.key,
355
+ createdAt: decision.createdAt, remoteApplied: false }, migratedFromExecutionHost: true };
356
+ });
357
+ return project(state, context);
358
+ }
359
+ return { reconcile, read, decide, importSnapshot };
343
360
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { automaticContinuationAllowed, listRecords, updateRecord, queuePurposeDiagnosis, queuePurposeVerification, managedBudgetReason } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
2
+ import { automaticContinuationAllowed, listRecords, updateRecord, queuePurposeDiagnosis, queuePurposeVerification, managedBudgetReason, diagnoseControllerApproval } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
3
3
  import { autonomousPolicyAuthorized, policyContractIdentity, acceptanceIdentity, legacyIndependentCriterion, effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
4
4
  import { purposeFailureMode } from '../codex-plugins/simy/hooks/purpose-search.mjs';
5
5
 
@@ -93,6 +93,8 @@ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
93
93
  run.reason = 'Monitoring the original dependency and permitted independent work; no authority or prior effect is assumed.';
94
94
  m.purpose.nextDependencyCheckAt = now;
95
95
  }
96
+ const priorApproval = m.pendingApproval;
97
+ if (run.state === 'blocked') diagnoseControllerApproval(record, now);
96
98
  const canRecover = metadata.isRunning === false && !m.pendingApproval && !m.userQuestion &&
97
99
  !m.understandingQuestion && !m.dependencyWait && !m.providerWarning && !m.nativeWait && !m.executionUncertain;
98
100
  if (canRecover && run.state === 'paused' && pauseOrigin(run) === 'system') {
@@ -102,7 +104,7 @@ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
102
104
  } else if (canRecover && run.state === 'verification_pending' && effectiveAcceptanceCriteria(m).some(c => c.check.type === 'semantic' && !m.verified[c.id])) {
103
105
  queuePurposeVerification(run, 'Existing outcome awaits independent evidence inspection; inspect original instructions and actual artifacts, not a worker completion claim.', now);
104
106
  }
105
- return { changed: before !== after || run.state !== previousState, reason: 'applied', originalContract: original };
107
+ return { changed: before !== after || run.state !== previousState || Boolean(priorApproval && !m.pendingApproval), reason: 'applied', originalContract: original };
106
108
  }
107
109
 
108
110
  // Explicit CLI user control; never inferred from installation, ON, descriptions
@@ -0,0 +1,57 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { createClaudeDesktopNativeAdapter } from './claude-desktop-native.js';
3
+ import { resolveClaudeDesktopTarget } from './claude-desktop-target.js';
4
+
5
+ const failure = (code, message) => Object.assign(new Error(message), { code });
6
+ const sameIdentity = (a, b) => ['desktopId', 'session', 'cwd', 'partition'].every(key => a[key] === b[key]);
7
+
8
+ // One native Desktop window is shared by all conversations. Serialize operations
9
+ // across identities; a per-session queue would allow one operation to navigate
10
+ // away from the other operation's target.
11
+ export function createClaudeDesktopArchiveController({
12
+ readTarget = resolveClaudeDesktopTarget,
13
+ native = createClaudeDesktopNativeAdapter(),
14
+ wait = delay,
15
+ attempts = 60,
16
+ interval = 250,
17
+ } = {}) {
18
+ if (!Number.isInteger(attempts) || attempts < 1 || attempts > 120 || !Number.isFinite(interval) || interval < 0 || interval > 5000)
19
+ throw new TypeError('Invalid Claude Desktop readback bounds.');
20
+ let queue = Promise.resolve();
21
+ return function setArchived(candidate, archived, { signal, beforeDispatch = async () => {} } = {}) {
22
+ // Copy the caller's selection before queueing: later catalog refreshes must
23
+ // not redirect an already requested native operation.
24
+ const selection = { provider: candidate?.provider, session: candidate?.session, cwd: candidate?.cwd };
25
+ const run = async () => {
26
+ if (typeof archived !== 'boolean') throw new TypeError('Expected an explicit archive state.');
27
+ signal?.throwIfAborted();
28
+ const original = await readTarget(selection);
29
+ if (original.archived === archived) return { state: 'verified', archived, changed: false };
30
+ if (!native?.setArchived) throw failure('claude_desktop_unavailable', 'Native Claude Desktop archive control is unavailable on this host.');
31
+ await beforeDispatch();
32
+ signal?.throwIfAborted();
33
+ const current = await readTarget(selection);
34
+ if (!sameIdentity(original, current)) throw failure('claude_desktop_identity_changed', 'Claude Desktop conversation identity changed.');
35
+ if (current.archived === archived) return { state: 'verified', archived, changed: false };
36
+ // Dispatch at most once. A timeout may mean the provider applied the
37
+ // operation; reconcile its own state rather than blindly clicking again.
38
+ let dispatchError;
39
+ try { await native.setArchived(current, archived, { signal }); }
40
+ catch (error) { dispatchError = error; }
41
+ const deadline = Date.now() + 15_000;
42
+ for (let index = 0; index < attempts; index++) {
43
+ const observed = await readTarget(selection);
44
+ if (!sameIdentity(original, observed)) throw failure('claude_desktop_identity_changed', 'Claude Desktop conversation identity changed during native control.');
45
+ if (observed.archived === archived) return { state: 'verified', archived, changed: true };
46
+ signal?.throwIfAborted();
47
+ if (dispatchError?.mutationPossible === false || Date.now() >= deadline) break;
48
+ if (index + 1 < attempts) await wait(interval, undefined, { signal });
49
+ }
50
+ if (dispatchError) throw dispatchError;
51
+ throw failure('claude_desktop_state_unconfirmed', 'Claude Desktop has not confirmed the requested archive state. Refresh before retrying.');
52
+ };
53
+ const result = queue.then(run);
54
+ queue = result.catch(() => {});
55
+ return result;
56
+ };
57
+ }
@@ -0,0 +1,50 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { fileURLToPath } from 'node:url';
4
+ const execute = promisify(execFile);
5
+ const script = fileURLToPath(new URL('./claude-desktop-native.jxa.js', import.meta.url));
6
+ const failure = (code, message) => Object.assign(new Error(message), { code });
7
+
8
+ export function createClaudeDesktopNativeAdapter({ platform = process.platform, run = execute } = {}) {
9
+ async function invoke(request, { signal } = {}) {
10
+ if (platform !== 'darwin') throw failure('claude_desktop_platform_unsupported', 'Claude Desktop native archive control currently requires a macOS conversation host.');
11
+ signal?.throwIfAborted();
12
+ let stdout;
13
+ try {
14
+ ({ stdout } = await run('/usr/bin/osascript', ['-l', 'JavaScript', script, JSON.stringify(request)], {
15
+ encoding: 'utf8', timeout: ['inspect', 'preflight'].includes(request.operation) ? 5000 : 20_000, maxBuffer: 32_768, killSignal: 'SIGKILL', signal,
16
+ }));
17
+ } catch (error) {
18
+ signal?.throwIfAborted();
19
+ throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop did not return a result. Refresh its archive state before retrying.');
20
+ }
21
+ let response;
22
+ try { response = JSON.parse(stdout); } catch { throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop returned an unreadable result.'); }
23
+ if (response?.ok !== true) {
24
+ const code = /^claude_desktop_[a-z_]+$/.test(response?.code || '') ? response.code : 'claude_desktop_ui_unavailable';
25
+ throw Object.assign(failure(code, typeof response?.message === 'string' && response.message.length <= 1000 ? response.message : 'Claude Desktop native control is unavailable.'), { mutationPossible: response.mutationPossible !== false });
26
+ }
27
+ return response.data;
28
+ }
29
+ return {
30
+ inspect: options => invoke({ operation: 'inspect' }, options),
31
+ setup: options => invoke({ operation: 'setup' }, options),
32
+ async setArchived(target, archived, options) {
33
+ if (typeof archived !== 'boolean') throw new TypeError('An explicit archive state is required.');
34
+ if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target?.desktopId || '')) throw failure('claude_desktop_identity_unverified', 'An exact Claude Desktop identity is required.');
35
+ const readiness = await invoke({ operation: 'inspect' }, options);
36
+ if (readiness?.available !== true) throw failure('claude_desktop_accessibility_required', 'Enable Accessibility for the app running SIMY on the conversation host, then retry.');
37
+ const preflight = await invoke({ operation: 'preflight' }, options);
38
+ if (preflight?.safeToNavigate !== true) throw Object.assign(failure('claude_desktop_ui_unavailable', 'Claude Desktop navigation safety could not be confirmed.'), { mutationPossible: false });
39
+ try { await run('/usr/bin/open', ['-b', 'com.anthropic.claudefordesktop', 'claude://claude.ai/epitaxy/' + target.desktopId], {
40
+ encoding: 'utf8', timeout: 5000, maxBuffer: 4096, killSignal: 'SIGKILL', signal: options?.signal,
41
+ }); } catch {
42
+ options?.signal?.throwIfAborted();
43
+ throw Object.assign(failure('claude_desktop_ui_unavailable', 'Claude Desktop could not open the selected conversation. Open its Code tab and retry.'), { mutationPossible: false });
44
+ }
45
+ const result = await invoke({ operation: archived ? 'archive' : 'restore', desktopId: target.desktopId, title: target.title }, options);
46
+ if (result?.dispatched !== true) throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop did not confirm dispatch.');
47
+ return result;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,195 @@
1
+ // macOS system JavaScript for Automation. No compiler, developer mode, private
2
+ // provider file writes, or embedded credentials are required on the user's Mac.
3
+ ObjC.import('AppKit');
4
+ ObjC.import('ApplicationServices');
5
+ ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['int']]);
6
+ ObjC.bindFunction('AXUIElementCreateSystemWide', ['id', []]);
7
+ ObjC.bindFunction('AXUIElementCopyAttributeValue', ['int', ['id', 'id', 'id *']]);
8
+ ObjC.bindFunction('AXUIElementCopyElementAtPosition', ['int', ['id', 'float', 'float', 'id *']]);
9
+ ObjC.bindFunction('AXValueGetValue', ['bool', ['id', 'int', 'void *']]);
10
+ ObjC.bindFunction('AXIsProcessTrustedWithOptions', ['bool', ['id']]);
11
+ ObjC.bindFunction('AXUIElementPerformAction', ['int', ['id', 'id']]);
12
+ ObjC.bindFunction('AXUIElementSetAttributeValue', ['int', ['id', 'id', 'id']]);
13
+ ObjC.bindFunction('CFEqual', ['bool', ['id', 'id']]);
14
+
15
+ function pause(seconds) { $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(seconds)); }
16
+ function failure(code, message) { var error = new Error(message); error.code = code; throw error; }
17
+ function attribute(element, name, requireReadable) {
18
+ var result = Ref(), status = $.AXUIElementCopyAttributeValue(element, $(name), result);
19
+ // A leaf may omit optional children. Transport/stale-element errors do not
20
+ // prove that a subtree is empty and must not allow navigation past drafts.
21
+ if (requireReadable && status !== 0 && status !== Number($.kAXErrorAttributeUnsupported) && status !== Number($.kAXErrorNoValue))
22
+ failure('claude_desktop_ui_unavailable', 'Claude Desktop accessibility content could not be inspected.');
23
+ return status === 0 ? result[0] : null;
24
+ }
25
+ function textAttribute(element, name) {
26
+ var value = attribute(element, name);
27
+ return value ? (name === 'AXURL' ? String(ObjC.unwrap(value.absoluteString)) : String(ObjC.unwrap(value))) : '';
28
+ }
29
+ function descendants(root, predicate) {
30
+ var result = [], count = 0;
31
+ function visit(element, depth) {
32
+ if (++count > 12000 || depth > 55) failure('claude_desktop_ui_unavailable', 'Claude Desktop accessibility tree exceeded its supported bounds.');
33
+ if (predicate(element)) result.push(element);
34
+ var children = attribute(element, 'AXChildren', true);
35
+ if (children) ObjC.unwrap(children).forEach(function(child) { visit(child, depth + 1); });
36
+ }
37
+ visit(root, 0); return result;
38
+ }
39
+ function hasAncestor(element, predicate) {
40
+ for (var index = 0; index < 60 && element; index++, element = attribute(element, 'AXParent')) if (predicate(element)) return true;
41
+ return false;
42
+ }
43
+ // AXValue stores two CGFloat doubles on supported 64-bit Macs. Foundation owns
44
+ // the full 16-byte output buffer; do not write a CGPoint into an untyped Ref.
45
+ function geometry(element, name, type) {
46
+ var data = $.NSMutableData.dataWithLength(16), value = attribute(element, name);
47
+ if (!value || !$.AXValueGetValue(value, type, data.mutableBytes)) failure('claude_desktop_ui_unavailable', 'Claude control geometry is unavailable.');
48
+ var encoded = String(ObjC.unwrap(data.base64EncodedStringWithOptions(0)));
49
+ var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', bytes = [], bits = 0, accumulated = 0;
50
+ for (var index = 0; index < encoded.length; index++) {
51
+ var digit = alphabet.indexOf(encoded[index]); if (digit < 0) break;
52
+ accumulated = (accumulated << 6) | digit; bits += 6;
53
+ if (bits >= 8) { bits -= 8; bytes.push((accumulated >> bits) & 255); }
54
+ }
55
+ if (bytes.length !== 16) failure('claude_desktop_ui_unavailable', 'Invalid Claude control geometry.');
56
+ var view = new DataView(new Uint8Array(bytes).buffer);
57
+ return [view.getFloat64(0, true), view.getFloat64(8, true)];
58
+ }
59
+ function ensureNoActiveWork(root) {
60
+ descendants(root, function(element) {
61
+ var role = textAttribute(element, 'AXRole');
62
+ if (!role) failure('claude_desktop_ui_unavailable', 'Claude Desktop element identity could not be inspected.');
63
+ if (['AXSheet', 'AXDialog'].indexOf(role) >= 0 || textAttribute(element, 'AXSubrole') === 'AXDialog')
64
+ failure('claude_desktop_ui_changed', 'Close the Claude Desktop dialog before changing conversations.');
65
+ if (role === 'AXButton' && ['Stop', 'Stop response', 'Stop generating', '停止', '応答を停止', '生成を停止'].indexOf(textAttribute(element, 'AXTitle') || textAttribute(element, 'AXDescription')) >= 0)
66
+ failure('claude_desktop_running', 'Claude Desktop is generating a response. Wait before changing conversations.');
67
+ if (['AXTextArea', 'AXTextField', 'AXComboBox'].indexOf(role) >= 0) {
68
+ var value = attribute(element, 'AXValue');
69
+ if (value === null) failure('claude_desktop_ui_unavailable', 'Claude Desktop input state could not be inspected.');
70
+ if (String(ObjC.unwrap(value)).trim()) failure('claude_desktop_unsaved_input', 'Claude Desktop contains unsent input. Preserve it before changing conversations.');
71
+ }
72
+ return false;
73
+ });
74
+ }
75
+ function perform(input) {
76
+ if (!input || ['inspect', 'setup', 'preflight', 'archive', 'restore'].indexOf(input.operation) < 0) failure('claude_desktop_invalid_request', 'Invalid Claude Desktop operation.');
77
+ var bundle = 'com.anthropic.claudefordesktop', workspace = $.NSWorkspace.sharedWorkspace;
78
+ var appURL = workspace.URLForApplicationWithBundleIdentifier(bundle);
79
+ if (!appURL || appURL.isNil()) failure('claude_desktop_not_installed', 'Install Claude Desktop on the conversation host and sign in to its Code tab.');
80
+ var version = String(ObjC.unwrap($.NSBundle.bundleWithURL(appURL).objectForInfoDictionaryKey('CFBundleShortVersionString')));
81
+ if (input.operation === 'setup') {
82
+ $.AXIsProcessTrustedWithOptions($({ AXTrustedCheckOptionPrompt: true }));
83
+ // Open the real OS setting. Never grant accessibility or change TCC records.
84
+ workspace.openURL($.NSURL.URLWithString('x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'));
85
+ }
86
+ var trusted = Boolean($.AXIsProcessTrusted());
87
+ if (['inspect', 'setup'].indexOf(input.operation) >= 0) return { available: trusted, version: version, code: trusted ? 'ready' : 'claude_desktop_accessibility_required' };
88
+ if (!trusted) failure('claude_desktop_accessibility_required', 'Enable Accessibility for the app running SIMY in macOS System Settings, then retry. Run simy sessions provider-setup --provider claude on this host to open the setting.');
89
+ if (input.operation === 'preflight') {
90
+ var existingApps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle));
91
+ if (existingApps.length === 0) return { safeToNavigate: true };
92
+ if (existingApps.length !== 1) failure('claude_desktop_ui_unavailable', 'Claude Desktop application identity is ambiguous.');
93
+ var existingAX = $.AXUIElementCreateApplication(existingApps[0].processIdentifier);
94
+ // Electron may initially expose only its window chrome. Request its AX
95
+ // tree without navigating or granting any OS permission, then wait briefly
96
+ // for content. Unsupported attributes are harmless; actual reads decide.
97
+ $.AXUIElementSetAttributeValue(existingAX, $('AXManualAccessibility'), $(true));
98
+ for (var attempt = 0; attempt < 11; attempt++) {
99
+ var windows = attribute(existingAX, 'AXWindows', true);
100
+ if (!windows || ObjC.unwrap(windows).length === 0) failure('claude_desktop_ui_unavailable', 'Claude Desktop windows could not be inspected before navigation.');
101
+ var ready = true;
102
+ ObjC.unwrap(windows).forEach(function(window) {
103
+ ensureNoActiveWork(window);
104
+ if (descendants(window, function(element) { return textAttribute(element, 'AXRole') === 'AXWebArea'; }).length === 0) ready = false;
105
+ });
106
+ if (ready) return { safeToNavigate: true };
107
+ if (attempt < 10) pause(0.2);
108
+ }
109
+ failure('claude_desktop_ui_unavailable', 'Claude Desktop conversation content could not be inspected before navigation.');
110
+ }
111
+ if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input.desktopId || '') || typeof input.title !== 'string' || !input.title.trim() || input.title.length > 512)
112
+ failure('claude_desktop_invalid_request', 'An exact Claude Desktop conversation is required.');
113
+ var apps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle));
114
+ if (apps.length === 0) {
115
+ workspace.openURL(appURL);
116
+ for (var launch = 0; launch < 30 && apps.length === 0; launch++) { pause(0.2); apps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle)); }
117
+ }
118
+ if (apps.length !== 1) failure('claude_desktop_ui_unavailable', 'Open a single Claude Desktop application and retry.');
119
+ var running = apps[0], app = $.AXUIElementCreateApplication(running.processIdentifier);
120
+ // The resume/import deep link may change archive state. Use navigation only.
121
+ // The parent opens the navigation URL through LaunchServices before invoking us.
122
+ // LaunchServices already activates the deep-linked window; let its AX tree
123
+ // settle without issuing another application activation.
124
+ pause(0.2);
125
+ function targetArea() {
126
+ var matches = descendants(app, function(element) {
127
+ return textAttribute(element, 'AXRole') === 'AXWebArea' && ['https://claude.ai/epitaxy/' + input.desktopId, 'https://claude.ai/code/' + input.desktopId].indexOf(textAttribute(element, 'AXURL')) >= 0;
128
+ });
129
+ if (matches.length !== 1) failure('claude_desktop_ui_unavailable', 'The exact Claude Desktop conversation is not uniquely visible. Open its Code tab and retry.');
130
+ return matches[0];
131
+ }
132
+ function ensureActionTarget(element) {
133
+ var currentArea = targetArea();
134
+ if (!running.active) failure('claude_desktop_ui_changed', 'Claude Desktop lost focus before the operation. Retry when it is visible.');
135
+ if (!hasAncestor(element, function(parent) { return $.CFEqual(parent, currentArea); }))
136
+ failure('claude_desktop_ui_changed', 'The selected Claude control no longer belongs to the requested conversation. Refresh before retrying.');
137
+ }
138
+ function click(element) {
139
+ ensureActionTarget(element);
140
+ var position = geometry(element, 'AXPosition', 1), size = geometry(element, 'AXSize', 2);
141
+ var x = position[0] + size[0] / 2, y = position[1] + size[1] / 2, hit = Ref();
142
+ if (!isFinite(x) || !isFinite(y) || size[0] <= 0 || size[1] <= 0) failure('claude_desktop_ui_unavailable', 'Claude control is not visible.');
143
+ var status = $.AXUIElementCopyElementAtPosition($.AXUIElementCreateSystemWide(), x, y, hit);
144
+ if (status !== 0 || !hasAncestor(hit[0], function(parent) { return $.CFEqual(parent, element); }))
145
+ failure('claude_desktop_ui_changed', 'Another window or control covers the selected Claude action. Retry when it is visible.');
146
+ var point = $.CGPointMake(x, y);
147
+ $.CGEventPost(0, $.CGEventCreateMouseEvent(null, 1, point, 0));
148
+ $.CGEventPost(0, $.CGEventCreateMouseEvent(null, 2, point, 0));
149
+ }
150
+ var area;
151
+ for (var attempt = 0; attempt < 40; attempt++) {
152
+ try { area = targetArea(); break; } catch (error) { if (attempt === 39) throw error; pause(0.2); }
153
+ }
154
+ // Do not archive an actively generating turn or accept a provider permission,
155
+ // discard, or trust dialog. Those belong to the provider, not to SIMY.
156
+ function ensureIdle() {
157
+ ensureNoActiveWork(app);
158
+ }
159
+ ensureIdle();
160
+ var controls;
161
+ if (input.operation === 'archive') {
162
+ var labels = [input.title + 'のその他のオプション', 'More options for ' + input.title];
163
+ controls = descendants(area, function(element) {
164
+ return textAttribute(element, 'AXRole') === 'AXPopUpButton' && labels.indexOf(textAttribute(element, 'AXDescription')) >= 0 && !hasAncestor(element, function(parent) {
165
+ return ['サイドバー', 'Sidebar'].indexOf(textAttribute(parent, 'AXDescription')) >= 0 || textAttribute(parent, 'AXDOMIdentifier') === 'frame-peek-popover';
166
+ });
167
+ });
168
+ if (controls.length !== 1) failure('claude_desktop_ui_unavailable', 'The exact Claude conversation menu is unavailable or ambiguous.');
169
+ var popup = controls[0];
170
+ function archiveItems() {
171
+ return descendants(targetArea(), function(element) {
172
+ return textAttribute(element, 'AXRole') === 'AXMenuItem' && ['アーカイブ', 'Archive'].indexOf(textAttribute(element, 'AXTitle')) >= 0 && hasAncestor(element, function(parent) { return labels.indexOf(textAttribute(parent, 'AXTitle')) >= 0; });
173
+ });
174
+ }
175
+ controls = archiveItems();
176
+ if (controls.length === 0) {
177
+ ensureActionTarget(popup);
178
+ $.AXUIElementPerformAction(popup, $('AXPress')); pause(0.6);
179
+ controls = archiveItems();
180
+ if (controls.length === 0) { click(popup); pause(0.6); controls = archiveItems(); }
181
+ }
182
+ } else {
183
+ controls = descendants(area, function(element) { return textAttribute(element, 'AXRole') === 'AXButton' && ['アーカイブ解除', 'Unarchive'].indexOf(textAttribute(element, 'AXTitle')) >= 0; });
184
+ }
185
+ if (controls.length !== 1) failure('claude_desktop_ui_unavailable', 'The native archive or restore action is unavailable or ambiguous.');
186
+ ensureIdle();
187
+ ensureActionTarget(controls[0]);
188
+ if ($.AXUIElementPerformAction(controls[0], $('AXPress')) !== 0) failure('claude_desktop_native_unconfirmed', 'The native Claude action did not return a confirmed result.');
189
+ // Only the caller's independent provider readback can report success.
190
+ return { dispatched: true, version: version };
191
+ }
192
+ function run(argv) {
193
+ try { return JSON.stringify({ ok: true, data: perform(JSON.parse(argv[0])) }); }
194
+ catch (error) { return JSON.stringify({ ok: false, mutationPossible: !error.code || error.code === 'claude_desktop_native_unconfirmed', code: error.code || 'claude_desktop_ui_unavailable', message: error.code ? error.message : 'Claude Desktop native control is unavailable. Open its Code tab and retry.' }); }
195
+ }
@@ -0,0 +1,87 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, readdir } from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+ import { claudeDesktopSessionRoot } from './claude-desktop-lineage.js';
7
+
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const LOCAL = /^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const fail = message => Object.assign(new Error(message), { code: 'claude_desktop_identity_unverified' });
11
+ const changed = () => Object.assign(fail('Claude Desktop metadata changed.'), { retryable: true });
12
+ const fingerprint = stat => [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':');
13
+
14
+ async function directory(path) {
15
+ const stat = await lstat(path);
16
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw fail('Claude Desktop storage is not a regular directory.');
17
+ }
18
+
19
+ // Mutation identity must be resolved from a complete scan, not the bounded,
20
+ // eventually refreshed catalog. Do not expose account settings or transcript text.
21
+ export async function resolveClaudeDesktopTarget(candidate, { home = homedir(), root = claudeDesktopSessionRoot(home) } = {}) {
22
+ if (candidate?.provider !== 'claude' || !UUID.test(candidate.session || '') || !isAbsolute(candidate.cwd || ''))
23
+ throw fail('Select an exact Claude conversation.');
24
+ await directory(root);
25
+ const matches = []; let inspected = 0;
26
+ const accounts = await readdir(root, { withFileTypes: true });
27
+ for (const account of accounts.filter(item => UUID.test(item.name))) {
28
+ const accountPath = join(root, account.name); await directory(accountPath);
29
+ for (const org of (await readdir(accountPath, { withFileTypes: true })).filter(item => UUID.test(item.name))) {
30
+ const partition = join(accountPath, org.name); await directory(partition);
31
+ const entries = (await readdir(partition, { withFileTypes: true })).filter(entry => entry.name.endsWith('.json') && LOCAL.test(entry.name.slice(0, -5)));
32
+ inspected += entries.length;
33
+ if (inspected > 16384) throw fail('Claude Desktop identity scan exceeded its limit.');
34
+ const readEntry = async entry => {
35
+ const path = join(partition, entry.name), before = await lstat(path, { bigint: true });
36
+ if (!before.isFile() || before.isSymbolicLink() || before.size > 262144n) throw fail('Claude Desktop metadata cannot be verified.');
37
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
38
+ let item;
39
+ try {
40
+ if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before)) throw changed();
41
+ // Bound allocation even if the provider replaces/grows metadata while
42
+ // it is being read. Read one extra byte to detect oversized content.
43
+ const bytes = Buffer.alloc(262145);
44
+ let length = 0;
45
+ while (length < bytes.length) {
46
+ const { bytesRead } = await handle.read(bytes, length, bytes.length - length, length);
47
+ if (!bytesRead) break;
48
+ length += bytesRead;
49
+ }
50
+ if (length > 262144) throw fail('Claude Desktop metadata exceeds its size limit.');
51
+ if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before) || fingerprint(await lstat(path, { bigint: true })) !== fingerprint(before)) throw changed();
52
+ try { item = JSON.parse(bytes.subarray(0, length).toString('utf8')); }
53
+ catch { throw fail('Claude Desktop metadata is invalid JSON.'); }
54
+ if (!item || typeof item !== 'object' || Array.isArray(item)) throw fail('Claude Desktop metadata is invalid.');
55
+ if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before) || fingerprint(await lstat(path, { bigint: true })) !== fingerprint(before))
56
+ throw changed();
57
+ } finally { await handle.close(); }
58
+ if (item.cliSessionId !== candidate.session) return;
59
+ // Include competing claims even when their cwd differs. A CLI UUID alone
60
+ // must never select a Desktop account or organization implicitly.
61
+ matches.push({ desktopId: item.sessionId, session: item.cliSessionId, cwd: item.cwd,
62
+ title: item.title, archived: item.isArchived,
63
+ partition: createHash('sha256').update(account.name + '/' + org.name).digest('hex'),
64
+ fileId: entry.name.slice(0, -5) });
65
+ };
66
+ // Bound concurrent descriptors while avoiding a serial full scan for each
67
+ // readback. Wait for every opened descriptor to close before failing.
68
+ for (let offset = 0; offset < entries.length; offset += 8) {
69
+ const results = await Promise.allSettled(entries.slice(offset, offset + 8).map(async entry => {
70
+ for (let attempt = 0; ; attempt++) {
71
+ try { return await readEntry(entry); }
72
+ catch (error) { if (!error.retryable || attempt >= 2) throw error; }
73
+ }
74
+ }));
75
+ const failed = results.find(result => result.status === 'rejected');
76
+ if (failed) throw failed.reason;
77
+ }
78
+ }
79
+ }
80
+ if (matches.length !== 1) throw fail('Claude conversation is missing or belongs to multiple Desktop records.');
81
+ const target = matches[0];
82
+ if (target.desktopId !== target.fileId || !LOCAL.test(target.desktopId || '') || !isAbsolute(target.cwd || '') || resolve(target.cwd) !== resolve(candidate.cwd) ||
83
+ typeof target.archived !== 'boolean' || typeof target.title !== 'string' || !target.title.trim() || target.title.length > 512)
84
+ throw fail('Claude Desktop identity or archive state does not match the selected conversation.');
85
+ const { fileId, ...verified } = target;
86
+ return verified;
87
+ }