shennian 0.3.85 → 0.3.86

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 (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +28 -28
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
@@ -1,2 +1,569 @@
1
- import p from"node:crypto";import l from"node:fs";import f from"node:path";import{sessionPreviewFromPayload as L}from"@shennian/wire";import{resolvePersonalSyncV1Path as R}from"../personal-sync/paths.js";import{getCanonicalSessionStore as P,getDaemonInstallationId as H,listSessionRecords as x,recordSession as w}from"../session/store.js";import{readToolDetail as U,writeToolDetail as J}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as V}from"./native-source-deletions.js";import{recordPersonalSyncLocalMetric as v}from"../personal-sync/metrics.js";const I=1;class q extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class W extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class z extends Error{sourceEventKey;code="native_event_identity_conflict";constructor(e){super(`native event identity conflict: ${e}`),this.sourceEventKey=e,this.name="NativeEventIdentityConflictError"}}class de{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}updateMessageDeliveryState(e,s,o){if(!this.canonicalStore().updateMessageDeliveryState(e,s,o))return;const i=(this.options.listLocalSessions??x)().find(a=>a.sessionId===e);if(!i)return;(this.options.recordLocalSession??w)({...i})}commitEvent(e){if(G(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new W(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=Y(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=l.existsSync(a)?D(a):null;if(r&&r.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(r&&r.eventFingerprint!==c)throw new z(e.sourceEventKey);if(!i&&!r&&e.role!=="user")throw new q(e.sourceSessionKey);const m=e.claimSessionId??null,d=i?.sessionId??r?.sessionId??m??Q(s,e.agentType,e.sourceSessionKey);if(m&&i&&i.sessionId!==m)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const h=this.canonicalStore(),y=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",u=r?.canonicalMessageId??(y?null:e.claimCanonicalMessageId??X(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let S=h.getManifest(d)?.bodyRevision??0,T=!!r;const E=!y&&!!e.claimCanonicalMessageId;if(E){if(!u)throw new Error("managed echo claim requires a canonical message");const M=h.getMessageState(d,u);if(!M||M.tombstoned)throw new Error(`managed echo claim target is not durable: ${u}`)}if(!r&&!E&&!y){if(!u)throw new Error("native event requires a canonical message");const M=e.canonicalMessageKey?h.getMessageState(d,u):null,F=e.canonicalMutation==="upsert"||!!M,$=F?M??h.getMessageState(d,u):null,_={id:u,sessionId:d,role:e.role,ts:$?.message?.ts??e.ts,payload:Z($?.message?.payload,e.payload)},C=F?h.upsert(d,_):h.append(d,_);if(S=C.bodyRevision,T=C.duplicate,e.localToolDetail){const N=this.options.writeLocalToolDetail??J,j=this.options.readLocalToolDetail??U,B=ee(j(d,e.localToolDetail.toolCallId),e.localToolDetail);N({sessionId:d,...B,ts:e.ts,detailRef:{toolIndex:0}})}}e.terminal&&(S=h.completeStartedUserMessages(d,e.ts).at(-1)?.bodyRevision??S),(!r||e.terminal)&&!y&&this.recordIndex(e,d,u,S);const K=new Date().toISOString();return r||g(a,{schemaVersion:I,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:u,canonicalRevision:S,cursor:e.cursor??null,mappedBy:y?"managed_echo_suppressed":E?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:K}),o.sources[n]={schemaVersion:I,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:r?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:r?i?.lastCursor??r.cursor:e.cursor??i?.lastCursor??null,bodyRevision:S,createdAt:i?.createdAt??K,updatedAt:K},g(this.sourcesFile(),o),!r&&e.cursor&&v("sourceCursorAdvanced"),!r&&!E&&!y&&!T&&v("canonicalCommits"),{sessionId:d,messageId:u,bodyRevision:S,duplicate:T,claimed:E,suppressed:y}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??x,s=this.options.recordLocalSession??w,o=this.canonicalStore();let n=0;for(const i of e()){if(i.localReady)continue;const a=o.getManifest(i.sessionId)?.firstAvailableSeq;if(a==null)continue;const r=o.readPage(i.sessionId,{limit:1,beforeSequence:a+1}).entries[0];!r||r.sequence!==a||r.message.role!=="user"||(s({sessionId:i.sessionId,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,managerDefaultWorkerAgentType:i.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:i.managerDefaultWorkerModelId,status:i.status,lastActivityAt:i.lastActivityAt,lastMessagePreview:i.lastMessagePreview,localReady:!0,firstUserMessageId:r.message.id,firstUserBodyRevision:r.revision}),n+=1)}return n}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){A(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,g(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const n=this.readManagedEchoState();let i=!1;for(const c of Object.values(n.claims))c.sessionId!==e||c.sourceAgentType!==s||(c.sourceSessionKey=o,i=!0);i&&g(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],g(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(k(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),g(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],g(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[a,r]of Object.entries(s.sources))r.sessionId===e&&(delete s.sources[a],o=!0);o&&g(this.sourcesFile(),s);const n=this.readManagedEchoState();let i=!1;for(const[a,r]of Object.entries(n.claims))r.sessionId===e&&(delete n.claims[a],i=!0);for(const[a,r]of Object.entries(n.suppressions))r.sessionId===e&&(delete n.suppressions[a],i=!0);i&&g(this.managedEchoStateFile(),n);const c=f.join(this.rootDir(),"ledger");if(l.existsSync(c))for(const a of l.readdirSync(c,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const r=f.join(c,a.name),m=D(r);if(m.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");m.sessionId===e&&l.unlinkSync(r)}}isSourceDeleted(e,s){return(this.options.deletionStore??new V({rootDir:f.basename(this.rootDir())==="native-fusion"?f.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){const n=o.trim().slice(0,120);if(!n)return!1;const i=this.readSources().sources[b(e,s)];if(!i||this.isSourceDeleted(e,s))return!1;const c=(this.options.listLocalSessions??x)().find(m=>m.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=p.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??w)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:s,title:n,titleSource:"agent_native",titleOriginEventId:`agent-native:${a}`}),!0}recordIndex(e,s,o,n){const i=this.options.recordLocalSession??w,c=e.terminal?`terminal:${p.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0,a=e.role==="user"||e.role==="agent"?O(e.payload):null,r=a&&o?{lastMessagePreview:a,lastMessagePreviewMessageId:o,lastMessagePreviewAt:new Date(e.ts).toISOString()}:{};i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?e.terminalState==="failed"||e.terminalState==="aborted"?"failed":"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),...r,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?O(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!l.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),sources:{}};const s=D(e);if(s.schemaVersion!==I||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??H()}canonicalStore(){return this.options.canonicalStore??P()}rootDir(){return this.options.rootDir??R("native-fusion")}sourcesFile(){return f.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return f.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!l.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=D(e);if(s.schemaVersion!==I||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))A(o);for(const o of Object.values(s.suppressions))k(o);return s}ledgerFile(e,s){const o=p.createHash("sha256").update(`${e}\0${s}`).digest("hex");return f.join(this.rootDir(),"ledger",`${o}.json`)}}function G(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return p.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function Q(t,e,s){return`native-session-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function X(t,e,s){return`native-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function Y(t){return p.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function A(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function k(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function Z(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const n=typeof o.name=="string"?o.name:"",i=/(?:_output|_result)$/.test(n);return JSON.stringify({...s,...o,...i&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function ee(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function O(t){return L(t,120)}function g(t,e){const s=f.dirname(t);l.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;let n;try{n=l.openSync(o,"wx",384),l.writeFileSync(n,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),l.fsyncSync(n),l.closeSync(n),n=void 0,l.renameSync(o,t)}finally{n!==void 0&&l.closeSync(n);try{l.unlinkSync(o)}catch{}}}function D(t){return JSON.parse(l.readFileSync(t,"utf8"))}export{q as MissingUserBoundaryError,z as NativeEventIdentityConflictError,de as NativeFusionLocalRepository,W as NativeSourceDeletedError};
1
+ // @arch docs/architecture/cli/native-session-fusion.md#2-本地-source-binding-与事件账本
2
+ // @test src/__tests__/native-fusion-local-repository.test.ts
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { sessionPreviewFromPayload, } from '@shennian/wire';
7
+ import { resolvePersonalSyncV1Path } from '../personal-sync/paths.js';
8
+ import { getCanonicalSessionStore, getDaemonInstallationId, listSessionRecords, recordSession, } from '../session/store.js';
9
+ import { readToolDetail, writeToolDetail } from '../session/tool-detail-store.js';
10
+ import { NativeSourceDeletionStore } from './native-source-deletions.js';
11
+ import { recordPersonalSyncLocalMetric } from '../personal-sync/metrics.js';
12
+ const SCHEMA_VERSION = 1;
13
+ export class MissingUserBoundaryError extends Error {
14
+ code = 'missing_user_boundary';
15
+ constructor(sourceSessionKey) {
16
+ super(`native source ${sourceSessionKey} does not start at a visible user boundary`);
17
+ this.name = 'MissingUserBoundaryError';
18
+ }
19
+ }
20
+ export class NativeSourceDeletedError extends Error {
21
+ code = 'native_source_deleted';
22
+ constructor(sourceSessionKey) {
23
+ super(`native source ${sourceSessionKey} is blocked by a durable deletion tombstone`);
24
+ this.name = 'NativeSourceDeletedError';
25
+ }
26
+ }
27
+ export class NativeEventIdentityConflictError extends Error {
28
+ sourceEventKey;
29
+ code = 'native_event_identity_conflict';
30
+ constructor(sourceEventKey) {
31
+ super(`native event identity conflict: ${sourceEventKey}`);
32
+ this.sourceEventKey = sourceEventKey;
33
+ this.name = 'NativeEventIdentityConflictError';
34
+ }
35
+ }
36
+ export class NativeFusionLocalRepository {
37
+ options;
38
+ constructor(options = {}) {
39
+ this.options = options;
40
+ }
41
+ commitEvents(events) {
42
+ return [...events]
43
+ .sort((left, right) => left.ts - right.ts)
44
+ .map((event) => this.commitEvent(event));
45
+ }
46
+ updateMessageDeliveryState(sessionId, messageId, deliveryState) {
47
+ const committed = this.canonicalStore().updateMessageDeliveryState(sessionId, messageId, deliveryState);
48
+ if (!committed)
49
+ return;
50
+ const record = (this.options.listLocalSessions ?? listSessionRecords)()
51
+ .find((item) => item.sessionId === sessionId);
52
+ if (!record)
53
+ return;
54
+ const persist = this.options.recordLocalSession ?? recordSession;
55
+ persist({
56
+ ...record,
57
+ });
58
+ }
59
+ commitEvent(event) {
60
+ validateEvent(event);
61
+ if (this.isSourceDeleted(event.agentType, event.sourceSessionKey)) {
62
+ throw new NativeSourceDeletedError(event.sourceSessionKey);
63
+ }
64
+ const installationId = this.installationId();
65
+ const sourceFile = this.readSources();
66
+ const sourceKey = stableSourceKey(event.agentType, event.sourceSessionKey);
67
+ const existingBinding = sourceFile.sources[sourceKey];
68
+ const fingerprint = eventFingerprint(event);
69
+ const ledgerFile = this.ledgerFile(event.agentType, event.sourceEventKey);
70
+ const existingLedger = fs.existsSync(ledgerFile) ? readJson(ledgerFile) : null;
71
+ if (existingLedger && existingLedger.daemonInstallationId !== installationId) {
72
+ throw new Error('native event ledger belongs to another daemon installation');
73
+ }
74
+ if (existingLedger && existingLedger.eventFingerprint !== fingerprint) {
75
+ throw new NativeEventIdentityConflictError(event.sourceEventKey);
76
+ }
77
+ if (!existingBinding && !existingLedger && event.role !== 'user') {
78
+ throw new MissingUserBoundaryError(event.sourceSessionKey);
79
+ }
80
+ const claimedSessionId = event.claimSessionId ?? null;
81
+ const sessionId = existingBinding?.sessionId
82
+ ?? existingLedger?.sessionId
83
+ ?? claimedSessionId
84
+ ?? stableSessionId(installationId, event.agentType, event.sourceSessionKey);
85
+ if (claimedSessionId && existingBinding && existingBinding.sessionId !== claimedSessionId) {
86
+ throw new Error(`managed claim session conflicts with source binding ${event.sourceSessionKey}`);
87
+ }
88
+ const store = this.canonicalStore();
89
+ const suppressed = event.localDisposition === 'managed_echo_suppressed'
90
+ || existingLedger?.mappedBy === 'managed_echo_suppressed';
91
+ const messageId = existingLedger?.canonicalMessageId
92
+ ?? (suppressed ? null : event.claimCanonicalMessageId
93
+ ?? stableMessageId(installationId, event.agentType, event.canonicalMessageKey ?? event.sourceEventKey));
94
+ let bodyRevision = store.getManifest(sessionId)?.bodyRevision ?? 0;
95
+ let duplicate = Boolean(existingLedger);
96
+ const claimed = !suppressed && Boolean(event.claimCanonicalMessageId);
97
+ if (claimed) {
98
+ if (!messageId)
99
+ throw new Error('managed echo claim requires a canonical message');
100
+ const claimedState = store.getMessageState(sessionId, messageId);
101
+ if (!claimedState || claimedState.tombstoned) {
102
+ throw new Error(`managed echo claim target is not durable: ${messageId}`);
103
+ }
104
+ }
105
+ if (!existingLedger && !claimed && !suppressed) {
106
+ if (!messageId)
107
+ throw new Error('native event requires a canonical message');
108
+ // Codex can emit the same logical tool call more than once (for example,
109
+ // a replayed running frame before its completed frame). The source-event
110
+ // ledger is keyed by the physical rollout line, while canonical identity
111
+ // is keyed by canonicalMessageKey. Recover from that split-brain window
112
+ // by updating an already durable logical message instead of aborting the
113
+ // whole scanner batch with an append conflict.
114
+ const durableLogicalMessage = event.canonicalMessageKey
115
+ ? store.getMessageState(sessionId, messageId)
116
+ : null;
117
+ const shouldUpsert = event.canonicalMutation === 'upsert' || Boolean(durableLogicalMessage);
118
+ const previous = shouldUpsert
119
+ ? durableLogicalMessage ?? store.getMessageState(sessionId, messageId)
120
+ : null;
121
+ const envelope = {
122
+ id: messageId,
123
+ sessionId,
124
+ role: event.role,
125
+ ts: previous?.message?.ts ?? event.ts,
126
+ payload: mergeToolSkeleton(previous?.message?.payload, event.payload),
127
+ };
128
+ const committed = shouldUpsert
129
+ ? store.upsert(sessionId, envelope)
130
+ : store.append(sessionId, envelope);
131
+ bodyRevision = committed.bodyRevision;
132
+ duplicate = committed.duplicate;
133
+ if (event.localToolDetail) {
134
+ const persistDetail = this.options.writeLocalToolDetail ?? writeToolDetail;
135
+ const readDetail = this.options.readLocalToolDetail ?? readToolDetail;
136
+ const detail = mergeToolDetail(readDetail(sessionId, event.localToolDetail.toolCallId), event.localToolDetail);
137
+ persistDetail({
138
+ sessionId,
139
+ ...detail,
140
+ ts: event.ts,
141
+ detailRef: { toolIndex: 0 },
142
+ });
143
+ }
144
+ }
145
+ if (event.terminal) {
146
+ bodyRevision = store.completeStartedUserMessages(sessionId, event.ts).at(-1)?.bodyRevision ?? bodyRevision;
147
+ }
148
+ if ((!existingLedger || event.terminal) && !suppressed) {
149
+ this.recordIndex(event, sessionId, messageId, bodyRevision);
150
+ }
151
+ const now = new Date().toISOString();
152
+ if (!existingLedger) {
153
+ atomicWriteJson(ledgerFile, {
154
+ schemaVersion: SCHEMA_VERSION,
155
+ daemonInstallationId: installationId,
156
+ sessionId,
157
+ agentType: event.agentType,
158
+ sourceSessionKey: event.sourceSessionKey,
159
+ sourceEventKey: event.sourceEventKey,
160
+ eventFingerprint: fingerprint,
161
+ canonicalMessageId: messageId,
162
+ canonicalRevision: bodyRevision,
163
+ cursor: event.cursor ?? null,
164
+ mappedBy: suppressed
165
+ ? 'managed_echo_suppressed'
166
+ : claimed ? 'managed_echo_claim' : event.sourceMode,
167
+ eventTs: event.ts,
168
+ createdAt: now,
169
+ });
170
+ }
171
+ sourceFile.sources[sourceKey] = {
172
+ schemaVersion: SCHEMA_VERSION,
173
+ daemonInstallationId: installationId,
174
+ sessionId,
175
+ agentType: event.agentType,
176
+ sourceSessionKey: event.sourceSessionKey,
177
+ sourceMode: existingLedger ? existingBinding?.sourceMode ?? event.sourceMode : event.sourceMode,
178
+ lastCursor: existingLedger
179
+ ? existingBinding?.lastCursor ?? existingLedger.cursor
180
+ : event.cursor ?? existingBinding?.lastCursor ?? null,
181
+ bodyRevision,
182
+ createdAt: existingBinding?.createdAt ?? now,
183
+ updatedAt: now,
184
+ };
185
+ atomicWriteJson(this.sourcesFile(), sourceFile);
186
+ if (!existingLedger && event.cursor)
187
+ recordPersonalSyncLocalMetric('sourceCursorAdvanced');
188
+ if (!existingLedger && !claimed && !suppressed && !duplicate) {
189
+ recordPersonalSyncLocalMetric('canonicalCommits');
190
+ }
191
+ return { sessionId, messageId, bodyRevision, duplicate, claimed, suppressed };
192
+ }
193
+ repairMissingUserBoundaries() {
194
+ const list = this.options.listLocalSessions ?? listSessionRecords;
195
+ const record = this.options.recordLocalSession ?? recordSession;
196
+ const store = this.canonicalStore();
197
+ let repaired = 0;
198
+ for (const existing of list()) {
199
+ if (existing.localReady)
200
+ continue;
201
+ const manifest = store.getManifest(existing.sessionId);
202
+ const firstSequence = manifest?.firstAvailableSeq;
203
+ if (firstSequence == null)
204
+ continue;
205
+ const first = store.readPage(existing.sessionId, {
206
+ limit: 1,
207
+ beforeSequence: firstSequence + 1,
208
+ }).entries[0];
209
+ if (!first || first.sequence !== firstSequence || first.message.role !== 'user')
210
+ continue;
211
+ record({
212
+ sessionId: existing.sessionId,
213
+ agentType: existing.agentType,
214
+ sessionMode: existing.sessionMode,
215
+ managerConfig: existing.managerConfig,
216
+ workDir: existing.workDir,
217
+ agentSessionId: existing.agentSessionId,
218
+ modelId: existing.modelId,
219
+ managerDefaultWorkerAgentType: existing.managerDefaultWorkerAgentType,
220
+ managerDefaultWorkerModelId: existing.managerDefaultWorkerModelId,
221
+ status: existing.status,
222
+ lastActivityAt: existing.lastActivityAt,
223
+ lastMessagePreview: existing.lastMessagePreview,
224
+ localReady: true,
225
+ firstUserMessageId: first.message.id,
226
+ firstUserBodyRevision: first.revision,
227
+ });
228
+ repaired += 1;
229
+ }
230
+ return repaired;
231
+ }
232
+ listManagedClaims() {
233
+ return Object.values(this.readManagedEchoState().claims);
234
+ }
235
+ listManagedSuppressions() {
236
+ return Object.values(this.readManagedEchoState().suppressions);
237
+ }
238
+ upsertManagedClaim(claim) {
239
+ validateManagedClaim(claim);
240
+ const state = this.readManagedEchoState();
241
+ state.claims[claim.canonicalMessageId] = claim;
242
+ atomicWriteJson(this.managedEchoStateFile(), state);
243
+ }
244
+ updateManagedClaimSource(sessionId, agentType, sourceSessionKey) {
245
+ const state = this.readManagedEchoState();
246
+ let changed = false;
247
+ for (const claim of Object.values(state.claims)) {
248
+ if (claim.sessionId !== sessionId || claim.sourceAgentType !== agentType)
249
+ continue;
250
+ claim.sourceSessionKey = sourceSessionKey;
251
+ changed = true;
252
+ }
253
+ if (changed)
254
+ atomicWriteJson(this.managedEchoStateFile(), state);
255
+ }
256
+ removeManagedClaim(canonicalMessageId) {
257
+ const state = this.readManagedEchoState();
258
+ if (!state.claims[canonicalMessageId])
259
+ return;
260
+ delete state.claims[canonicalMessageId];
261
+ atomicWriteJson(this.managedEchoStateFile(), state);
262
+ }
263
+ completeManagedClaim(canonicalMessageId, suppression) {
264
+ const state = this.readManagedEchoState();
265
+ delete state.claims[canonicalMessageId];
266
+ if (suppression) {
267
+ validateSuppression(suppression);
268
+ state.suppressions[stableSourceKey(suppression.agentType, suppression.sourceSessionKey)] = suppression;
269
+ }
270
+ atomicWriteJson(this.managedEchoStateFile(), state);
271
+ }
272
+ removeManagedSuppression(agentType, sourceSessionKey) {
273
+ const state = this.readManagedEchoState();
274
+ const key = stableSourceKey(agentType, sourceSessionKey);
275
+ if (!state.suppressions[key])
276
+ return;
277
+ delete state.suppressions[key];
278
+ atomicWriteJson(this.managedEchoStateFile(), state);
279
+ }
280
+ listSourceBindings() {
281
+ return Object.values(this.readSources().sources)
282
+ .filter((binding) => !this.isSourceDeleted(binding.agentType, binding.sourceSessionKey));
283
+ }
284
+ purgeSession(sessionId) {
285
+ if (!sessionId)
286
+ throw new Error('sessionId is required');
287
+ const sourceFile = this.readSources();
288
+ let sourceChanged = false;
289
+ for (const [key, binding] of Object.entries(sourceFile.sources)) {
290
+ if (binding.sessionId !== sessionId)
291
+ continue;
292
+ delete sourceFile.sources[key];
293
+ sourceChanged = true;
294
+ }
295
+ if (sourceChanged)
296
+ atomicWriteJson(this.sourcesFile(), sourceFile);
297
+ const managedState = this.readManagedEchoState();
298
+ let managedChanged = false;
299
+ for (const [key, claim] of Object.entries(managedState.claims)) {
300
+ if (claim.sessionId !== sessionId)
301
+ continue;
302
+ delete managedState.claims[key];
303
+ managedChanged = true;
304
+ }
305
+ for (const [key, suppression] of Object.entries(managedState.suppressions)) {
306
+ if (suppression.sessionId !== sessionId)
307
+ continue;
308
+ delete managedState.suppressions[key];
309
+ managedChanged = true;
310
+ }
311
+ if (managedChanged)
312
+ atomicWriteJson(this.managedEchoStateFile(), managedState);
313
+ const ledgerDirectory = path.join(this.rootDir(), 'ledger');
314
+ if (!fs.existsSync(ledgerDirectory))
315
+ return;
316
+ for (const entry of fs.readdirSync(ledgerDirectory, { withFileTypes: true })) {
317
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
318
+ continue;
319
+ const file = path.join(ledgerDirectory, entry.name);
320
+ const ledger = readJson(file);
321
+ if (ledger.daemonInstallationId !== this.installationId()) {
322
+ throw new Error('native event ledger belongs to another daemon installation');
323
+ }
324
+ if (ledger.sessionId === sessionId)
325
+ fs.unlinkSync(file);
326
+ }
327
+ }
328
+ isSourceDeleted(agentType, sourceSessionKey) {
329
+ const store = this.options.deletionStore ?? new NativeSourceDeletionStore({
330
+ rootDir: path.basename(this.rootDir()) === 'native-fusion'
331
+ ? path.dirname(this.rootDir())
332
+ : this.rootDir(),
333
+ daemonInstallationId: this.installationId(),
334
+ });
335
+ return store.has(agentType, sourceSessionKey);
336
+ }
337
+ observeNativeTitle(agentType, sourceSessionKey, titleValue) {
338
+ const title = titleValue.trim().slice(0, 120);
339
+ if (!title)
340
+ return false;
341
+ const binding = this.readSources().sources[stableSourceKey(agentType, sourceSessionKey)];
342
+ if (!binding || this.isSourceDeleted(agentType, sourceSessionKey))
343
+ return false;
344
+ const existing = (this.options.listLocalSessions ?? listSessionRecords)()
345
+ .find((record) => record.sessionId === binding.sessionId);
346
+ if (existing?.titleLockedByUser || (existing?.titleSource === 'agent_native' && existing.title === title))
347
+ return false;
348
+ const originHash = crypto.createHash('sha256')
349
+ .update(`${agentType}\0${sourceSessionKey}\0${title}`)
350
+ .digest('hex');
351
+ const record = this.options.recordLocalSession ?? recordSession;
352
+ record({
353
+ sessionId: binding.sessionId,
354
+ agentType: binding.agentType,
355
+ workDir: existing?.workDir ?? '',
356
+ agentSessionId: sourceSessionKey,
357
+ title,
358
+ titleSource: 'agent_native',
359
+ titleOriginEventId: `agent-native:${originHash}`,
360
+ });
361
+ return true;
362
+ }
363
+ recordIndex(event, sessionId, messageId, bodyRevision) {
364
+ const record = this.options.recordLocalSession ?? recordSession;
365
+ const attentionId = event.terminal
366
+ ? `terminal:${crypto.createHash('sha256').update(event.sourceEventKey).digest('hex')}`
367
+ : undefined;
368
+ const nextPreview = event.role === 'user' || event.role === 'agent'
369
+ ? preview(event.payload)
370
+ : null;
371
+ const previewTuple = nextPreview && messageId
372
+ ? {
373
+ lastMessagePreview: nextPreview,
374
+ lastMessagePreviewMessageId: messageId,
375
+ lastMessagePreviewAt: new Date(event.ts).toISOString(),
376
+ }
377
+ : {};
378
+ record({
379
+ sessionId,
380
+ agentType: event.agentType,
381
+ workDir: event.workDir ?? '',
382
+ agentSessionId: event.sourceSessionKey,
383
+ modelId: event.modelId ?? null,
384
+ status: event.terminal
385
+ ? event.terminalState === 'failed' || event.terminalState === 'aborted'
386
+ ? 'failed'
387
+ : 'completed'
388
+ : 'active',
389
+ attentionId,
390
+ lastActivityAt: new Date(event.ts).toISOString(),
391
+ ...previewTuple,
392
+ ...(event.role === 'user' && messageId
393
+ ? {
394
+ localReady: true,
395
+ firstUserMessageId: messageId,
396
+ firstUserBodyRevision: bodyRevision,
397
+ }
398
+ : {}),
399
+ title: event.title?.trim() || (event.role === 'user' ? preview(event.payload) ?? '' : undefined),
400
+ titleSource: event.title?.trim()
401
+ ? event.titleSource ?? 'first_user_fallback'
402
+ : 'first_user_fallback',
403
+ titleOriginEventId: event.sourceEventKey,
404
+ });
405
+ }
406
+ readSources() {
407
+ const file = this.sourcesFile();
408
+ if (!fs.existsSync(file)) {
409
+ return { schemaVersion: SCHEMA_VERSION, daemonInstallationId: this.installationId(), sources: {} };
410
+ }
411
+ const parsed = readJson(file);
412
+ if (parsed.schemaVersion !== SCHEMA_VERSION
413
+ || parsed.daemonInstallationId !== this.installationId() || !parsed.sources) {
414
+ throw new Error('invalid native source binding store');
415
+ }
416
+ for (const binding of Object.values(parsed.sources)) {
417
+ if (binding.daemonInstallationId !== this.installationId()) {
418
+ throw new Error('native source binding belongs to another daemon installation');
419
+ }
420
+ }
421
+ return parsed;
422
+ }
423
+ installationId() {
424
+ return this.options.daemonInstallationId ?? getDaemonInstallationId();
425
+ }
426
+ canonicalStore() {
427
+ return this.options.canonicalStore ?? getCanonicalSessionStore();
428
+ }
429
+ rootDir() {
430
+ return this.options.rootDir ?? resolvePersonalSyncV1Path('native-fusion');
431
+ }
432
+ sourcesFile() {
433
+ return path.join(this.rootDir(), 'sources.json');
434
+ }
435
+ managedEchoStateFile() {
436
+ return path.join(this.rootDir(), 'managed-echo-state.json');
437
+ }
438
+ readManagedEchoState() {
439
+ const file = this.managedEchoStateFile();
440
+ if (!fs.existsSync(file)) {
441
+ return {
442
+ schemaVersion: SCHEMA_VERSION,
443
+ daemonInstallationId: this.installationId(),
444
+ claims: {},
445
+ suppressions: {},
446
+ };
447
+ }
448
+ const state = readJson(file);
449
+ if (state.schemaVersion !== SCHEMA_VERSION
450
+ || state.daemonInstallationId !== this.installationId()
451
+ || !state.claims || !state.suppressions)
452
+ throw new Error('invalid managed echo state');
453
+ for (const claim of Object.values(state.claims))
454
+ validateManagedClaim(claim);
455
+ for (const suppression of Object.values(state.suppressions))
456
+ validateSuppression(suppression);
457
+ return state;
458
+ }
459
+ ledgerFile(agentType, sourceEventKey) {
460
+ const name = crypto.createHash('sha256').update(`${agentType}\0${sourceEventKey}`).digest('hex');
461
+ return path.join(this.rootDir(), 'ledger', `${name}.json`);
462
+ }
463
+ }
464
+ function validateEvent(event) {
465
+ if (!event.agentType || !event.sourceSessionKey || !event.sourceEventKey || !event.payload) {
466
+ throw new Error('invalid native event');
467
+ }
468
+ if (!Number.isFinite(event.ts))
469
+ throw new Error('invalid native event timestamp');
470
+ }
471
+ function stableSourceKey(agentType, sourceSessionKey) {
472
+ return crypto.createHash('sha256').update(`${agentType}\0${sourceSessionKey}`).digest('hex');
473
+ }
474
+ function stableSessionId(installationId, agentType, sourceSessionKey) {
475
+ const digest = crypto.createHash('sha256')
476
+ .update(`${installationId}\0${agentType}\0${sourceSessionKey}`).digest('hex').slice(0, 32);
477
+ return `native-session-${digest}`;
478
+ }
479
+ function stableMessageId(installationId, agentType, sourceEventKey) {
480
+ const digest = crypto.createHash('sha256')
481
+ .update(`${installationId}\0${agentType}\0${sourceEventKey}`).digest('hex').slice(0, 32);
482
+ return `native-${digest}`;
483
+ }
484
+ function eventFingerprint(event) {
485
+ return crypto.createHash('sha256').update(JSON.stringify({
486
+ agentType: event.agentType,
487
+ sourceSessionKey: event.sourceSessionKey,
488
+ sourceEventKey: event.sourceEventKey,
489
+ role: event.role,
490
+ ts: event.ts,
491
+ payload: event.payload,
492
+ canonicalMessageKey: event.canonicalMessageKey ?? null,
493
+ canonicalMutation: event.canonicalMutation ?? 'append',
494
+ })).digest('hex');
495
+ }
496
+ function validateManagedClaim(claim) {
497
+ if (!claim.sessionId || !claim.canonicalMessageId || !claim.textHash
498
+ || !Number.isFinite(claim.createdAt)
499
+ || !['codex', 'claude', 'opencode'].includes(claim.sourceAgentType)
500
+ || !['suppress_following', 'import_following'].includes(claim.managedEchoPolicy))
501
+ throw new Error('invalid managed echo claim');
502
+ }
503
+ function validateSuppression(window) {
504
+ if (!window.sessionId || !window.sourceSessionKey || !window.claimSourceEventKey
505
+ || !['codex', 'claude', 'opencode'].includes(window.agentType)
506
+ || !Number.isFinite(window.startedAt) || !Number.isFinite(window.endsAt)
507
+ || window.endsAt < window.startedAt)
508
+ throw new Error('invalid managed echo suppression');
509
+ }
510
+ function mergeToolSkeleton(previousPayload, incomingPayload) {
511
+ if (!previousPayload)
512
+ return incomingPayload;
513
+ try {
514
+ const previous = JSON.parse(previousPayload);
515
+ const incoming = JSON.parse(incomingPayload);
516
+ if (previous.type !== 'tool' || incoming.type !== 'tool' ||
517
+ previous.toolCallId !== incoming.toolCallId)
518
+ return incomingPayload;
519
+ const incomingName = typeof incoming.name === 'string' ? incoming.name : '';
520
+ const usePreviousName = /(?:_output|_result)$/.test(incomingName);
521
+ return JSON.stringify({
522
+ ...previous,
523
+ ...incoming,
524
+ ...(usePreviousName && typeof previous.name === 'string' ? { name: previous.name } : {}),
525
+ });
526
+ }
527
+ catch {
528
+ return incomingPayload;
529
+ }
530
+ }
531
+ function mergeToolDetail(previous, incoming) {
532
+ if (!previous || previous.toolCallId !== incoming.toolCallId)
533
+ return incoming;
534
+ const usePreviousName = /(?:_output|_result)$/.test(incoming.name);
535
+ return {
536
+ ...incoming,
537
+ name: usePreviousName ? previous.name : incoming.name,
538
+ ...(incoming.args === undefined && previous.args !== undefined ? { args: previous.args } : {}),
539
+ ...(incoming.result === undefined && previous.result !== undefined ? { result: previous.result } : {}),
540
+ };
541
+ }
542
+ function preview(payload) {
543
+ return sessionPreviewFromPayload(payload, 120);
544
+ }
545
+ function atomicWriteJson(file, value) {
546
+ const directory = path.dirname(file);
547
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
548
+ const temp = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
549
+ let descriptor;
550
+ try {
551
+ descriptor = fs.openSync(temp, 'wx', 0o600);
552
+ fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
553
+ fs.fsyncSync(descriptor);
554
+ fs.closeSync(descriptor);
555
+ descriptor = undefined;
556
+ fs.renameSync(temp, file);
557
+ }
558
+ finally {
559
+ if (descriptor !== undefined)
560
+ fs.closeSync(descriptor);
561
+ try {
562
+ fs.unlinkSync(temp);
563
+ }
564
+ catch { }
565
+ }
566
+ }
567
+ function readJson(file) {
568
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
569
+ }