@lqc123qwe/car-runtime 1.22.1 → 1.24.0

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/SHA256SUMS CHANGED
@@ -1 +1 @@
1
- d2873814312c5dd65480de476fb2cceab7b0ec59be92f37fb7a7d6fd4a61bf82 car-runtime-1.22.1.tgz
1
+ aa80b65782ac20a0d01289545992a9f89c745fd342eeb5c2c4f41bd84e0ff920 car-runtime-1.24.0.tgz
Binary file
@@ -0,0 +1 @@
1
+ {"base64Signature":"MEYCIQDiF46jsQyUu5MC5YYxG9m3lolFgWHYy/ZYkf4k3oG0DgIhAJVfUldLgNqb/5Ie8OhzH11EUeulKuNL1ZHufvANg8/R","cert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUc2RENDQm02Z0F3SUJBZ0lVRVhTRjZBSjNXOS9vK2x4V2xzbjVoUTFpcThZd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZeE1URTFNRGsxT0RNeldoY05Nall4TVRFMU1UQXdPRE16V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVuZUJGeFprRHRCWDBFVHNsNXlnMEg1Y1BJMGJRYTA1QW5RSkwKWHdTRTlPMUtyWmpIYjc3UnN1TTdCT3lNWEliTHoxbDcyR2pZdTRsM0I2bm9HNS9YZEtPQ0JZMHdnZ1dKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWZCZ05WSFNNRUdEQVdnQlRmCjArblBWaVFSbHZtbzJPa29WYUxHTGhoa1B6QmlCZ05WSFJFQkFmOEVXREJXaGxSb2RIUndjem92TDJkcGRHaDEKWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHpMM0psYkdWaApjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakkwTGpBd09RWUtLd1lCQkFHRHZ6QUJBUVFyYUhSMGNITTZMeTkwCmIydGxiaTVoWTNScGIyNXpMbWRwZEdoMVluVnpaWEpqYjI1MFpXNTBMbU52YlRBVkJnb3JCZ0VFQVlPL01BRUMKQkFkeVpXeGxZWE5sTURZR0Npc0dBUVFCZzc4d0FRTUVLR1ZqT1RKak1XRm1ZbVpsTldKbE16ZGlZVEEyWW1ReQpOVGhrWWpGbE1qRTNZVFptWmpaaU4yTXdGUVlLS3dZQkJBR0R2ekFCQkFRSGNtVnNaV0Z6WlRBZ0Jnb3JCZ0VFCkFZTy9NQUVGQkJKc2NXTTJNalF2WTJGeUxYSjFiblJwYldVd0hnWUtLd1lCQkFHRHZ6QUJCZ1FRY21WbWN5OTAKWVdkekx6RXVNalF1TURBN0Jnb3JCZ0VFQVlPL01BRUlCQzBNSzJoMGRIQnpPaTh2ZEc5clpXNHVZV04wYVc5dQpjeTVuYVhSb2RXSjFjMlZ5WTI5dWRHVnVkQzVqYjIwd1pBWUtLd1lCQkFHRHZ6QUJDUVJXREZSb2RIUndjem92CkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHoKTDNKbGJHVmhjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakkwTGpBd09BWUtLd1lCQkFHRHZ6QUJDZ1FxRENobApZemt5WXpGaFptSm1aVFZpWlRNM1ltRXdObUprTWpVNFpHSXhaVEl4TjJFMlptWTJZamRqTUIwR0Npc0dBUVFCCmc3OHdBUXNFRHd3TloybDBhSFZpTFdodmMzUmxaREExQmdvckJnRUVBWU8vTUFFTUJDY01KV2gwZEhCek9pOHYKWjJsMGFIVmlMbU52YlM5c2NXTTJNalF2WTJGeUxYSjFiblJwYldVd09BWUtLd1lCQkFHRHZ6QUJEUVFxRENobApZemt5WXpGaFptSm1aVFZpWlRNM1ltRXdObUprTWpVNFpHSXhaVEl4TjJFMlptWTJZamRqTUNBR0Npc0dBUVFCCmc3OHdBUTRFRWd3UWNtVm1jeTkwWVdkekx6RXVNalF1TURBYUJnb3JCZ0VFQVlPL01BRVBCQXdNQ2pFek5qRXgKT1RJM01UUXdLUVlLS3dZQkJBR0R2ekFCRUFRYkRCbG9kSFJ3Y3pvdkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMApNQmdHQ2lzR0FRUUJnNzh3QVJFRUNnd0lOamt5TXpRMU5qUXdaQVlLS3dZQkJBR0R2ekFCRWdSV0RGUm9kSFJ3CmN6b3ZMMmRwZEdoMVlpNWpiMjB2YkhGak5qSTBMMk5oY2kxeWRXNTBhVzFsTHk1bmFYUm9kV0l2ZDI5eWEyWnMKYjNkekwzSmxiR1ZoYzJVdWVXMXNRSEpsWm5NdmRHRm5jeTh4TGpJMExqQXdPQVlLS3dZQkJBR0R2ekFCRXdRcQpEQ2hsWXpreVl6RmhabUptWlRWaVpUTTNZbUV3Tm1Ka01qVTRaR0l4WlRJeE4yRTJabVkyWWpkak1CY0dDaXNHCkFRUUJnNzh3QVJRRUNRd0hjbVZzWldGelpUQlpCZ29yQmdFRUFZTy9NQUVWQkVzTVNXaDBkSEJ6T2k4dloybDAKYUhWaUxtTnZiUzlzY1dNMk1qUXZZMkZ5TFhKMWJuUnBiV1V2WVdOMGFXOXVjeTl5ZFc1ekx6UXpOekF4TnpnNApNRE0yTDJGMGRHVnRjSFJ6THpFd0ZnWUtLd1lCQkFHRHZ6QUJGZ1FJREFad2RXSnNhV013VUFZS0t3WUJCQUdECnZ6QUJHQVJDREVCeVpYQnZPbXh4WXpZeU5FQTJPVEl6TkRVMk5DOWpZWEl0Y25WdWRHbHRaVUF4TXpZeE1Ua3kKTnpFME9uSmxaanB5WldaekwzUmhaM012TVM0eU5DNHdNSUdMQmdvckJnRUVBZFo1QWdRQ0JIMEVld0I1QUhjQQozVDB3YXNiSEVUSmpHUjRjbVdjM0FxSktYcmplUEszL2g0cHlnQzhwN280QUFBR2gzcVR1bEFBQUJBTUFTREJHCkFpRUE2TnI0Z2VIQUh4cVN0K1cwenJKVXdCMk1ReFQ2Yjh4NVI1OVBoc1IvN2FvQ0lRQ1FvT1JWTFgrc0RmMzkKbVRiZklJYkpaQktEMGtPYnZJY0dMTllHWEhFdDl6QUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQUpWSGZsQ2JNZApMWWtDWndDTmMzVTRGcUVwVzFsQ3JmcTdyMDNJUW9aajM3eUJUTDhWVkZYSFlEQlczUmtGNTBZQ01RQ25COTROCk9LVkhXRkJGM3E5TFlSRjhXN1ZnTkdJbE1rOUdXb2tFbm50R3FIWVBGVGF2WVJhWGxQWmNCN2dYbytrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==","rekorBundle":{"SignedEntryTimestamp":"MEUCIQDl3bXOX15YpnZ1dQThXg/rqc4GueDfrRcJ9xfgTb4HTgIgfiTDW3QOsmV4fWzLGKtxSDvM64AG/1HJkNoDDnxIJ58=","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJhYTgwYjY1NzgyYWMyMGEwZDAxMjg5NTQ1OTkyYTlmODljNzQ1ZmQzNDJlZWI1YzJjNGY0MWJkODRlMGZmOTIwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURpRjQ2anNReVV1NU1DNVlZeEc5bTNsb2xGZ1dIWXkvWllrZjRrM29HMERnSWhBSlZmVWxkTGdOcWIvNUllOE9oekgxMUVVZXVsS3VOTDFaSHVmdkFOZzgvUiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVYzJSRU5EUW0wMlowRjNTVUpCWjBsVlJWaFRSalpCU2pOWE9TOXZLMng0VjJ4emJqVm9VVEZwY1RoWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFplRTFVUlRGTlJHc3hUMFJOZWxkb1kwNU5hbGw0VFZSRk1VMVVRWGRQUkUxNlYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ1WlVKR2VGcHJSSFJDV0RCRlZITnNOWGxuTUVnMVkxQkpNR0pSWVRBMVFXNVJTa3dLV0hkVFJUbFBNVXR5V21wSVlqYzNVbk4xVFRkQ1QzbE5XRWxpVEhveGJEY3lSMnBaZFRSc00wSTJibTlITlM5WVpFdFBRMEpaTUhkbloxZEtUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXWkNaMDVXU0ZOTlJVZEVRVmRuUWxSbUNqQXJibEJXYVZGU2JIWnRiekpQYTI5V1lVeEhUR2hvYTFCNlFtbENaMDVXU0ZKRlFrRm1PRVZYUkVKWGFHeFNiMlJJVW5kamVtOTJUREprY0dSSGFERUtXV2sxYW1JeU1IWmlTRVpxVG1wSk1Fd3lUbWhqYVRGNVpGYzFNR0ZYTVd4TWVUVnVZVmhTYjJSWFNYWmtNamw1WVRKYWMySXpaSHBNTTBwc1lrZFdhQXBqTWxWMVpWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9IaE1ha2t3VEdwQmQwOVJXVXRMZDFsQ1FrRkhSSFo2UVVKQlVWRnlZVWhTTUdOSVRUWk1lVGt3Q21JeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUZwWE5UQk1iVTUyWWxSQlZrSm5iM0pDWjBWRlFWbFBMMDFCUlVNS1FrRmtlVnBYZUd4WldFNXNUVVJaUjBOcGMwZEJVVkZDWnpjNGQwRlJUVVZMUjFacVQxUkthazFYUm0xWmJWcHNUbGRLYkUxNlpHbFpWRUV5V1cxUmVRcE9WR2hyV1dwR2JFMXFSVE5aVkZwdFdtcGFhVTR5VFhkR1VWbExTM2RaUWtKQlIwUjJla0ZDUWtGUlNHTnRWbk5hVjBaNldsUkJaMEpuYjNKQ1owVkZDa0ZaVHk5TlFVVkdRa0pLYzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMGhuV1V0TGQxbENRa0ZIUkhaNlFVSkNaMUZSWTIxV2JXTjVPVEFLV1Zka2VreDZSWFZOYWxGMVRVUkJOMEpuYjNKQ1owVkZRVmxQTDAxQlJVbENRekJOU3pKb01HUklRbnBQYVRoMlpFYzVjbHBYTkhWWlYwNHdZVmM1ZFFwamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcVlqSXdkMXBCV1V0TGQxbENRa0ZIUkhaNlFVSkRVVkpYUkVaU2IyUklVbmRqZW05MkNrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUV3eVRtaGphVEY1WkZjMU1HRlhNV3hNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIb0tURE5LYkdKSFZtaGpNbFYxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T0hoTWFra3dUR3BCZDA5QldVdExkMWxDUWtGSFJIWjZRVUpEWjFGeFJFTm9iQXBaZW10NVdYcEdhRnB0U20xYVZGWnBXbFJOTTFsdFJYZE9iVXByVFdwVk5GcEhTWGhhVkVsNFRqSkZNbHB0V1RKWmFtUnFUVUl3UjBOcGMwZEJVVkZDQ21jM09IZEJVWE5GUkhkM1Rsb3liREJoU0ZacFRGZG9kbU16VW14YVJFRXhRbWR2Y2tKblJVVkJXVTh2VFVGRlRVSkRZMDFLVjJnd1pFaENlazlwT0hZS1dqSnNNR0ZJVm1sTWJVNTJZbE01YzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMDlCV1V0TGQxbENRa0ZIUkhaNlFVSkVVVkZ4UkVOb2JBcFplbXQ1V1hwR2FGcHRTbTFhVkZacFdsUk5NMWx0UlhkT2JVcHJUV3BWTkZwSFNYaGFWRWw0VGpKRk1scHRXVEpaYW1ScVRVTkJSME5wYzBkQlVWRkNDbWMzT0hkQlVUUkZSV2QzVVdOdFZtMWplVGt3V1Zka2VreDZSWFZOYWxGMVRVUkJZVUpuYjNKQ1owVkZRVmxQTDAxQlJWQkNRWGROUTJwRmVrNXFSWGdLVDFSSk0wMVVVWGRMVVZsTFMzZFpRa0pCUjBSMmVrRkNSVUZSWWtSQ2JHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUFwTlFtZEhRMmx6UjBGUlVVSm5OemgzUVZKRlJVTm5kMGxPYW10NVRYcFJNVTVxVVhkYVFWbExTM2RaUWtKQlIwUjJla0ZDUldkU1YwUkdVbTlrU0ZKM0NtTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2hHYWs1cVNUQk1NazVvWTJreGVXUlhOVEJoVnpGc1RIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTUtZak5rZWt3elNteGlSMVpvWXpKVmRXVlhNWE5SU0Vwc1dtNU5kbVJIUm01amVUaDRUR3BKTUV4cVFYZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JYZFJjUXBFUTJoc1dYcHJlVmw2Um1oYWJVcHRXbFJXYVZwVVRUTlpiVVYzVG0xS2EwMXFWVFJhUjBsNFdsUkplRTR5UlRKYWJWa3lXV3BrYWsxQ1kwZERhWE5IQ2tGUlVVSm5OemgzUVZKUlJVTlJkMGhqYlZaeldsZEdlbHBVUWxwQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtWelRWTlhhREJrU0VKNlQyazRkbG95YkRBS1lVaFdhVXh0VG5aaVV6bHpZMWROTWsxcVVYWlpNa1o1VEZoS01XSnVVbkJpVjFWMldWZE9NR0ZYT1hWamVUbDVaRmMxZWt4NlVYcE9la0Y0VG5wbk5BcE5SRTB5VERKR01HUkhWblJqU0ZKNlRIcEZkMFpuV1V0TGQxbENRa0ZIUkhaNlFVSkdaMUZKUkVGYWQyUlhTbk5oVjAxM1ZVRlpTMHQzV1VKQ1FVZEVDblo2UVVKSFFWSkRSRVZDZVZwWVFuWlBiWGg0V1hwWmVVNUZRVEpQVkVsNlRrUlZNazVET1dwWldFbDBZMjVXZFdSSGJIUmFWVUY0VFhwWmVFMVVhM2tLVG5wRk1FOXVTbXhhYW5CNVdsZGFla3d6VW1oYU0wMTJUVk0wZVU1RE5IZE5TVWRNUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJTUVWbGQwSTFRVWhqUVFvelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIyZ3pjVlIxYkVGQlFVSkJUVUZUUkVKSENrRnBSVUUyVG5JMFoyVklRVWg0Y1ZOMEsxY3dlbkpLVlhkQ01rMVJlRlEyWWpoNE5WSTFPVkJvYzFJdk4yRnZRMGxSUTFGdlQxSldURmdyYzBSbU16a0tiVlJpWmtsSllrcGFRa3RFTUd0UFluWkpZMGRNVGxsSFdFaEZkRGw2UVV0Q1oyZHhhR3RxVDFCUlVVUkJkMDV2UVVSQ2JFRnFRVXBXU0dac1EySk5aQXBNV1d0RFduZERUbU16VlRSR2NVVndWekZzUTNKbWNUZHlNRE5KVVc5YWFqTTNlVUpVVERoV1ZrWllTRmxFUWxjelVtdEdOVEJaUTAxUlEyNUNPVFJPQ2s5TFZraFhSa0pHTTNFNVRGbFNSamhYTjFablRrZEpiRTFyT1VkWGIydEZibTUwUjNGSVdWQkdWR0YyV1ZKaFdHeFFXbU5DTjJkWWJ5dHJQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=","integratedTime":1794736714,"logIndex":3899685861,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}
@@ -1,3 +1,4 @@
1
+ import { appendSettlement } from './settlement.js';
1
2
  /** 扫描日志中未闭合的 turn(有 turnStart 语义的用户事件但没有 turnEnd)与未配对 toolCall */
2
3
  export function recoverInterrupted(log) {
3
4
  const report = { turnsClosed: 0, pairedCalls: 0 };
@@ -31,7 +32,10 @@ export function recoverInterrupted(log) {
31
32
  report.pairedCalls++;
32
33
  }
33
34
  // 未闭合 turn → 补记 turnEnd=interrupted(不可与 aborted 混用)
35
+ // 1.24-W2(D-W2-2):补记前先落 settlement——部分链对账 = hasUnknownOutcomes 主战场
36
+ // (上方成对补记的 recovered:true 合成结果在结算里按「结果不明」显式记账,不丢信号)
34
37
  for (const turnId of openTurns) {
38
+ appendSettlement(log, turnId, { reason: 'interrupted', recovered: true });
35
39
  log.append('runtime', 'turnEnd', turnId, null, { reason: 'interrupted', recovered: true });
36
40
  report.turnsClosed++;
37
41
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 1.24-W2 · 回合结算回执(settlement receipt)
3
+ *
4
+ * 口径(《Qwen-Code 对照分析》§2.3/§5 / 借鉴四篇裁决记录 Q-W2 / 迭代规划 D-W2-1~6):
5
+ * - 「结算必留回执」:任何 turn 无论成败都有一张结构化凭据——观测 / 恢复 / 审计消费同一张
6
+ * - 落链形态 = kind:'system' + payload.type:'settlement'(EventKind 零新增;payload.type 扩展
7
+ * 沿 routing 'decide' / memory 'write|edit|delete|rollback' 先例);一 turn 一张,llm 面与
8
+ * tool 节合并(D-W2-1)
9
+ * - 产出点仅两处:runTurn closeTurn(turnEnd 之前——turnEnd 保持终止标记语义)与
10
+ * recoverInterrupted 补记(部分链对账);authz 台账 turnEnd 与 facade sessionStop 不经
11
+ * closeTurn、不触发(D-W2-2 登记)
12
+ * - 事实源 = 链(非运行时状态):两本账按链上 toolCall/toolResult payload.id 对账;
13
+ * permissionDenialCount = toolResult error:'authorization-denied' 且 granted===false;
14
+ * hasUnknownOutcomes = 无配对 toolResult 或配对结果 recovered:true(副作用状态未知显式记账);
15
+ * bounded = aborted-before-dispatch 补记形态在链(取消落在派发边界,未打断在途执行)
16
+ * - 字段 CAR 化:outcome 由 reason 映射(complete|interrupted|failed);failureKind = 1.23
17
+ * meta.kind 诚实缺键;assistantSeq 代 Qwen assistantMessageUuid;Qwen nextMaxOutputTokens
18
+ * 在 CAR 无对应机制 → 诚实缺键不预埋(D-W2-3 偏差登记)
19
+ * - turnEnd meta 零变化:settlement 是独立事件非 meta 尾键(D-W2-4,s78-16 字节兼容锚不动);
20
+ * stopRequested 经 closeTurn hints 传递不落 meta
21
+ * - kill-switch:CAR_SETTLEMENT=off 跳过 append(K1 env 逐 turn 求值先例);缺省开(D-W2-5)
22
+ * - 链量纪律(R-W2):定长小 payload——账本数组 cap 64 + truncated 标记;内容类信息不进链
23
+ */
24
+ import type { SessionLog } from '../session/log.ts';
25
+ export type SettlementOutcome = 'complete' | 'interrupted' | 'failed';
26
+ /** 账本数组上限(超出截断 + ledgerTruncated 标记——防病态长 turn 链量失控) */
27
+ export declare const SETTLEMENT_LEDGER_CAP = 64;
28
+ export interface SettlementHints {
29
+ /** terminate 型收口(工具主动 stop 语义,stop.ts closeTurn hints——不落 turnEnd meta) */
30
+ stopRequested?: boolean;
31
+ }
32
+ /** TurnEndReason → settlement outcome 映射(completed/max-tokens=complete;aborted/interrupted=interrupted;error/blocked=failed) */
33
+ export declare function outcomeFromReason(reason: string): SettlementOutcome;
34
+ /**
35
+ * 结算回执落链(closeTurn / recover 共用单点)。meta = 同 turn turnEnd 将携带的 meta
36
+ * (reason/kind 等事实同源);usage/hints 缺省时从链自证(requestSent 由 usage 事件或
37
+ * assistant 在场推导)。
38
+ */
39
+ export declare function appendSettlement(log: SessionLog, turnId: string, meta: Record<string, unknown>, hints?: SettlementHints): void;
@@ -0,0 +1,76 @@
1
+ /** 账本数组上限(超出截断 + ledgerTruncated 标记——防病态长 turn 链量失控) */
2
+ export const SETTLEMENT_LEDGER_CAP = 64;
3
+ /** TurnEndReason → settlement outcome 映射(completed/max-tokens=complete;aborted/interrupted=interrupted;error/blocked=failed) */
4
+ export function outcomeFromReason(reason) {
5
+ if (reason === 'completed' || reason === 'max-tokens')
6
+ return 'complete';
7
+ if (reason === 'aborted' || reason === 'interrupted')
8
+ return 'interrupted';
9
+ return 'failed';
10
+ }
11
+ /**
12
+ * 结算回执落链(closeTurn / recover 共用单点)。meta = 同 turn turnEnd 将携带的 meta
13
+ * (reason/kind 等事实同源);usage/hints 缺省时从链自证(requestSent 由 usage 事件或
14
+ * assistant 在场推导)。
15
+ */
16
+ export function appendSettlement(log, turnId, meta, hints) {
17
+ if (process.env.CAR_SETTLEMENT === 'off')
18
+ return;
19
+ const reason = typeof meta.reason === 'string' ? meta.reason : 'error';
20
+ const kind = typeof meta.kind === 'string' ? meta.kind : undefined;
21
+ // 单次链扫描:两本账 + 拒绝计数 + unknown/bounded 事实 + requestSent 事实 + assistantSeq
22
+ const declared = [];
23
+ const resultById = new Map();
24
+ let denialCount = 0;
25
+ let bounded = false;
26
+ let usageRequests = 0;
27
+ let assistantSeq;
28
+ for (const e of log.events) {
29
+ if (e.turnId !== turnId)
30
+ continue;
31
+ const p = (e.payload ?? {});
32
+ if (e.kind === 'toolCall') {
33
+ if (typeof p.id === 'string')
34
+ declared.push(p.id);
35
+ }
36
+ else if (e.kind === 'toolResult') {
37
+ if (typeof p.id === 'string') {
38
+ resultById.set(p.id, p); // 1.6-S1 last-wins 同款口径
39
+ if (p.error === 'authorization-denied' && p.granted === false)
40
+ denialCount++;
41
+ if (p.error === 'aborted-before-dispatch')
42
+ bounded = true;
43
+ }
44
+ }
45
+ else if (e.kind === 'usage') {
46
+ usageRequests++;
47
+ }
48
+ else if (e.kind === 'assistant') {
49
+ assistantSeq = e.seq;
50
+ }
51
+ }
52
+ const dispatched = [...resultById.keys()];
53
+ const unknownIds = declared.filter((id) => {
54
+ const res = resultById.get(id);
55
+ return !res || res.recovered === true;
56
+ });
57
+ const cap = (arr) => (arr.length > SETTLEMENT_LEDGER_CAP ? arr.slice(0, SETTLEMENT_LEDGER_CAP) : arr);
58
+ const ledgerTruncated = declared.length > SETTLEMENT_LEDGER_CAP || dispatched.length > SETTLEMENT_LEDGER_CAP;
59
+ log.append('runtime', 'system', turnId, {
60
+ type: 'settlement',
61
+ outcome: outcomeFromReason(reason),
62
+ ...(kind ? { failureKind: kind } : {}),
63
+ requestSent: usageRequests > 0 || assistantSeq !== undefined,
64
+ ...(assistantSeq !== undefined ? { assistantSeq } : {}),
65
+ declaredToolUseIds: cap(declared),
66
+ dispatchedToolUseIds: cap(dispatched),
67
+ ...(ledgerTruncated ? { ledgerTruncated: true } : {}),
68
+ tool: {
69
+ permissionDenialCount: denialCount,
70
+ hasUnknownOutcomes: unknownIds.length > 0,
71
+ ...(unknownIds.length ? { unknownToolUseIds: cap(unknownIds) } : {}),
72
+ stopRequested: hints?.stopRequested === true,
73
+ bounded,
74
+ },
75
+ });
76
+ }
@@ -84,8 +84,15 @@ export interface TurnResult {
84
84
  reason: TurnEndReason;
85
85
  }>;
86
86
  }
87
+ /**
88
+ * 1.23-W1(D-W1-6):取消时机标注(W-0 审计 F-W0-3——aborted 三路径链上不区分)——
89
+ * 'before-request' = 请求未发出(runTurn 循环头 / chatStep 零 chunk 流结束检查);
90
+ * 'in-flight' = 在途打断(chatStep chunk 间隙 / 有 chunk 后的流结束检查)。
91
+ * 缺省缺席 = 1.22 形态(terminate 型 abort 为工具主动收口语义,非取消,不带键)。
92
+ */
87
93
  export declare class TurnAborted extends Error {
88
- constructor();
94
+ readonly abortedAt?: 'before-request' | 'in-flight' | undefined;
95
+ constructor(abortedAt?: 'before-request' | 'in-flight' | undefined);
89
96
  }
90
97
  export declare function runTurn(opts: {
91
98
  log: SessionLog;
package/dist/loop/stop.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import { CarM8Error } from '../runtime-core/errors.js';
2
+ import { appendSettlement } from './settlement.js';
3
+ /**
4
+ * 1.23-W1(D-W1-6):取消时机标注(W-0 审计 F-W0-3——aborted 三路径链上不区分)——
5
+ * 'before-request' = 请求未发出(runTurn 循环头 / chatStep 零 chunk 流结束检查);
6
+ * 'in-flight' = 在途打断(chatStep chunk 间隙 / 有 chunk 后的流结束检查)。
7
+ * 缺省缺席 = 1.22 形态(terminate 型 abort 为工具主动收口语义,非取消,不带键)。
8
+ */
2
9
  export class TurnAborted extends Error {
3
- constructor() { super('CAR-ABORTED: turn cancelled by user'); }
10
+ abortedAt;
11
+ constructor(abortedAt) {
12
+ super('CAR-ABORTED: turn cancelled by user');
13
+ this.abortedAt = abortedAt;
14
+ }
4
15
  }
5
16
  /**
6
17
  * 1.11-S2(D-33 / 计量设计 §3.3):turn 级 usage 聚合——扫描本 turn 的 'usage' 事件(事实本体
@@ -45,13 +56,16 @@ export async function runTurn(opts) {
45
56
  let steps = 0;
46
57
  // 1.11-S2(D-33):turnEnd 落链单点包装——聚合本 turn usage 事件(无 usage 不带键,
47
58
  // 缺省=未采集向后兼容);reason 保持 meta 首键,usage 恒为加法尾键
48
- const closeTurn = (meta) => {
59
+ // 1.24-W2(D-W2-2):turnEnd 之前先落 settlement 结算回执(turnEnd 保持终止标记语义;
60
+ // hints 仅 terminate 收口传递 stopRequested——不落 turnEnd meta,D-W2-4)
61
+ const closeTurn = (meta, hints) => {
49
62
  const usage = turnUsageFrom(log, turnId);
50
63
  // 1.18.2-K8(R-K7-2):计量缺失 fail-visible——商汤网关实测 20% 概率 message_delta
51
64
  // 不带 usage(流正常回复但计量整条缺席)→ 缺失口径纪律不落半条事实(turnUsageFrom
52
65
  // undefined),缺失本身在此显式标注:本 turn 存在 assistant 事件(模型有可见输出 ⇒
53
66
  // 必有请求)而无 usage 聚合时带 usageMissing: true(加法尾键,与 usage 同语义位)
54
67
  const hasAssistant = log.events.some((e) => e.kind === 'assistant' && e.turnId === turnId);
68
+ appendSettlement(log, turnId, meta, hints);
55
69
  return log.append('runtime', 'turnEnd', turnId, null, {
56
70
  ...meta,
57
71
  ...(usage ? { usage } : hasAssistant ? { usageMissing: true } : {}),
@@ -166,7 +180,8 @@ export async function runTurn(opts) {
166
180
  try {
167
181
  while (steps < maxSteps) {
168
182
  if (signal?.aborted) {
169
- const seq = closeTurn({ reason: 'aborted' }).seq;
183
+ // 1.23-W1(D-W1-6):循环头取消 = 请求未发出(无在途请求)
184
+ const seq = closeTurn({ reason: 'aborted', abortedAt: 'before-request' }).seq;
170
185
  trail.push({ seq, reason: 'aborted' });
171
186
  return { reason: 'aborted', steps, endReasonTrail: trail };
172
187
  }
@@ -226,7 +241,8 @@ export async function runTurn(opts) {
226
241
  const agg = preset.aggregate ?? {};
227
242
  const abortedNow = (agg.terminate ?? 'all') === 'any' ? batch.terminateVotes > 0 : batch.terminateAll;
228
243
  if (abortedNow) { // terminate 收口:默认 AND(整批),可配置 any
229
- const seq = closeTurn({ reason: 'aborted' }).seq;
244
+ // 1.24-W2:工具主动 stop 语义 → settlement stopRequested=true(hints 不落 meta,D-W2-4)
245
+ const seq = closeTurn({ reason: 'aborted' }, { stopRequested: true }).seq;
230
246
  trail.push({ seq, reason: 'aborted' });
231
247
  return { reason: 'aborted', steps, endReasonTrail: trail };
232
248
  }
@@ -252,17 +268,19 @@ export async function runTurn(opts) {
252
268
  }
253
269
  catch (e) {
254
270
  if (e instanceof TurnAborted) {
255
- const seq = closeTurn({ reason: 'aborted' }).seq;
271
+ // 1.23-W1(D-W1-6):chatStep 甄别过的取消时机随异常携带(terminate 型 abort 不经此路径)
272
+ const seq = closeTurn({ reason: 'aborted', ...(e.abortedAt ? { abortedAt: e.abortedAt } : {}) }).seq;
256
273
  trail.push({ seq, reason: 'aborted' });
257
274
  return { reason: 'aborted', steps, endReasonTrail: trail };
258
275
  }
259
276
  // 1.4-S4:CarM8Error 保形——code/userHint/retryable 进 turnEnd(A080001 引导文案「请运行
260
- // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
277
+ // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变);
278
+ // 1.23-W1(D-W1-3):+kind 失败细类(加法尾键——B080001 多形态分型,审计 F-W0-2)
261
279
  const m8 = e instanceof CarM8Error;
262
280
  const detail = m8 ? `${e.slug}: ${e.message}` : String(e);
263
281
  const seq = closeTurn({
264
282
  reason: 'error', detail,
265
- ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
283
+ ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable, ...(e.kind ? { kind: e.kind } : {}) } : {}),
266
284
  }).seq;
267
285
  trail.push({ seq, reason: 'error' });
268
286
  return { reason: 'error', steps, endReasonTrail: trail };
@@ -1,4 +1,4 @@
1
- {"stage":"archive","detail":"git archive HEAD → car-runtime-1.22.1.tgz(444943 bytes,树哈希锚定)","ts":1794650951102}
2
- {"stage":"sha256sums","detail":"SHA256SUMS d2873814312c5dd6…(裸字节复核通过)","ts":1794650951103}
3
- {"stage":"sbom","detail":"55 个组件(运行时依赖 0 项)","ts":1794650951107}
4
- {"stage":"cosign-keyless","detail":"signed car-runtime-1.22.1.tgz (keyless OIDC + Rekor)","ts":1794650954633}
1
+ {"stage":"archive","detail":"git archive HEAD → car-runtime-1.24.0.tgz(455869 bytes,树哈希锚定)","ts":1794736712665}
2
+ {"stage":"sha256sums","detail":"SHA256SUMS aa80b65782ac20a0…(裸字节复核通过)","ts":1794736712666}
3
+ {"stage":"sbom","detail":"56 个组件(运行时依赖 0 项)","ts":1794736712672}
4
+ {"stage":"cosign-keyless","detail":"signed car-runtime-1.24.0.tgz (keyless OIDC + Rekor)","ts":1794736714665}
@@ -1,7 +1,7 @@
1
1
  import { TurnAborted } from '../loop/stop.js';
2
2
  import { StreamRedactor } from './redaction.js';
3
3
  import { scanSecrets } from '../security/secrets.js';
4
- import { providerUnreachable } from './errors.js';
4
+ import { CarM8Error, providerUnreachable } from './errors.js';
5
5
  export async function chatStep(opts) {
6
6
  const { core, log, turnId, model, tools, maxTokens, adapterId } = opts;
7
7
  // 1. 请求前断言(SQ-07 #2):N1 失守 = 带病请求,显式拒绝
@@ -49,6 +49,12 @@ export async function chatStep(opts) {
49
49
  const thinkingRedactor = new StreamRedactor();
50
50
  let stopReason;
51
51
  let errorDetail;
52
+ // 1.23-W1(D-W1-2):error chunk 细类捕获(最后一条 error chunk 的 kind——providerUnreachable
53
+ // 携带 → runTurn turnEnd meta.kind;缺失 = 1.22 形态)
54
+ let errorKind;
55
+ // 1.23-W1(D-W1-6):零 chunk 甄别——流结束检查抛 TurnAborted 时区分「请求未发出(attempt 前
56
+ // 静默收口)」与「在途打断」;chunk 间隙路径恒 in-flight
57
+ let sawChunk = false;
52
58
  let usage;
53
59
  const stream = core.chat({
54
60
  model,
@@ -62,9 +68,11 @@ export async function chatStep(opts) {
62
68
  ...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
63
69
  });
64
70
  for await (const chunk of stream) {
65
- // 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息
71
+ sawChunk = true;
72
+ // 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息;
73
+ // 1.23-W1(D-W1-6):在途打断标注 in-flight
66
74
  if (opts.signal?.aborted)
67
- throw new TurnAborted();
75
+ throw new TurnAborted('in-flight');
68
76
  if (chunk.delta !== undefined) {
69
77
  const masked = redactor.push(chunk.delta);
70
78
  text += masked;
@@ -109,17 +117,21 @@ export async function chatStep(opts) {
109
117
  opts.onDelta({ thinkingDelta: masked });
110
118
  }
111
119
  }
112
- if (chunk.error)
120
+ if (chunk.error) {
113
121
  errorDetail = chunk.error.message;
122
+ errorKind = chunk.error.kind; // 1.23-W1(D-W1-2):细类透传(最后一条 error chunk 为准)
123
+ }
114
124
  if (chunk.usage)
115
125
  usage = chunk.usage; // D-33:附着于最后一个 chunk——后到覆盖
116
126
  if (chunk.finishReason !== undefined)
117
127
  stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason;
118
128
  }
119
129
  // 1.4-S4(D-19):finish 检查前先查取消(适配器取消路径静默收口无 finishReason——
120
- // 必须先于此处的契约兜底,否则取消被误判为适配器违规)
130
+ // 必须先于此处的契约兜底,否则取消被误判为适配器违规);
131
+ // 1.23-W1(D-W1-6):abortedAt 甄别——有 chunk = 在途打断(in-flight);
132
+ // 零 chunk = attempt 前静默收口,请求未发出(before-request)
121
133
  if (opts.signal?.aborted)
122
- throw new TurnAborted();
134
+ throw new TurnAborted(sawChunk ? 'in-flight' : 'before-request');
123
135
  if (stopReason === undefined) {
124
136
  // 适配器层已保证首 finishReason(AL-05);防御性兜底显式化
125
137
  throw new Error('CAR-E-LLM-FINISH: 流结束无 finishReason(适配器契约违规)');
@@ -201,16 +213,19 @@ export async function chatStep(opts) {
201
213
  args = c.argsBuf ? JSON.parse(c.argsBuf) : {};
202
214
  }
203
215
  catch {
204
- // toolUse 声明下 args 非法 JSON = 协议违规显式化(非 length 场景无 truncatedTools 口径)
205
- throw new Error(`CAR-E-LLM-PROTO: 工具调用 ${c.name} 参数非法 JSON(finishReason=${stopReason},${errorDetail ?? '无附加错误'})`);
216
+ // toolUse 声明下 args 非法 JSON = 协议违规显式化(非 length 场景无 truncatedTools 口径);
217
+ // 1.23-W1(D-W1-3):转 CarM8Error——turnEnd meta 获得 code/userHint/retryable + kind
218
+ // 'proto-invalid-args'(detail 形态变更登记:CarM8Error 统一格式带(B080001)码后缀)
219
+ throw new CarM8Error('B080001', 'CAR-E-LLM-PROTO', `工具调用 ${c.name} 参数非法 JSON(finishReason=${stopReason},${errorDetail ?? '无附加错误'})`, undefined, false, 'proto-invalid-args');
206
220
  }
207
221
  toolCalls.push({ id: c.id ?? `tc-${idx}-${c.name}`, tool: c.name, args });
208
222
  }
209
223
  return { stopReason: 'toolUse', ...(text ? { text } : {}), ...(toolCalls.length ? { toolCalls } : {}), secretsRedacted };
210
224
  }
211
- // error(含 aborted 映射):以 B080001 抛出 → runTurn catch → turnEnd error 携带 detail(BD-04 收口)
225
+ // error(含 aborted 映射):以 B080001 抛出 → runTurn catch → turnEnd error 携带 detail(BD-04 收口);
226
+ // 1.23-W1(D-W1-2):细类 kind 透传(error chunk 捕获的 errorKind;缺失 = 1.22 形态)
212
227
  if (stopReason === 'error') {
213
- throw providerUnreachable(errorDetail ?? 'provider 返回 error 终止(无附加说明)');
228
+ throw providerUnreachable(errorDetail ?? 'provider 返回 error 终止(无附加说明)', errorKind);
214
229
  }
215
230
  return { stopReason: 'stop', ...(text ? { text } : {}), secretsRedacted };
216
231
  }
@@ -16,6 +16,12 @@ export declare class CarM8Error extends Error {
16
16
  readonly userHint?: string | undefined;
17
17
  /** 重试建议(A080001 不重试 / B080001 退避后重试) */
18
18
  readonly retryable: boolean;
19
+ /**
20
+ * 1.23-W1(D-W1-3):失败细类(LlmErrorKind 注册表)——B080001 一码多形态的分型标注,
21
+ * 经 runTurn closeTurn 落 turnEnd meta.kind(加法尾键,消费面免字符串反解)。
22
+ * 缺省缺席 = 1.22 形态(A080001 凭据缺失以独立码区分,无需 kind)。
23
+ */
24
+ readonly kind?: string | undefined;
19
25
  constructor(
20
26
  /** 六位错误码(§3.5.1 注册表) */
21
27
  code: M8ErrorCode,
@@ -24,7 +30,13 @@ export declare class CarM8Error extends Error {
24
30
  /** 用户文案(§3.5.1 注册表原文) */
25
31
  userHint?: string | undefined,
26
32
  /** 重试建议(A080001 不重试 / B080001 退避后重试) */
27
- retryable?: boolean);
33
+ retryable?: boolean,
34
+ /**
35
+ * 1.23-W1(D-W1-3):失败细类(LlmErrorKind 注册表)——B080001 一码多形态的分型标注,
36
+ * 经 runTurn closeTurn 落 turnEnd meta.kind(加法尾键,消费面免字符串反解)。
37
+ * 缺省缺席 = 1.22 形态(A080001 凭据缺失以独立码区分,无需 kind)。
38
+ */
39
+ kind?: string | undefined);
28
40
  }
29
41
  export declare function credentialMissing(provider: string, searched: string[]): CarM8Error;
30
- export declare function providerUnreachable(detail: string): CarM8Error;
42
+ export declare function providerUnreachable(detail: string, kind?: string): CarM8Error;
@@ -11,6 +11,7 @@ export class CarM8Error extends Error {
11
11
  slug;
12
12
  userHint;
13
13
  retryable;
14
+ kind;
14
15
  constructor(
15
16
  /** 六位错误码(§3.5.1 注册表) */
16
17
  code,
@@ -19,18 +20,25 @@ export class CarM8Error extends Error {
19
20
  /** 用户文案(§3.5.1 注册表原文) */
20
21
  userHint,
21
22
  /** 重试建议(A080001 不重试 / B080001 退避后重试) */
22
- retryable = false) {
23
+ retryable = false,
24
+ /**
25
+ * 1.23-W1(D-W1-3):失败细类(LlmErrorKind 注册表)——B080001 一码多形态的分型标注,
26
+ * 经 runTurn closeTurn 落 turnEnd meta.kind(加法尾键,消费面免字符串反解)。
27
+ * 缺省缺席 = 1.22 形态(A080001 凭据缺失以独立码区分,无需 kind)。
28
+ */
29
+ kind) {
23
30
  super(`${slug}: ${message}(${code})`);
24
31
  this.code = code;
25
32
  this.slug = slug;
26
33
  this.userHint = userHint;
27
34
  this.retryable = retryable;
35
+ this.kind = kind;
28
36
  this.name = 'CarM8Error';
29
37
  }
30
38
  }
31
39
  export function credentialMissing(provider, searched) {
32
40
  return new CarM8Error('A080001', 'CAR-E-CRED-MISSING', `未找到模型服务凭据(provider=${provider},已检索:${searched.join('、') || '无可用通道'})`, '未找到模型服务凭据,请运行 car doctor', false);
33
41
  }
34
- export function providerUnreachable(detail) {
35
- return new CarM8Error('B080001', 'CAR-E-LLM-UNREACHABLE', `LLM Provider 不可达/重试耗尽:${detail}`, '模型服务暂时不可用,本轮已停止', true);
42
+ export function providerUnreachable(detail, kind) {
43
+ return new CarM8Error('B080001', 'CAR-E-LLM-UNREACHABLE', `LLM Provider 不可达/重试耗尽:${detail}`, '模型服务暂时不可用,本轮已停止', true, kind);
36
44
  }
@@ -228,7 +228,9 @@ async function* streamSsePost(spec, parse) {
228
228
  if (!res.ok) {
229
229
  const text = await res.text().catch(() => '');
230
230
  // 4xx 业务错不重试(§3.5.4);错误信息不含凭据(凭据值不进错误路径)
231
- yield { finishReason: 'error', error: { code: 'B080001', message: `provider HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}` } };
231
+ // 1.23-W1(D-W1-5):4xx 细类标注——overflow 特征命中 = context-limit-suspect(疑似),否则 transport-4xx
232
+ const kind = contextLimitSuspect(text) ? 'context-limit-suspect' : 'transport-4xx';
233
+ yield { finishReason: 'error', error: { code: 'B080001', message: `provider HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}`, kind } };
232
234
  return;
233
235
  }
234
236
  if (!res.body)
@@ -252,7 +254,8 @@ async function* streamSsePost(spec, parse) {
252
254
  // 流自然结束但未给 finishReason = 异常终止(AL-05 口径:缺失显式化,禁吞没);
253
255
  // 已携带 finishReason 的流原样收口——兜底不得追加(M8-BUG-1 口径延续)
254
256
  if (!sawFinish) {
255
- yield { finishReason: 'error', error: { code: 'B080001', message: '流结束未携带 finish_reason(AL-05)' } };
257
+ // 1.23-W1:细类 no-finish-reason(审计 F-W0-2 拆型)
258
+ yield { finishReason: 'error', error: { code: 'B080001', message: '流结束未携带 finish_reason(AL-05)', kind: 'no-finish-reason' } };
256
259
  }
257
260
  return;
258
261
  }
@@ -268,11 +271,11 @@ async function* streamSsePost(spec, parse) {
268
271
  continue;
269
272
  }
270
273
  if (beforeFirstByte) {
271
- // 重试耗尽 → B080001(BD-04:消费方 runTurn 以 error 收口)
272
- throw providerUnreachable(`${e.name}: ${e.message}(尝试 ${attempt + 1}/${spec.retries + 1},${Date.now() - started}ms)`);
274
+ // 重试耗尽 → B080001(BD-04:消费方 runTurn 以 error 收口);1.23-W1 细类 transport-exhausted
275
+ throw providerUnreachable(`${e.name}: ${e.message}(尝试 ${attempt + 1}/${spec.retries + 1},${Date.now() - started}ms)`, 'transport-exhausted');
273
276
  }
274
- // 流中途失败:已消费 chunk 不可重放 → error chunk 收口
275
- yield { finishReason: 'error', error: { code: 'B080001', message: `流中途失败:${e.message}` } };
277
+ // 流中途失败:已消费 chunk 不可重放 → error chunk 收口;1.23-W1 细类 stream-midbreak
278
+ yield { finishReason: 'error', error: { code: 'B080001', message: `流中途失败:${e.message}`, kind: 'stream-midbreak' } };
276
279
  return;
277
280
  }
278
281
  finally {
@@ -290,6 +293,22 @@ const FINISH_MAP = {
290
293
  };
291
294
  class Retryable extends Error {
292
295
  }
296
+ /**
297
+ * 1.23-W1(D-W1-5):context-limit-suspect 确定性关键词表——仅用于 4xx 错误体、只收高置信特征;
298
+ * 标注语义 = **疑似**(消费面文案须带「疑似」,禁估算禁 LLM 判断);未命中 = transport-4xx。
299
+ */
300
+ const CONTEXT_LIMIT_PATTERNS = [
301
+ /context length/i,
302
+ /maximum context/i,
303
+ /context[_ ]window/i,
304
+ /prompt is too long/i,
305
+ /too many tokens/i,
306
+ /input tokens? exceed/i,
307
+ /exceeds? the (maximum )?(context|token)/i,
308
+ /上下文(长度|超限)/,
309
+ /超出上下文/,
310
+ ];
311
+ const contextLimitSuspect = (text) => CONTEXT_LIMIT_PATTERNS.some((p) => p.test(text));
293
312
  /**
294
313
  * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
295
314
  * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
@@ -382,13 +401,20 @@ async function* parseSseStream(body, model, adapterId) {
382
401
  yield { usage };
383
402
  continue;
384
403
  }
404
+ // 1.23-W1(D-W1-4 / D-W1-1):finish 终态附带 error 细类——未映射 = unmapped-finish;
405
+ // content_filter(openai 族唯一映射 'aborted' 的信号)= refusal(与 anthropic stop_reason=refusal 对齐)
406
+ const finishError = finish === 'error'
407
+ ? { code: 'B080001', message: `未映射的 finish_reason "${raw}"`, kind: 'unmapped-finish' }
408
+ : finish === 'aborted'
409
+ ? { code: 'B080001', message: `provider content_filter (finish_reason="${raw}")`, kind: 'refusal' }
410
+ : undefined;
385
411
  const chunks = [];
386
412
  if (toolCallDeltas.length === 0) {
387
413
  chunks.push({
388
414
  ...(reasoningContent !== undefined ? { thinkingDelta: reasoningContent } : {}),
389
415
  ...(text !== undefined ? { delta: text } : {}),
390
416
  ...(finish !== undefined
391
- ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
417
+ ? { finishReason: finish, ...(finishError ? { error: finishError } : {}) }
392
418
  : {}),
393
419
  });
394
420
  }
@@ -400,7 +426,7 @@ async function* parseSseStream(body, model, adapterId) {
400
426
  ...(i === 0 && text !== undefined ? { delta: text } : {}),
401
427
  toolCallDelta: toolCallDeltas[i],
402
428
  ...(last && finish !== undefined
403
- ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
429
+ ? { finishReason: finish, ...(finishError ? { error: finishError } : {}) }
404
430
  : {}),
405
431
  });
406
432
  }
@@ -800,9 +826,13 @@ async function* parseAnthropicSseStream(body, model, adapterId) {
800
826
  const usage = mergeAnthropicUsage(startUsage, (evt.usage ?? null), model, adapterId);
801
827
  if (raw != null) {
802
828
  const finish = ANTHROPIC_STOP_REASON_MAP[String(raw)];
829
+ // 1.23-W1(D-W1-4 / D-W1-1):stop_reason 细类标注——refusal 附加 error chunk(kind 'refusal',
830
+ // 与 openai content_filter 对齐;finishReason 映射不动);未映射 = unmapped-finish
803
831
  const chunk = finish
804
- ? { finishReason: finish }
805
- : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"` } };
832
+ ? finish === 'aborted'
833
+ ? { finishReason: finish, error: { code: 'B080001', message: `provider refusal (stop_reason="${raw}")`, kind: 'refusal' } }
834
+ : { finishReason: finish }
835
+ : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"`, kind: 'unmapped-finish' } };
806
836
  yield usage ? { ...chunk, usage } : chunk;
807
837
  }
808
838
  else if (usage) {
@@ -811,8 +841,9 @@ async function* parseAnthropicSseStream(body, model, adapterId) {
811
841
  }
812
842
  }
813
843
  else if (type === 'error') {
814
- // 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突)
815
- yield { finishReason: 'error', error: { code: 'B080001', message: `provider 流错误:${evt.error?.message ?? JSON.stringify(evt).slice(0, 200)}` } };
844
+ // 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突);
845
+ // 1.23-W1 细类 provider-stream-error
846
+ yield { finishReason: 'error', error: { code: 'B080001', message: `provider 流错误:${evt.error?.message ?? JSON.stringify(evt).slice(0, 200)}`, kind: 'provider-stream-error' } };
816
847
  }
817
848
  }
818
849
  }
@@ -47,6 +47,13 @@ export interface ToolCallDelta {
47
47
  /** JSON 增量片段,聚合后整体 JSON.parse(半截 JSON 不解析——ADR-001 同纪律) */
48
48
  argumentsDelta?: string;
49
49
  }
50
+ /**
51
+ * 1.23-W1(D-W1-1 / 《W-0 失败分类学对照审计》§5):LLM 失败细类注册表——把 B080001 下压着的
52
+ * 多形态失败拆到可对应处置动作的粒度(审计 F-W0-2)。**加法演进**:可选键,缺失 = 1.22 形态
53
+ * 逐字节兼容。`transport-network` 从审计 8 类剔除(无独立产出点——网络失败只以 exhausted /
54
+ * midbreak 到达消费面);`unmapped-finish` / `provider-stream-error` 为实施中浮出的两形态增补。
55
+ */
56
+ export type LlmErrorKind = 'transport-4xx' | 'transport-exhausted' | 'stream-midbreak' | 'no-finish-reason' | 'refusal' | 'proto-invalid-args' | 'context-limit-suspect' | 'unmapped-finish' | 'provider-stream-error';
50
57
  /**
51
58
  * 1.11-S1(D-33 / 计量设计 §3.1):token 四分量归一化用量——跨 provider 可加总的库内口径
52
59
  * (§1.1:openai prompt_tokens 含 cached 必须减法归一;anthropic input_tokens 天然不含 cache)。
@@ -79,10 +86,12 @@ export interface LlmChunk {
79
86
  delta?: string;
80
87
  toolCallDelta?: ToolCallDelta;
81
88
  finishReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted';
82
- /** finishReason='error' 时的结构化原因(消费方落 turnEnd meta,不进模型消息流) */
89
+ /** finishReason='error' 时的结构化原因(消费方落 turnEnd meta,不进模型消息流);
90
+ * 1.23-W1(D-W1-2):+kind 失败细类(LlmErrorKind 注册表——B080001 一码多形态的分型标注) */
83
91
  error?: {
84
92
  code: string;
85
93
  message: string;
94
+ kind?: LlmErrorKind;
86
95
  };
87
96
  /** 1.11-S1(D-33):每次请求最后一个 chunk 附带归一化用量;缺失 = provider 未回,禁补造 */
88
97
  usage?: LlmUsage;
package/dist/sbom.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bomFormat": "CAR-SBOM",
3
3
  "specVersion": "0.1",
4
- "version": "1.22.1",
4
+ "version": "1.24.0",
5
5
  "components": [
6
6
  {
7
7
  "type": "file",
@@ -101,12 +101,17 @@
101
101
  {
102
102
  "type": "file",
103
103
  "path": "src/loop/recover.ts",
104
- "sha256": "095d68827ae129f80f84cf485d7d2c41549dec99574311b8fe0f65fae9498ec5"
104
+ "sha256": "28f3ee3c244380b91ea5f037bb7920c55c2979ea5e818f77827ab2c473e1578a"
105
+ },
106
+ {
107
+ "type": "file",
108
+ "path": "src/loop/settlement.ts",
109
+ "sha256": "07b9da93f216f5bee4dc7e034e7e194e9a209449599e71371255c7cd388f4c3a"
105
110
  },
106
111
  {
107
112
  "type": "file",
108
113
  "path": "src/loop/stop.ts",
109
- "sha256": "4b144151b40c07445592795006fd9b3388cb2e9bc398dd41c5d3df883853e098"
114
+ "sha256": "7e185d31c4c65ee4ba1d43135a609572e677a70f93110c183e34d593d8aaf0a6"
110
115
  },
111
116
  {
112
117
  "type": "file",
@@ -161,7 +166,7 @@
161
166
  {
162
167
  "type": "file",
163
168
  "path": "src/runtime-core/chatStep.ts",
164
- "sha256": "8847c49264abcb451bb9587c1c15abb06c940038be8f95ddcf8a8ba776b58d03"
169
+ "sha256": "c95f71f3fbe5a99b2fe02d3a71450efb2bb31b003a30da6eafc39eb57c791f33"
165
170
  },
166
171
  {
167
172
  "type": "file",
@@ -171,12 +176,12 @@
171
176
  {
172
177
  "type": "file",
173
178
  "path": "src/runtime-core/errors.ts",
174
- "sha256": "e94d3ce47aba5ed85fffcc468bc785e8d083b689385c91321f397a5cbb3a445b"
179
+ "sha256": "b7611e5a1647bb0022a76b9f8211a59f8097c0d70c4747d56600e122ca5198a8"
175
180
  },
176
181
  {
177
182
  "type": "file",
178
183
  "path": "src/runtime-core/llm.ts",
179
- "sha256": "8e8e255b0de192c6118d5972a62e30a4812ce72f8d3acd5641f8241b63c842f6"
184
+ "sha256": "fcab344c1dae5a4fac24dcb8e7be3a82cf0442411b4b9943e558a07f35e86a30"
180
185
  },
181
186
  {
182
187
  "type": "file",
@@ -201,7 +206,7 @@
201
206
  {
202
207
  "type": "file",
203
208
  "path": "src/runtime-core/types.ts",
204
- "sha256": "fbe2d03167420169ebe601bd425e641f3df5ad518d109995ff5302b839a82480"
209
+ "sha256": "c11f7fbb38ff1f5567e7a18c87300f3bc3bf802b7d3660ebb30a8191017ad37a"
205
210
  },
206
211
  {
207
212
  "type": "file",
@@ -251,7 +256,7 @@
251
256
  {
252
257
  "type": "file",
253
258
  "path": "src/session/log.ts",
254
- "sha256": "e87fdaaeecb3ad71924664bd565369b8bfb7347500cd2d97c51c22c5c68ab8be"
259
+ "sha256": "7cecb0ef745d27bd5c1a8935b19f2f2b54546cab0b0865c29b0826aad32bcd11"
255
260
  },
256
261
  {
257
262
  "type": "file",
@@ -271,7 +276,7 @@
271
276
  {
272
277
  "type": "file",
273
278
  "path": "package.json",
274
- "sha256": "105c8c6caf3b46768fac8ed106f305b85307a71e5bdf55cc9395212584c4161a"
279
+ "sha256": "4c74e9d524e6d57a89b85936fc67b7d454a947c22686d87a0d7a09a1afade230"
275
280
  },
276
281
  {
277
282
  "type": "file",
@@ -132,6 +132,11 @@ export class SessionLog {
132
132
  let pendingThinkingTurn = null;
133
133
  let pendingBlocks = [];
134
134
  for (const e of projected) {
135
+ // 1.24-W2(D-W2-1):settlement 回执不投影(model_visible=0 同 usage 纪律——结算凭据是
136
+ // 审计面事实非模型上下文;system kind 语义位但 payload.type 判别排除,先于 thinking
137
+ // 隔断检查,杜绝任何投影面影响;无 settlement 链投影逐字节不变)
138
+ if (e.kind === 'system' && e.payload?.type === 'settlement')
139
+ continue;
135
140
  if (pendingBlocks.length && (e.turnId !== pendingThinkingTurn || e.kind === 'user' || e.kind === 'system'))
136
141
  pendingBlocks = [];
137
142
  if (e.kind === 'thinking') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -7,6 +7,7 @@
7
7
  * - 副作用审计:补记事件带 recovered=true 标记(副作用是否落地由恢复层显式声明为未知)
8
8
  */
9
9
  import type { SessionLog, SessionEvent } from '../session/log.ts'
10
+ import { appendSettlement } from './settlement.ts'
10
11
 
11
12
  export interface RecoveryReport {
12
13
  /** 补记 interrupted 的 turn 数 */
@@ -39,7 +40,10 @@ export function recoverInterrupted(log: SessionLog): RecoveryReport {
39
40
  report.pairedCalls++
40
41
  }
41
42
  // 未闭合 turn → 补记 turnEnd=interrupted(不可与 aborted 混用)
43
+ // 1.24-W2(D-W2-2):补记前先落 settlement——部分链对账 = hasUnknownOutcomes 主战场
44
+ // (上方成对补记的 recovered:true 合成结果在结算里按「结果不明」显式记账,不丢信号)
42
45
  for (const turnId of openTurns) {
46
+ appendSettlement(log, turnId, { reason: 'interrupted', recovered: true })
43
47
  log.append('runtime', 'turnEnd', turnId, null, { reason: 'interrupted', recovered: true })
44
48
  report.turnsClosed++
45
49
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 1.24-W2 · 回合结算回执(settlement receipt)
3
+ *
4
+ * 口径(《Qwen-Code 对照分析》§2.3/§5 / 借鉴四篇裁决记录 Q-W2 / 迭代规划 D-W2-1~6):
5
+ * - 「结算必留回执」:任何 turn 无论成败都有一张结构化凭据——观测 / 恢复 / 审计消费同一张
6
+ * - 落链形态 = kind:'system' + payload.type:'settlement'(EventKind 零新增;payload.type 扩展
7
+ * 沿 routing 'decide' / memory 'write|edit|delete|rollback' 先例);一 turn 一张,llm 面与
8
+ * tool 节合并(D-W2-1)
9
+ * - 产出点仅两处:runTurn closeTurn(turnEnd 之前——turnEnd 保持终止标记语义)与
10
+ * recoverInterrupted 补记(部分链对账);authz 台账 turnEnd 与 facade sessionStop 不经
11
+ * closeTurn、不触发(D-W2-2 登记)
12
+ * - 事实源 = 链(非运行时状态):两本账按链上 toolCall/toolResult payload.id 对账;
13
+ * permissionDenialCount = toolResult error:'authorization-denied' 且 granted===false;
14
+ * hasUnknownOutcomes = 无配对 toolResult 或配对结果 recovered:true(副作用状态未知显式记账);
15
+ * bounded = aborted-before-dispatch 补记形态在链(取消落在派发边界,未打断在途执行)
16
+ * - 字段 CAR 化:outcome 由 reason 映射(complete|interrupted|failed);failureKind = 1.23
17
+ * meta.kind 诚实缺键;assistantSeq 代 Qwen assistantMessageUuid;Qwen nextMaxOutputTokens
18
+ * 在 CAR 无对应机制 → 诚实缺键不预埋(D-W2-3 偏差登记)
19
+ * - turnEnd meta 零变化:settlement 是独立事件非 meta 尾键(D-W2-4,s78-16 字节兼容锚不动);
20
+ * stopRequested 经 closeTurn hints 传递不落 meta
21
+ * - kill-switch:CAR_SETTLEMENT=off 跳过 append(K1 env 逐 turn 求值先例);缺省开(D-W2-5)
22
+ * - 链量纪律(R-W2):定长小 payload——账本数组 cap 64 + truncated 标记;内容类信息不进链
23
+ */
24
+ import type { SessionLog } from '../session/log.ts'
25
+
26
+ export type SettlementOutcome = 'complete' | 'interrupted' | 'failed'
27
+
28
+ /** 账本数组上限(超出截断 + ledgerTruncated 标记——防病态长 turn 链量失控) */
29
+ export const SETTLEMENT_LEDGER_CAP = 64
30
+
31
+ export interface SettlementHints {
32
+ /** terminate 型收口(工具主动 stop 语义,stop.ts closeTurn hints——不落 turnEnd meta) */
33
+ stopRequested?: boolean
34
+ }
35
+
36
+ /** TurnEndReason → settlement outcome 映射(completed/max-tokens=complete;aborted/interrupted=interrupted;error/blocked=failed) */
37
+ export function outcomeFromReason(reason: string): SettlementOutcome {
38
+ if (reason === 'completed' || reason === 'max-tokens') return 'complete'
39
+ if (reason === 'aborted' || reason === 'interrupted') return 'interrupted'
40
+ return 'failed'
41
+ }
42
+
43
+ /**
44
+ * 结算回执落链(closeTurn / recover 共用单点)。meta = 同 turn turnEnd 将携带的 meta
45
+ * (reason/kind 等事实同源);usage/hints 缺省时从链自证(requestSent 由 usage 事件或
46
+ * assistant 在场推导)。
47
+ */
48
+ export function appendSettlement(
49
+ log: SessionLog,
50
+ turnId: string,
51
+ meta: Record<string, unknown>,
52
+ hints?: SettlementHints,
53
+ ): void {
54
+ if (process.env.CAR_SETTLEMENT === 'off') return
55
+ const reason = typeof meta.reason === 'string' ? meta.reason : 'error'
56
+ const kind = typeof meta.kind === 'string' ? meta.kind : undefined
57
+
58
+ // 单次链扫描:两本账 + 拒绝计数 + unknown/bounded 事实 + requestSent 事实 + assistantSeq
59
+ const declared: string[] = []
60
+ const resultById = new Map<string, Record<string, unknown>>()
61
+ let denialCount = 0
62
+ let bounded = false
63
+ let usageRequests = 0
64
+ let assistantSeq: number | undefined
65
+ for (const e of log.events) {
66
+ if (e.turnId !== turnId) continue
67
+ const p = (e.payload ?? {}) as Record<string, unknown>
68
+ if (e.kind === 'toolCall') {
69
+ if (typeof p.id === 'string') declared.push(p.id)
70
+ } else if (e.kind === 'toolResult') {
71
+ if (typeof p.id === 'string') {
72
+ resultById.set(p.id, p) // 1.6-S1 last-wins 同款口径
73
+ if (p.error === 'authorization-denied' && p.granted === false) denialCount++
74
+ if (p.error === 'aborted-before-dispatch') bounded = true
75
+ }
76
+ } else if (e.kind === 'usage') {
77
+ usageRequests++
78
+ } else if (e.kind === 'assistant') {
79
+ assistantSeq = e.seq
80
+ }
81
+ }
82
+ const dispatched = [...resultById.keys()]
83
+ const unknownIds = declared.filter((id) => {
84
+ const res = resultById.get(id)
85
+ return !res || res.recovered === true
86
+ })
87
+ const cap = (arr: string[]) => (arr.length > SETTLEMENT_LEDGER_CAP ? arr.slice(0, SETTLEMENT_LEDGER_CAP) : arr)
88
+ const ledgerTruncated = declared.length > SETTLEMENT_LEDGER_CAP || dispatched.length > SETTLEMENT_LEDGER_CAP
89
+
90
+ log.append('runtime', 'system', turnId, {
91
+ type: 'settlement',
92
+ outcome: outcomeFromReason(reason),
93
+ ...(kind ? { failureKind: kind } : {}),
94
+ requestSent: usageRequests > 0 || assistantSeq !== undefined,
95
+ ...(assistantSeq !== undefined ? { assistantSeq } : {}),
96
+ declaredToolUseIds: cap(declared),
97
+ dispatchedToolUseIds: cap(dispatched),
98
+ ...(ledgerTruncated ? { ledgerTruncated: true } : {}),
99
+ tool: {
100
+ permissionDenialCount: denialCount,
101
+ hasUnknownOutcomes: unknownIds.length > 0,
102
+ ...(unknownIds.length ? { unknownToolUseIds: cap(unknownIds) } : {}),
103
+ stopRequested: hints?.stopRequested === true,
104
+ bounded,
105
+ },
106
+ })
107
+ }
package/src/loop/stop.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import type { SessionLog } from '../session/log.ts'
18
18
  import { CarM8Error } from '../runtime-core/errors.ts'
19
+ import { appendSettlement } from './settlement.ts'
19
20
 
20
21
  export type StepOutcome = 'completed' | 'max-tokens' | 'null'
21
22
  export type TurnEndReason = 'completed' | 'max-tokens' | 'aborted' | 'error' | 'blocked' | 'interrupted'
@@ -74,8 +75,14 @@ export interface TurnPreset {
74
75
 
75
76
  export interface TurnResult { reason: TurnEndReason; steps: number; endReasonTrail: Array<{ seq: number; reason: TurnEndReason }> }
76
77
 
78
+ /**
79
+ * 1.23-W1(D-W1-6):取消时机标注(W-0 审计 F-W0-3——aborted 三路径链上不区分)——
80
+ * 'before-request' = 请求未发出(runTurn 循环头 / chatStep 零 chunk 流结束检查);
81
+ * 'in-flight' = 在途打断(chatStep chunk 间隙 / 有 chunk 后的流结束检查)。
82
+ * 缺省缺席 = 1.22 形态(terminate 型 abort 为工具主动收口语义,非取消,不带键)。
83
+ */
77
84
  export class TurnAborted extends Error {
78
- constructor() { super('CAR-ABORTED: turn cancelled by user') }
85
+ constructor(readonly abortedAt?: 'before-request' | 'in-flight') { super('CAR-ABORTED: turn cancelled by user') }
79
86
  }
80
87
 
81
88
  /**
@@ -123,13 +130,16 @@ export async function runTurn(opts: {
123
130
 
124
131
  // 1.11-S2(D-33):turnEnd 落链单点包装——聚合本 turn usage 事件(无 usage 不带键,
125
132
  // 缺省=未采集向后兼容);reason 保持 meta 首键,usage 恒为加法尾键
126
- const closeTurn = (meta: Record<string, unknown>) => {
133
+ // 1.24-W2(D-W2-2):turnEnd 之前先落 settlement 结算回执(turnEnd 保持终止标记语义;
134
+ // hints 仅 terminate 收口传递 stopRequested——不落 turnEnd meta,D-W2-4)
135
+ const closeTurn = (meta: Record<string, unknown>, hints?: { stopRequested?: boolean }) => {
127
136
  const usage = turnUsageFrom(log, turnId)
128
137
  // 1.18.2-K8(R-K7-2):计量缺失 fail-visible——商汤网关实测 20% 概率 message_delta
129
138
  // 不带 usage(流正常回复但计量整条缺席)→ 缺失口径纪律不落半条事实(turnUsageFrom
130
139
  // undefined),缺失本身在此显式标注:本 turn 存在 assistant 事件(模型有可见输出 ⇒
131
140
  // 必有请求)而无 usage 聚合时带 usageMissing: true(加法尾键,与 usage 同语义位)
132
141
  const hasAssistant = log.events.some((e) => e.kind === 'assistant' && e.turnId === turnId)
142
+ appendSettlement(log, turnId, meta, hints)
133
143
  return log.append('runtime', 'turnEnd', turnId, null, {
134
144
  ...meta,
135
145
  ...(usage ? { usage } : hasAssistant ? { usageMissing: true } : {}),
@@ -231,7 +241,8 @@ export async function runTurn(opts: {
231
241
  try {
232
242
  while (steps < maxSteps) {
233
243
  if (signal?.aborted) {
234
- const seq = closeTurn({ reason: 'aborted' }).seq
244
+ // 1.23-W1(D-W1-6):循环头取消 = 请求未发出(无在途请求)
245
+ const seq = closeTurn({ reason: 'aborted', abortedAt: 'before-request' }).seq
235
246
  trail.push({ seq, reason: 'aborted' })
236
247
  return { reason: 'aborted', steps, endReasonTrail: trail }
237
248
  }
@@ -294,7 +305,8 @@ export async function runTurn(opts: {
294
305
  const agg = preset.aggregate ?? {}
295
306
  const abortedNow = (agg.terminate ?? 'all') === 'any' ? batch.terminateVotes > 0 : batch.terminateAll
296
307
  if (abortedNow) { // terminate 收口:默认 AND(整批),可配置 any
297
- const seq = closeTurn({ reason: 'aborted' }).seq
308
+ // 1.24-W2:工具主动 stop 语义 → settlement stopRequested=true(hints 不落 meta,D-W2-4)
309
+ const seq = closeTurn({ reason: 'aborted' }, { stopRequested: true }).seq
298
310
  trail.push({ seq, reason: 'aborted' })
299
311
  return { reason: 'aborted', steps, endReasonTrail: trail }
300
312
  }
@@ -320,17 +332,19 @@ export async function runTurn(opts: {
320
332
  return { reason: 'error', steps, endReasonTrail: trail }
321
333
  } catch (e) {
322
334
  if (e instanceof TurnAborted) {
323
- const seq = closeTurn({ reason: 'aborted' }).seq
335
+ // 1.23-W1(D-W1-6):chatStep 甄别过的取消时机随异常携带(terminate 型 abort 不经此路径)
336
+ const seq = closeTurn({ reason: 'aborted', ...(e.abortedAt ? { abortedAt: e.abortedAt } : {}) }).seq
324
337
  trail.push({ seq, reason: 'aborted' })
325
338
  return { reason: 'aborted', steps, endReasonTrail: trail }
326
339
  }
327
340
  // 1.4-S4:CarM8Error 保形——code/userHint/retryable 进 turnEnd(A080001 引导文案「请运行
328
- // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
341
+ // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变);
342
+ // 1.23-W1(D-W1-3):+kind 失败细类(加法尾键——B080001 多形态分型,审计 F-W0-2)
329
343
  const m8 = e instanceof CarM8Error
330
344
  const detail = m8 ? `${e.slug}: ${e.message}` : String(e)
331
345
  const seq = closeTurn({
332
346
  reason: 'error', detail,
333
- ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
347
+ ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable, ...(e.kind ? { kind: e.kind } : {}) } : {}),
334
348
  }).seq
335
349
  trail.push({ seq, reason: 'error' })
336
350
  return { reason: 'error', steps, endReasonTrail: trail }
@@ -23,7 +23,7 @@ import type { RuntimeCore } from './llm.ts'
23
23
  import type { LlmUsage, ToolDefinition } from './types.ts'
24
24
  import { StreamRedactor } from './redaction.ts'
25
25
  import { scanSecrets } from '../security/secrets.ts'
26
- import { providerUnreachable } from './errors.ts'
26
+ import { CarM8Error, providerUnreachable } from './errors.ts'
27
27
 
28
28
  export interface ChatStepOptions {
29
29
  core: RuntimeCore
@@ -119,6 +119,12 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
119
119
  const thinkingRedactor = new StreamRedactor()
120
120
  let stopReason: ModelStep['stopReason'] | undefined
121
121
  let errorDetail: string | undefined
122
+ // 1.23-W1(D-W1-2):error chunk 细类捕获(最后一条 error chunk 的 kind——providerUnreachable
123
+ // 携带 → runTurn turnEnd meta.kind;缺失 = 1.22 形态)
124
+ let errorKind: string | undefined
125
+ // 1.23-W1(D-W1-6):零 chunk 甄别——流结束检查抛 TurnAborted 时区分「请求未发出(attempt 前
126
+ // 静默收口)」与「在途打断」;chunk 间隙路径恒 in-flight
127
+ let sawChunk = false
122
128
  let usage: LlmUsage | undefined
123
129
 
124
130
  const stream = core.chat({
@@ -133,8 +139,10 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
133
139
  ...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
134
140
  })
135
141
  for await (const chunk of stream) {
136
- // 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息
137
- if (opts.signal?.aborted) throw new TurnAborted()
142
+ sawChunk = true
143
+ // 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息;
144
+ // 1.23-W1(D-W1-6):在途打断标注 in-flight
145
+ if (opts.signal?.aborted) throw new TurnAborted('in-flight')
138
146
  if (chunk.delta !== undefined) {
139
147
  const masked = redactor.push(chunk.delta)
140
148
  text += masked
@@ -171,13 +179,18 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
171
179
  if (masked) opts.onDelta({ thinkingDelta: masked })
172
180
  }
173
181
  }
174
- if (chunk.error) errorDetail = chunk.error.message
182
+ if (chunk.error) {
183
+ errorDetail = chunk.error.message
184
+ errorKind = chunk.error.kind // 1.23-W1(D-W1-2):细类透传(最后一条 error chunk 为准)
185
+ }
175
186
  if (chunk.usage) usage = chunk.usage // D-33:附着于最后一个 chunk——后到覆盖
176
187
  if (chunk.finishReason !== undefined) stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason
177
188
  }
178
189
  // 1.4-S4(D-19):finish 检查前先查取消(适配器取消路径静默收口无 finishReason——
179
- // 必须先于此处的契约兜底,否则取消被误判为适配器违规)
180
- if (opts.signal?.aborted) throw new TurnAborted()
190
+ // 必须先于此处的契约兜底,否则取消被误判为适配器违规);
191
+ // 1.23-W1(D-W1-6):abortedAt 甄别——有 chunk = 在途打断(in-flight);
192
+ // 零 chunk = attempt 前静默收口,请求未发出(before-request)
193
+ if (opts.signal?.aborted) throw new TurnAborted(sawChunk ? 'in-flight' : 'before-request')
181
194
  if (stopReason === undefined) {
182
195
  // 适配器层已保证首 finishReason(AL-05);防御性兜底显式化
183
196
  throw new Error('CAR-E-LLM-FINISH: 流结束无 finishReason(适配器契约违规)')
@@ -256,16 +269,26 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
256
269
  if (!c.name) continue
257
270
  let args: Record<string, unknown>
258
271
  try { args = c.argsBuf ? JSON.parse(c.argsBuf) as Record<string, unknown> : {} } catch {
259
- // toolUse 声明下 args 非法 JSON = 协议违规显式化(非 length 场景无 truncatedTools 口径)
260
- throw new Error(`CAR-E-LLM-PROTO: 工具调用 ${c.name} 参数非法 JSON(finishReason=${stopReason},${errorDetail ?? '无附加错误'})`)
272
+ // toolUse 声明下 args 非法 JSON = 协议违规显式化(非 length 场景无 truncatedTools 口径);
273
+ // 1.23-W1(D-W1-3):转 CarM8Error——turnEnd meta 获得 code/userHint/retryable + kind
274
+ // 'proto-invalid-args'(detail 形态变更登记:CarM8Error 统一格式带(B080001)码后缀)
275
+ throw new CarM8Error(
276
+ 'B080001',
277
+ 'CAR-E-LLM-PROTO',
278
+ `工具调用 ${c.name} 参数非法 JSON(finishReason=${stopReason},${errorDetail ?? '无附加错误'})`,
279
+ undefined,
280
+ false,
281
+ 'proto-invalid-args',
282
+ )
261
283
  }
262
284
  toolCalls.push({ id: c.id ?? `tc-${idx}-${c.name}`, tool: c.name, args })
263
285
  }
264
286
  return { stopReason: 'toolUse', ...(text ? { text } : {}), ...(toolCalls.length ? { toolCalls } : {}), secretsRedacted }
265
287
  }
266
- // error(含 aborted 映射):以 B080001 抛出 → runTurn catch → turnEnd error 携带 detail(BD-04 收口)
288
+ // error(含 aborted 映射):以 B080001 抛出 → runTurn catch → turnEnd error 携带 detail(BD-04 收口);
289
+ // 1.23-W1(D-W1-2):细类 kind 透传(error chunk 捕获的 errorKind;缺失 = 1.22 形态)
267
290
  if (stopReason === 'error') {
268
- throw providerUnreachable(errorDetail ?? 'provider 返回 error 终止(无附加说明)')
291
+ throw providerUnreachable(errorDetail ?? 'provider 返回 error 终止(无附加说明)', errorKind)
269
292
  }
270
293
  return { stopReason: 'stop', ...(text ? { text } : {}), secretsRedacted }
271
294
  }
@@ -20,6 +20,12 @@ export class CarM8Error extends Error {
20
20
  readonly userHint?: string,
21
21
  /** 重试建议(A080001 不重试 / B080001 退避后重试) */
22
22
  readonly retryable = false,
23
+ /**
24
+ * 1.23-W1(D-W1-3):失败细类(LlmErrorKind 注册表)——B080001 一码多形态的分型标注,
25
+ * 经 runTurn closeTurn 落 turnEnd meta.kind(加法尾键,消费面免字符串反解)。
26
+ * 缺省缺席 = 1.22 形态(A080001 凭据缺失以独立码区分,无需 kind)。
27
+ */
28
+ readonly kind?: string,
23
29
  ) {
24
30
  super(`${slug}: ${message}(${code})`)
25
31
  this.name = 'CarM8Error'
@@ -36,12 +42,13 @@ export function credentialMissing(provider: string, searched: string[]): CarM8Er
36
42
  )
37
43
  }
38
44
 
39
- export function providerUnreachable(detail: string): CarM8Error {
45
+ export function providerUnreachable(detail: string, kind?: string): CarM8Error {
40
46
  return new CarM8Error(
41
47
  'B080001',
42
48
  'CAR-E-LLM-UNREACHABLE',
43
49
  `LLM Provider 不可达/重试耗尽:${detail}`,
44
50
  '模型服务暂时不可用,本轮已停止',
45
51
  true,
52
+ kind,
46
53
  )
47
54
  }
@@ -244,7 +244,9 @@ async function* streamSsePost(spec: SsePostSpec, parse: (body: AsyncIterable<Uin
244
244
  if (!res.ok) {
245
245
  const text = await res.text().catch(() => '')
246
246
  // 4xx 业务错不重试(§3.5.4);错误信息不含凭据(凭据值不进错误路径)
247
- yield { finishReason: 'error', error: { code: 'B080001', message: `provider HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}` } }
247
+ // 1.23-W1(D-W1-5):4xx 细类标注——overflow 特征命中 = context-limit-suspect(疑似),否则 transport-4xx
248
+ const kind = contextLimitSuspect(text) ? 'context-limit-suspect' : 'transport-4xx'
249
+ yield { finishReason: 'error', error: { code: 'B080001', message: `provider HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}`, kind } }
248
250
  return
249
251
  }
250
252
  if (!res.body) throw new Retryable('响应无 body 流')
@@ -260,7 +262,8 @@ async function* streamSsePost(spec: SsePostSpec, parse: (body: AsyncIterable<Uin
260
262
  // 流自然结束但未给 finishReason = 异常终止(AL-05 口径:缺失显式化,禁吞没);
261
263
  // 已携带 finishReason 的流原样收口——兜底不得追加(M8-BUG-1 口径延续)
262
264
  if (!sawFinish) {
263
- yield { finishReason: 'error', error: { code: 'B080001', message: '流结束未携带 finish_reason(AL-05)' } }
265
+ // 1.23-W1:细类 no-finish-reason(审计 F-W0-2 拆型)
266
+ yield { finishReason: 'error', error: { code: 'B080001', message: '流结束未携带 finish_reason(AL-05)', kind: 'no-finish-reason' } }
264
267
  }
265
268
  return
266
269
  } catch (e) {
@@ -274,11 +277,11 @@ async function* streamSsePost(spec: SsePostSpec, parse: (body: AsyncIterable<Uin
274
277
  continue
275
278
  }
276
279
  if (beforeFirstByte) {
277
- // 重试耗尽 → B080001(BD-04:消费方 runTurn 以 error 收口)
278
- throw providerUnreachable(`${(e as Error).name}: ${(e as Error).message}(尝试 ${attempt + 1}/${spec.retries + 1},${Date.now() - started}ms)`)
280
+ // 重试耗尽 → B080001(BD-04:消费方 runTurn 以 error 收口);1.23-W1 细类 transport-exhausted
281
+ throw providerUnreachable(`${(e as Error).name}: ${(e as Error).message}(尝试 ${attempt + 1}/${spec.retries + 1},${Date.now() - started}ms)`, 'transport-exhausted')
279
282
  }
280
- // 流中途失败:已消费 chunk 不可重放 → error chunk 收口
281
- yield { finishReason: 'error', error: { code: 'B080001', message: `流中途失败:${(e as Error).message}` } }
283
+ // 流中途失败:已消费 chunk 不可重放 → error chunk 收口;1.23-W1 细类 stream-midbreak
284
+ yield { finishReason: 'error', error: { code: 'B080001', message: `流中途失败:${(e as Error).message}`, kind: 'stream-midbreak' } }
282
285
  return
283
286
  } finally {
284
287
  clearTimeout(firstByteTimer)
@@ -317,6 +320,23 @@ const FINISH_MAP: Record<string, LlmChunk['finishReason']> = {
317
320
 
318
321
  class Retryable extends Error {}
319
322
 
323
+ /**
324
+ * 1.23-W1(D-W1-5):context-limit-suspect 确定性关键词表——仅用于 4xx 错误体、只收高置信特征;
325
+ * 标注语义 = **疑似**(消费面文案须带「疑似」,禁估算禁 LLM 判断);未命中 = transport-4xx。
326
+ */
327
+ const CONTEXT_LIMIT_PATTERNS: RegExp[] = [
328
+ /context length/i,
329
+ /maximum context/i,
330
+ /context[_ ]window/i,
331
+ /prompt is too long/i,
332
+ /too many tokens/i,
333
+ /input tokens? exceed/i,
334
+ /exceeds? the (maximum )?(context|token)/i,
335
+ /上下文(长度|超限)/,
336
+ /超出上下文/,
337
+ ]
338
+ const contextLimitSuspect = (text: string): boolean => CONTEXT_LIMIT_PATTERNS.some((p) => p.test(text))
339
+
320
340
  /**
321
341
  * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
322
342
  * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
@@ -399,13 +419,21 @@ async function* parseSseStream(body: AsyncIterable<Uint8Array>, model: string, a
399
419
  if (usage) yield { usage }
400
420
  continue
401
421
  }
422
+ // 1.23-W1(D-W1-4 / D-W1-1):finish 终态附带 error 细类——未映射 = unmapped-finish;
423
+ // content_filter(openai 族唯一映射 'aborted' 的信号)= refusal(与 anthropic stop_reason=refusal 对齐)
424
+ const finishError: LlmChunk['error'] | undefined =
425
+ finish === 'error'
426
+ ? { code: 'B080001', message: `未映射的 finish_reason "${raw}"`, kind: 'unmapped-finish' }
427
+ : finish === 'aborted'
428
+ ? { code: 'B080001', message: `provider content_filter (finish_reason="${raw}")`, kind: 'refusal' }
429
+ : undefined
402
430
  const chunks: LlmChunk[] = []
403
431
  if (toolCallDeltas.length === 0) {
404
432
  chunks.push({
405
433
  ...(reasoningContent !== undefined ? { thinkingDelta: reasoningContent } : {}),
406
434
  ...(text !== undefined ? { delta: text } : {}),
407
435
  ...(finish !== undefined
408
- ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
436
+ ? { finishReason: finish, ...(finishError ? { error: finishError } : {}) }
409
437
  : {}),
410
438
  })
411
439
  } else {
@@ -416,7 +444,7 @@ async function* parseSseStream(body: AsyncIterable<Uint8Array>, model: string, a
416
444
  ...(i === 0 && text !== undefined ? { delta: text } : {}),
417
445
  toolCallDelta: toolCallDeltas[i],
418
446
  ...(last && finish !== undefined
419
- ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
447
+ ? { finishReason: finish, ...(finishError ? { error: finishError } : {}) }
420
448
  : {}),
421
449
  })
422
450
  }
@@ -835,17 +863,22 @@ async function* parseAnthropicSseStream(body: AsyncIterable<Uint8Array>, model:
835
863
  const usage = mergeAnthropicUsage(startUsage, (evt.usage ?? null) as Record<string, unknown> | null, model, adapterId)
836
864
  if (raw != null) {
837
865
  const finish = ANTHROPIC_STOP_REASON_MAP[String(raw)]
866
+ // 1.23-W1(D-W1-4 / D-W1-1):stop_reason 细类标注——refusal 附加 error chunk(kind 'refusal',
867
+ // 与 openai content_filter 对齐;finishReason 映射不动);未映射 = unmapped-finish
838
868
  const chunk: LlmChunk = finish
839
- ? { finishReason: finish }
840
- : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"` } }
869
+ ? finish === 'aborted'
870
+ ? { finishReason: finish, error: { code: 'B080001', message: `provider refusal (stop_reason="${raw}")`, kind: 'refusal' } }
871
+ : { finishReason: finish }
872
+ : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"`, kind: 'unmapped-finish' } }
841
873
  yield usage ? { ...chunk, usage } : chunk
842
874
  } else if (usage) {
843
875
  // delta 未携 stop_reason 但携 usage(网关形态):usage 不随缺失终态信号丢失
844
876
  yield { usage }
845
877
  }
846
878
  } else if (type === 'error') {
847
- // 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突)
848
- yield { finishReason: 'error', error: { code: 'B080001', message: `provider 流错误:${evt.error?.message ?? JSON.stringify(evt).slice(0, 200)}` } }
879
+ // 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突);
880
+ // 1.23-W1 细类 provider-stream-error
881
+ yield { finishReason: 'error', error: { code: 'B080001', message: `provider 流错误:${evt.error?.message ?? JSON.stringify(evt).slice(0, 200)}`, kind: 'provider-stream-error' } }
849
882
  }
850
883
  }
851
884
  }
@@ -40,6 +40,23 @@ export interface ToolCallDelta {
40
40
  argumentsDelta?: string
41
41
  }
42
42
 
43
+ /**
44
+ * 1.23-W1(D-W1-1 / 《W-0 失败分类学对照审计》§5):LLM 失败细类注册表——把 B080001 下压着的
45
+ * 多形态失败拆到可对应处置动作的粒度(审计 F-W0-2)。**加法演进**:可选键,缺失 = 1.22 形态
46
+ * 逐字节兼容。`transport-network` 从审计 8 类剔除(无独立产出点——网络失败只以 exhausted /
47
+ * midbreak 到达消费面);`unmapped-finish` / `provider-stream-error` 为实施中浮出的两形态增补。
48
+ */
49
+ export type LlmErrorKind =
50
+ | 'transport-4xx'
51
+ | 'transport-exhausted'
52
+ | 'stream-midbreak'
53
+ | 'no-finish-reason'
54
+ | 'refusal'
55
+ | 'proto-invalid-args'
56
+ | 'context-limit-suspect'
57
+ | 'unmapped-finish'
58
+ | 'provider-stream-error'
59
+
43
60
  /**
44
61
  * 1.11-S1(D-33 / 计量设计 §3.1):token 四分量归一化用量——跨 provider 可加总的库内口径
45
62
  * (§1.1:openai prompt_tokens 含 cached 必须减法归一;anthropic input_tokens 天然不含 cache)。
@@ -72,8 +89,9 @@ export interface LlmChunk {
72
89
  toolCallDelta?: ToolCallDelta
73
90
  finishReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted'
74
91
  // —— 规格外加法扩展(登记:M8 收口文档 §4 / 计量设计 §3.1,1.11.0)——
75
- /** finishReason='error' 时的结构化原因(消费方落 turnEnd meta,不进模型消息流) */
76
- error?: { code: string; message: string }
92
+ /** finishReason='error' 时的结构化原因(消费方落 turnEnd meta,不进模型消息流);
93
+ * 1.23-W1(D-W1-2):+kind 失败细类(LlmErrorKind 注册表——B080001 一码多形态的分型标注) */
94
+ error?: { code: string; message: string; kind?: LlmErrorKind }
77
95
  /** 1.11-S1(D-33):每次请求最后一个 chunk 附带归一化用量;缺失 = provider 未回,禁补造 */
78
96
  usage?: LlmUsage
79
97
  // —— 1.21(R-TOKENHUB-1 / D-T1)thinking 捕获加法——anthropic SSE thinking 面捕获;
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto'
11
11
  import { readFileSync } from 'node:fs'
12
12
  import { decodeLogBuffer, splitJsonlLines } from './format.ts'
13
13
 
14
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' | 'compact' | 'routing' | 'memory' | 'thinking' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);1.11-S2 加法扩展:usage 计量事实(D-33,actor='runtime'、model_visible=0 不进模型消息流,deriveMessages 不投影);六值 TurnEndReason 不变;1.14-S2 加法扩展(K-MP-2 / D-40):历史压缩事件(actor='runtime',append-only 只追加——payload={summary, compactedUpTo, ...},投影按 compactedUpTo 前缀折叠为摘要,原事件链不改写);1.14-S4 加法扩展(K-POOL-1 / D-42):模型池路由决策(actor='runtime',payload.type='decide',selected/reason/retries/attempts——禁静默换模型;model_visible=0 不进模型消息流,deriveMessages 不投影——同 usage 纪律);1.15-S1 加法扩展(K-MEM-1 / D-46):白盒记忆操作事件(actor='user' 宿主代理用户操作,turnId='memory' 会话级标识同 'compaction' 先例,payload.type='write'|'edit'|'delete'|'rollback'——审计链同源差异化面,链即版本库〔rollback 恢复数据源=链上 payload.before/after 全文〕;model_visible=0 不进模型消息流——记忆注入走宿主装配层 system ctx 行,deriveMessages 不投影);1.21 加法扩展(R-TOKENHUB-1 / D-T2):thinking 事件(actor='model',payload=thinking 原文禁改写,meta={signature?, redacted?, secretsRedacted?}——先于 assistant/toolCall 落链;投影 = 同 turn 首个 toolCall 消息附加回传(工具环 reasoning_content 契约,Anthropic thinking+tool_use 规范同款);standalone/隔断/跨 turn 不投影;无 thinking 事件链投影逐字节不变;1.22 加法扩展(1.22 迭代规划 D-B1):多块语义——一块一事件(多块链每事件 meta.blockIndex=块序,单块链 meta 与 1.21 逐字节同形),投影单块 = 1.21 三元组、多块 = blocks 数组(D-B2,消费方 toAnthropic 双形态展开));
14
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' | 'compact' | 'routing' | 'memory' | 'thinking' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);1.11-S2 加法扩展:usage 计量事实(D-33,actor='runtime'、model_visible=0 不进模型消息流,deriveMessages 不投影);六值 TurnEndReason 不变;1.14-S2 加法扩展(K-MP-2 / D-40):历史压缩事件(actor='runtime',append-only 只追加——payload={summary, compactedUpTo, ...},投影按 compactedUpTo 前缀折叠为摘要,原事件链不改写);1.14-S4 加法扩展(K-POOL-1 / D-42):模型池路由决策(actor='runtime',payload.type='decide',selected/reason/retries/attempts——禁静默换模型;model_visible=0 不进模型消息流,deriveMessages 不投影——同 usage 纪律);1.15-S1 加法扩展(K-MEM-1 / D-46):白盒记忆操作事件(actor='user' 宿主代理用户操作,turnId='memory' 会话级标识同 'compaction' 先例,payload.type='write'|'edit'|'delete'|'rollback'——审计链同源差异化面,链即版本库〔rollback 恢复数据源=链上 payload.before/after 全文〕;model_visible=0 不进模型消息流——记忆注入走宿主装配层 system ctx 行,deriveMessages 不投影);1.21 加法扩展(R-TOKENHUB-1 / D-T2):thinking 事件(actor='model',payload=thinking 原文禁改写,meta={signature?, redacted?, secretsRedacted?}——先于 assistant/toolCall 落链;投影 = 同 turn 首个 toolCall 消息附加回传(工具环 reasoning_content 契约,Anthropic thinking+tool_use 规范同款);standalone/隔断/跨 turn 不投影;无 thinking 事件链投影逐字节不变;1.22 加法扩展(1.22 迭代规划 D-B1):多块语义——一块一事件(多块链每事件 meta.blockIndex=块序,单块链 meta 与 1.21 逐字节同形),投影单块 = 1.21 三元组、多块 = blocks 数组(D-B2,消费方 toAnthropic 双形态展开);1.24-W2 加法扩展(Q-W2 / D-W2-1):system kind payload.type='settlement' 回合结算回执(actor='runtime',一 turn 一张——closeTurn〔turnEnd 之前〕与 recoverInterrupted 补记两产出点,字段见 loop/settlement.ts,CAR_SETTLEMENT=off 可关;model_visible=0 白名单不投影同 usage 纪律));
15
15
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime'
16
16
  export interface SessionEvent {
17
17
  seq: number
@@ -156,6 +156,10 @@ export class SessionLog {
156
156
  let pendingThinkingTurn: string | null = null
157
157
  let pendingBlocks: Array<{ text: string; signature?: string; redacted?: boolean }> = []
158
158
  for (const e of projected) {
159
+ // 1.24-W2(D-W2-1):settlement 回执不投影(model_visible=0 同 usage 纪律——结算凭据是
160
+ // 审计面事实非模型上下文;system kind 语义位但 payload.type 判别排除,先于 thinking
161
+ // 隔断检查,杜绝任何投影面影响;无 settlement 链投影逐字节不变)
162
+ if (e.kind === 'system' && (e.payload as { type?: unknown } | null)?.type === 'settlement') continue
159
163
  if (pendingBlocks.length && (e.turnId !== pendingThinkingTurn || e.kind === 'user' || e.kind === 'system')) pendingBlocks = []
160
164
  if (e.kind === 'thinking') {
161
165
  const p = typeof e.payload === 'string' ? e.payload : ''
Binary file
@@ -1 +0,0 @@
1
- {"base64Signature":"MEQCIG5kpHlRBiuR0C1XTzOWawwa6B3jaeuUCD30RgQpZCPdAiBO9uojSbYcCjrLKnD668Am0dOc0yjRdG7lTZ92TpEwWQ==","cert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUc1akNDQm15Z0F3SUJBZ0lVRkdNdTBsKytWSE1uK0lQbFNTVnNPSEkrbDRJd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZeE1URTBNVEF3T1RFeVdoY05Nall4TVRFME1UQXhPVEV5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjcW03aXVPL1ljT21HcTdVYXpCZUtSa0JVQlpPbXZYREdLaXcKQ2J3TFJhMHdKS1lUQW1QQWRwMDRFR2s3ZXB1WDVBeGdIaG03b1pLSFRKK0ZWd0tBY0tPQ0JZc3dnZ1dITUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWZCZ05WSFNNRUdEQVdnQlRmCjArblBWaVFSbHZtbzJPa29WYUxHTGhoa1B6QmlCZ05WSFJFQkFmOEVXREJXaGxSb2RIUndjem92TDJkcGRHaDEKWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHpMM0psYkdWaApjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakl5TGpFd09RWUtLd1lCQkFHRHZ6QUJBUVFyYUhSMGNITTZMeTkwCmIydGxiaTVoWTNScGIyNXpMbWRwZEdoMVluVnpaWEpqYjI1MFpXNTBMbU52YlRBVkJnb3JCZ0VFQVlPL01BRUMKQkFkeVpXeGxZWE5sTURZR0Npc0dBUVFCZzc4d0FRTUVLREF5TjJOa1pUVTRaVFZoWlRZMll6WmlaV1kwWXpVNApPV0l5TUdZeVkyUXhaRFppTVRFMk5EZ3dGUVlLS3dZQkJBR0R2ekFCQkFRSGNtVnNaV0Z6WlRBZ0Jnb3JCZ0VFCkFZTy9NQUVGQkJKc2NXTTJNalF2WTJGeUxYSjFiblJwYldVd0hnWUtLd1lCQkFHRHZ6QUJCZ1FRY21WbWN5OTAKWVdkekx6RXVNakl1TVRBN0Jnb3JCZ0VFQVlPL01BRUlCQzBNSzJoMGRIQnpPaTh2ZEc5clpXNHVZV04wYVc5dQpjeTVuYVhSb2RXSjFjMlZ5WTI5dWRHVnVkQzVqYjIwd1pBWUtLd1lCQkFHRHZ6QUJDUVJXREZSb2RIUndjem92CkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHoKTDNKbGJHVmhjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakl5TGpFd09BWUtLd1lCQkFHRHZ6QUJDZ1FxRENndwpNamRqWkdVMU9HVTFZV1UyTm1NMlltVm1OR00xT0RsaU1qQm1NbU5rTVdRMllqRXhOalE0TUIwR0Npc0dBUVFCCmc3OHdBUXNFRHd3TloybDBhSFZpTFdodmMzUmxaREExQmdvckJnRUVBWU8vTUFFTUJDY01KV2gwZEhCek9pOHYKWjJsMGFIVmlMbU52YlM5c2NXTTJNalF2WTJGeUxYSjFiblJwYldVd09BWUtLd1lCQkFHRHZ6QUJEUVFxRENndwpNamRqWkdVMU9HVTFZV1UyTm1NMlltVm1OR00xT0RsaU1qQm1NbU5rTVdRMllqRXhOalE0TUNBR0Npc0dBUVFCCmc3OHdBUTRFRWd3UWNtVm1jeTkwWVdkekx6RXVNakl1TVRBYUJnb3JCZ0VFQVlPL01BRVBCQXdNQ2pFek5qRXgKT1RJM01UUXdLUVlLS3dZQkJBR0R2ekFCRUFRYkRCbG9kSFJ3Y3pvdkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMApNQmdHQ2lzR0FRUUJnNzh3QVJFRUNnd0lOamt5TXpRMU5qUXdaQVlLS3dZQkJBR0R2ekFCRWdSV0RGUm9kSFJ3CmN6b3ZMMmRwZEdoMVlpNWpiMjB2YkhGak5qSTBMMk5oY2kxeWRXNTBhVzFsTHk1bmFYUm9kV0l2ZDI5eWEyWnMKYjNkekwzSmxiR1ZoYzJVdWVXMXNRSEpsWm5NdmRHRm5jeTh4TGpJeUxqRXdPQVlLS3dZQkJBR0R2ekFCRXdRcQpEQ2d3TWpkalpHVTFPR1UxWVdVMk5tTTJZbVZtTkdNMU9EbGlNakJtTW1Oa01XUTJZakV4TmpRNE1CY0dDaXNHCkFRUUJnNzh3QVJRRUNRd0hjbVZzWldGelpUQlpCZ29yQmdFRUFZTy9NQUVWQkVzTVNXaDBkSEJ6T2k4dloybDAKYUhWaUxtTnZiUzlzY1dNMk1qUXZZMkZ5TFhKMWJuUnBiV1V2WVdOMGFXOXVjeTl5ZFc1ekx6UXpOVGd6T0RZMQpOVEl5TDJGMGRHVnRjSFJ6THpFd0ZnWUtLd1lCQkFHRHZ6QUJGZ1FJREFad2RXSnNhV013VUFZS0t3WUJCQUdECnZ6QUJHQVJDREVCeVpYQnZPbXh4WXpZeU5FQTJPVEl6TkRVMk5DOWpZWEl0Y25WdWRHbHRaVUF4TXpZeE1Ua3kKTnpFME9uSmxaanB5WldaekwzUmhaM012TVM0eU1pNHhNSUdKQmdvckJnRUVBZFo1QWdRQ0JIc0VlUUIzQUhVQQozVDB3YXNiSEVUSmpHUjRjbVdjM0FxSktYcmplUEszL2g0cHlnQzhwN280QUFBR2gyWWhTSndBQUJBTUFSakJFCkFpQmtYUkJoenJNKzlRdkpQeUQ1Qk1qNWZZNEhaQzFrdVZPOGM3SE1UejUyZHdJZ2ZqcHdUZC95YnAzTEhRaEoKd01wMUxnQXdyaFlwZFVvZDZjcUFpUDZYTWxrd0NnWUlLb1pJemowRUF3TURhQUF3WlFJd1ZsWUJyeE80d1hWNQozVERDN3F1WDJ4OTlHaFBQR3gwenQ0dUt4WVE4S2R5azhwNityL0VuSkxJSlVRc3F3THFUQWpFQWlvcGZGbWxuCmxGSXl2dnROeWV0SjRJcVBzRkpXeUZSa0xsMzAzTXRKamZNRVNwRmN0dVlrbmhmOVoyZFFVYlB0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K","rekorBundle":{"SignedEntryTimestamp":"MEQCIEjmbV0RoSQts2UKhH/hILFK0yAWexpkjLbd/Tp/jGXHAiAdgKwePusa2Oju7+xAwaXZ+DBLjv7N/VWl6srH8VpMEQ==","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkMjg3MzgxNDMxMmM1ZGQ2NTQ4MGRlNDc2ZmIyY2NlYWI3YjBlYzU5YmU5MmYzN2ZiN2E3ZDZmZDRhNjFiZjgyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRzVrcEhsUkJpdVIwQzFYVHpPV2F3d2E2QjNqYWV1VUNEMzBSZ1FwWkNQZEFpQk85dW9qU2JZY0NqckxLbkQ2NjhBbTBkT2MweWpSZEc3bFRaOTJUcEV3V1E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVYzFha05EUW0xNVowRjNTVUpCWjBsVlJrZE5kVEJzS3l0V1NFMXVLMGxRYkZOVFZuTlBTRWtyYkRSSmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFplRTFVUlRCTlZFRjNUMVJGZVZkb1kwNU5hbGw0VFZSRk1FMVVRWGhQVkVWNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZqY1cwM2FYVlBMMWxqVDIxSGNUZFZZWHBDWlV0U2EwSlZRbHBQYlhaWVJFZExhWGNLUTJKM1RGSmhNSGRLUzFsVVFXMVFRV1J3TURSRlIyczNaWEIxV0RWQmVHZElhRzAzYjFwTFNGUktLMFpXZDB0QlkwdFBRMEpaYzNkbloxZElUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXWkNaMDVXU0ZOTlJVZEVRVmRuUWxSbUNqQXJibEJXYVZGU2JIWnRiekpQYTI5V1lVeEhUR2hvYTFCNlFtbENaMDVXU0ZKRlFrRm1PRVZYUkVKWGFHeFNiMlJJVW5kamVtOTJUREprY0dSSGFERUtXV2sxYW1JeU1IWmlTRVpxVG1wSk1Fd3lUbWhqYVRGNVpGYzFNR0ZYTVd4TWVUVnVZVmhTYjJSWFNYWmtNamw1WVRKYWMySXpaSHBNTTBwc1lrZFdhQXBqTWxWMVpWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9IaE1ha2w1VEdwRmQwOVJXVXRMZDFsQ1FrRkhSSFo2UVVKQlVWRnlZVWhTTUdOSVRUWk1lVGt3Q21JeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUZwWE5UQk1iVTUyWWxSQlZrSm5iM0pDWjBWRlFWbFBMMDFCUlVNS1FrRmtlVnBYZUd4WldFNXNUVVJaUjBOcGMwZEJVVkZDWnpjNGQwRlJUVVZMUkVGNVRqSk9hMXBVVlRSYVZGWm9XbFJaTWxsNldtbGFWMWt3V1hwVk5BcFBWMGw1VFVkWmVWa3lVWGhhUkZwcFRWUkZNazVFWjNkR1VWbExTM2RaUWtKQlIwUjJla0ZDUWtGUlNHTnRWbk5hVjBaNldsUkJaMEpuYjNKQ1owVkZDa0ZaVHk5TlFVVkdRa0pLYzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMGhuV1V0TGQxbENRa0ZIUkhaNlFVSkNaMUZSWTIxV2JXTjVPVEFLV1Zka2VreDZSWFZOYWtsMVRWUkJOMEpuYjNKQ1owVkZRVmxQTDAxQlJVbENRekJOU3pKb01HUklRbnBQYVRoMlpFYzVjbHBYTkhWWlYwNHdZVmM1ZFFwamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcVlqSXdkMXBCV1V0TGQxbENRa0ZIUkhaNlFVSkRVVkpYUkVaU2IyUklVbmRqZW05MkNrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUV3eVRtaGphVEY1WkZjMU1HRlhNV3hNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIb0tURE5LYkdKSFZtaGpNbFYxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T0hoTWFrbDVUR3BGZDA5QldVdExkMWxDUWtGSFJIWjZRVUpEWjFGeFJFTm5kd3BOYW1ScVdrZFZNVTlIVlRGWlYxVXlUbTFOTWxsdFZtMU9SMDB4VDBSc2FVMXFRbTFOYlU1clRWZFJNbGxxUlhoT2FsRTBUVUl3UjBOcGMwZEJVVkZDQ21jM09IZEJVWE5GUkhkM1Rsb3liREJoU0ZacFRGZG9kbU16VW14YVJFRXhRbWR2Y2tKblJVVkJXVTh2VFVGRlRVSkRZMDFLVjJnd1pFaENlazlwT0hZS1dqSnNNR0ZJVm1sTWJVNTJZbE01YzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMDlCV1V0TGQxbENRa0ZIUkhaNlFVSkVVVkZ4UkVObmR3cE5hbVJxV2tkVk1VOUhWVEZaVjFVeVRtMU5NbGx0Vm0xT1IwMHhUMFJzYVUxcVFtMU5iVTVyVFZkUk1sbHFSWGhPYWxFMFRVTkJSME5wYzBkQlVWRkNDbWMzT0hkQlVUUkZSV2QzVVdOdFZtMWplVGt3V1Zka2VreDZSWFZOYWtsMVRWUkJZVUpuYjNKQ1owVkZRVmxQTDAxQlJWQkNRWGROUTJwRmVrNXFSWGdLVDFSSk0wMVVVWGRMVVZsTFMzZFpRa0pCUjBSMmVrRkNSVUZSWWtSQ2JHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUFwTlFtZEhRMmx6UjBGUlVVSm5OemgzUVZKRlJVTm5kMGxPYW10NVRYcFJNVTVxVVhkYVFWbExTM2RaUWtKQlIwUjJla0ZDUldkU1YwUkdVbTlrU0ZKM0NtTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2hHYWs1cVNUQk1NazVvWTJreGVXUlhOVEJoVnpGc1RIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTUtZak5rZWt3elNteGlSMVpvWXpKVmRXVlhNWE5SU0Vwc1dtNU5kbVJIUm01amVUaDRUR3BKZVV4cVJYZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JYZFJjUXBFUTJkM1RXcGthbHBIVlRGUFIxVXhXVmRWTWs1dFRUSlpiVlp0VGtkTk1VOUViR2xOYWtKdFRXMU9hMDFYVVRKWmFrVjRUbXBSTkUxQ1kwZERhWE5IQ2tGUlVVSm5OemgzUVZKUlJVTlJkMGhqYlZaeldsZEdlbHBVUWxwQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtWelRWTlhhREJrU0VKNlQyazRkbG95YkRBS1lVaFdhVXh0VG5aaVV6bHpZMWROTWsxcVVYWlpNa1o1VEZoS01XSnVVbkJpVjFWMldWZE9NR0ZYT1hWamVUbDVaRmMxZWt4NlVYcE9WR2Q2VDBSWk1RcE9WRWw1VERKR01HUkhWblJqU0ZKNlRIcEZkMFpuV1V0TGQxbENRa0ZIUkhaNlFVSkdaMUZKUkVGYWQyUlhTbk5oVjAxM1ZVRlpTMHQzV1VKQ1FVZEVDblo2UVVKSFFWSkRSRVZDZVZwWVFuWlBiWGg0V1hwWmVVNUZRVEpQVkVsNlRrUlZNazVET1dwWldFbDBZMjVXZFdSSGJIUmFWVUY0VFhwWmVFMVVhM2tLVG5wRk1FOXVTbXhhYW5CNVdsZGFla3d6VW1oYU0wMTJUVk0wZVUxcE5IaE5TVWRLUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJYzBWbFVVSXpRVWhWUVFvelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIyZ3lXV2hUU25kQlFVSkJUVUZTYWtKRkNrRnBRbXRZVWtKb2VuSk5LemxSZGtwUWVVUTFRazFxTldaWk5FaGFRekZyZFZaUE9HTTNTRTFVZWpVeVpIZEpaMlpxY0hkVVpDOTVZbkF6VEVoUmFFb0tkMDF3TVV4blFYZHlhRmx3WkZWdlpEWmpjVUZwVURaWVRXeHJkME5uV1VsTGIxcEplbW93UlVGM1RVUmhRVUYzV2xGSmQxWnNXVUp5ZUU4MGQxaFdOUW96VkVSRE4zRjFXREo0T1RsSGFGQlFSM2d3ZW5RMGRVdDRXVkU0UzJSNWF6aHdOaXR5TDBWdVNreEpTbFZSYzNGM1RIRlVRV3BGUVdsdmNHWkdiV3h1Q214R1NYbDJkblJPZVdWMFNqUkpjVkJ6UmtwWGVVWlNhMHhzTXpBelRYUkthbVpOUlZOd1JtTjBkVmxyYm1obU9Wb3laRkZWWWxCMENpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19","integratedTime":1794650953,"logIndex":3875442005,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}