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.
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- 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
|
-
|
|
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
|
+
}
|