Ë
    âß²jB5  ã                   ó  — d Z ddlZddlZddlZdZg e¢d‘­Zdedefd„ZdZ	e	d	d
dfZ
dej                  defd„Zdedefd„Zdedefd„ZdZdeez  dz  defd„ZdZdZ eedd«      Z ej,                  d«      Zdefd„Z G d„ de«      Z G d„ de«      Z G d„ de«      Z G d„ de«      Z G d„ d e«      Z G d!„ d"e«      Zd#Z d$Z!d%Z"d&Z#d'e#› d(�Z$ G d)„ d*ejJ                  «      Z&d+Z'ed,fed-fed.fed/fed0fe&d1ffZ(d2d3d4d5d6ed1fd7fZ)defd8„Z*y)9z¿Exception types and error-classification predicates for the state store.
Shared by hermes_state and its mixins; string predicates match wrapped RPC
strings as well as live sqlite3 exceptions.é    N)zmalformed database schemaz database disk image is malformedÚexcÚreturnc                 ól   ‡ — t        ‰ t        j                  «      xr t        ˆ fd„t        D «       «      S )u—   Malformed-schema OR generic corrupt-image error. Diagnostics / offline
    recovery only â€” runtime repair must use :func:`is_malformed_schema_error`.c              3   óT   •K  — | ]  }|t        ‰«      j                  «       v –— Œ! y ­w©N©ÚstrÚlower©Ú.0Úmarkerr   s     €ú8/home/ubuntu/.hermes/hermes-agent/hermes_state_errors.pyú	<genexpr>z(is_malformed_db_error.<locals>.<genexpr>   ó&   øè ø€ ò :Ø'-ˆ”#�c“(—.‘.Ó"Ô"ñ:ùó   ƒ%()Ú
isinstanceÚsqlite3ÚDatabaseErrorÚanyÚ_MALFORMED_DB_MARKERS©r   s   `r   Úis_malformed_db_errorr      s3   ø€ ô �cœ7×0Ñ0Ó1ò ´có :Ü1Fô:ó 7ð ó    zdisk i/o errorzdatabase is lockedzdatabase table is lockedÚbusyc                 ó8   — dt        | «      j                  «       v S )zŽTransient engine error on contended WAL appends (retries like locked/busy);
    message-scoped because some builds raise it as InterfaceError.zno more rows availabler   r   s    r   Ú_is_no_more_rowsr   %   s   € ð $¤s¨3£x§~¡~Ó'7Ð7Ð7r   c                 ól   ‡ — t        ‰ t        j                  «      xr t        ˆ fd„t        D «       «      S )zm"Busy right now", not "damaged": one predicate so retry and the HTTP
    503-vs-500 split cannot drift apart.c              3   óT   •K  — | ]  }|t        ‰«      j                  «       v –— Œ! y ­wr   r   r   s     €r   r   z,is_transient_sqlite_error.<locals>.<genexpr>.   s&   øè ø€ ò =Ø'-ˆ”#�c“(—.‘.Ó"Ô"ñ=ùr   )r   r   ÚOperationalErrorr   Ú_TRANSIENT_SQLITE_MARKERSr   s   `r   Úis_transient_sqlite_errorr!   +   s3   ø€ ô �cœ7×3Ñ3Ó4ò ¼ó =Ü1Jô=ó :ð r   c                 ól   ‡ — t        ‰ t        j                  «      xr t        ˆ fd„t        D «       «      S )z—Only SQLite's explicit malformed-schema text: a generic "disk image is
    malformed" may be any B-tree page, so runtime repair must fail closed on it.c              3   óT   •K  — | ]  }|t        ‰«      j                  «       v –— Œ! y ­wr   r   r   s     €r   r   z,is_malformed_schema_error.<locals>.<genexpr>6   r   r   )r   r   r   r   Ú_MALFORMED_SCHEMA_MARKERSr   s   `r   Úis_malformed_schema_errorr%   3   s3   ø€ ô �cœ7×0Ñ0Ó1ò ´có :Ü1Jô:ó 7ð r   )zno space left on deviceznot enough spacezdatabase or disk is fullz	disk fullz	full diskÚenospcc                 óî   ‡— | €yt        | t        «      rt        | dd«      t        j                  k(  ryt        | t
        «      r| n
t        | «      j                  «       Št        ˆfd„t        D «       «      S )zFDisk-full / ENOSPC: OSError(ENOSPC), SQLITE_FULL, or matching strings.NFÚerrnoTc              3   ó&   •K  — | ]  }|‰v –— Œ
 y ­wr   © )r   r   Úlowereds     €r   r   z%is_disk_full_error.<locals>.<genexpr>I   s   øè ø€ ÒB Vˆv˜Ô ÑBùó   ƒ)	r   ÚOSErrorÚgetattrr(   ÚENOSPCr	   r
   r   Ú_DISK_FULL_MARKERS)r   r+   s    @r   Úis_disk_full_errorr1   B   s[   ø€ à
€{ØÜ�#”wÔ¤G¨C°¸$Ó$?Ä5Ç<Á<Ò$OØÜ  ¤cÔ*‰s´°C³×?Ñ?ÓA€GÜÓBÔ/AÔBÓBÐBr   )
ÚlockedÚcompressionÚcompression_closedÚ
turn_leaseÚcorruptÚ	fts_indexÚreplacedÚdeleted_walÚdiskÚunknown)Ú	malformedzfile is not a databaseznot a databasezdatabase corruptionÚSQLITE_CORRUPT_VTABi  z\bmessages_fts\w*c                 ó  ‡— | €yt        | dd«      }|�	|t        k(  S t        | t        «      r| n
t        | «      j	                  «       Št
        j                  ‰«      sy‰j                  d«      rd‰v ryt        ˆfd„t        D «       «      S )a  Corruption SQLite itself attributes to the FTS index layer: the ONE provenance rule
    shared by the write-repair gate (``SessionDB._is_fts_write_corruption_error``), the
    gateway transcript retry and :func:`classify_persistence_error` (#96038, #97794).

    A known result code outranks prose: ``SQLITE_CORRUPT_VTAB`` is FTS-scoped even with
    the generic malformed-image text older SQLite builds emit, while bare ``SQLITE_CORRUPT``
    / ``SQLITE_NOTADB`` carry no object scope and any other known code contradicts
    FTS-looking prose, so both fail closed. Only without a code (Python < 3.11, RPC-wrapped
    strings) does the text decide, and then only an ``fts5:`` corruption report or a
    corruption marker that names a ``messages_fts*`` object counts.
    NFÚsqlite_errorcodezfts5:r6   Tc              3   ó&   •K  — | ]  }|‰v –— Œ
 y ­wr   r*   ©r   r   Útexts     €r   r   z1is_fts_scoped_corruption_error.<locals>.<genexpr>z   s   øè ø€ ÒC &ˆv˜Œ~ÑCùr,   )
r.   r=   r   r	   r
   Ú_FTS_OBJECT_REÚsearchÚ
startswithr   Ú_DB_CORRUPTION_MARKERS)Ú
exc_or_strÚcoderB   s     @r   Úis_fts_scoped_corruption_errorrI   d   s…   ø€ ð ÐØÜ�:Ð1°4Ó8€DØÐØÔ*Ñ*Ð*Ü$ Z´Ô5‰J¼3¸z»?×QÑQÓS€DÜ× Ñ  Ô&ØØ‡��wÔ I°Ñ$5ØÜÓCÔ,BÔCÓCÐCr   c                   ó(   ‡ — e Zd ZdZdefˆ fd„Zˆ xZS )ÚCompressionSessionClosedErrorz@A durable write targeted a parent already closed by compression.Ú
session_idc                 ó:   •— || _         t        ‰| �	  d|›d�«       y )NzSession zP is closed by compression; adopt its live continuation before appending messages)rL   ÚsuperÚ__init__)ÚselfrL   Ú	__class__s     €r   rO   z&CompressionSessionClosedError.__init__€   s*   ø€ Ø$ˆŒÜ‰ÑØ�z�nð %Dð Dõ	
r   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r	   rO   Ú__classcell__)rQ   s   @r   rK   rK   }   s   ø„ ÙJð
 3÷ 
ñ 
r   rK   c                   ó   — e Zd ZdZy)ÚCompressionSessionBusyErrorz>A non-owner tried to write while compression owns the session.N©rR   rS   rT   rU   r*   r   r   rX   rX   ˆ   s   „ ÚHr   rX   c                   ó   — e Zd ZdZy)Ú!SessionCompressionInProgressErroru+  A concurrent writer collided with a *live* compression lock â€” transient
    (the compressor publishes in seconds; ``_execute_write`` waits), unlike the
    parent class's other case (a compressor whose own lease is gone: permanent,
    fail fast). Subclassing keeps every existing handler working.NrY   r*   r   r   r[   r[   Œ   s   „ òEr   r[   c                   ó   — e Zd ZdZy)ÚSessionTurnLeaseLostErrorzíA transcript write presented a turn-lease holder that no longer owns it.
    Fail-fast fencing (no ``_execute_write`` retry): a later writer may already
    be persisting a newer turn, and landing this one would interleave a stale reply.NrY   r*   r   r   r]   r]   “   s   „ òXr   r]   c                   ó   — e Zd ZdZy)ÚStateDbReplacedErrorzËThe state.db path no longer names the file this SessionDB opened
    (out-of-band cp/mv/restore). In-place FTS repair and fail-open trigger
    dropping cannot fix a generation mismatch; they amplify it.NrY   r*   r   r   r_   r_   ™   s   „ òCr   r_   c                   ó   — e Zd ZdZy)ÚDeletedWalGenerationErroras  A live process holds a deleted state.db-wal / -shm generation. Opening or
    writing through this handle would mint a second WAL inode (split-brain ->
    intermittent SQLITE_CORRUPT / IOERR). Stop the writers; never unlink the WAL
    yourself. Subclasses StateDbReplacedError so every consumer that diverts
    transcripts on a replaced store handles this identically.NrY   r*   r   r   ra   ra   Ÿ   s   „ òAr   ra   éD   Údb_file_generationzàFATAL: state.db was replaced underneath the gateway; refusing further writes to this file. Divert transcripts to sessions/<id>.jsonl (and the gateway pending_messages spool) and restore or reopen after operator intervention.zNhttps://hermes-agent.nousresearch.com/docs/user-guide/session-storage-recoveryaÉ  FATAL: session storage stopped writing because another Hermes process still holds a deleted state.db-wal or state.db-shm inode (an old copy of the write-ahead log). Nothing is lost: quit every Hermes process on this profile (Desktop app, gateway, dashboard, cron), run `hermes doctor` (it names the processes still holding the log), then start Hermes again. Do not delete the WAL yourself and do not run `hermes doctor --fix` while they are running. Guide: zð Detail: the path names a different (or missing) generation than the one this process holds open; opening or writing through it would mint a second WAL (split-brain). database.journal_mode: delete is operator containment, not a new default.c                   ó   — e Zd ZdZy)ÚStateDbCorruptErroru–  A live SessionDB observed structural (non-FTS, non-replaced) corruption and
    is quarantined: sticky for the handle's life â€” writes fail fast, no reopen,
    no close-time checkpoint (a handle that kept writing after the first error
    checkpointed 15 pages under wrong page numbers and turned a readable file
    into "file is not a database"; SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE on 3.12+
    also stops SQLite's own). Subclasses sqlite3.DatabaseError so every degrade
    path keeps working. Recovery boundary: restart on a repaired/restored file.

    Stopping the writes is what prevents that; skipping the explicit checkpoint is the second line of
    defence. SQLite still runs its own last-connection checkpoint inside ``close()`` (and deletes the
    ``-wal`` sidecar) unless ``SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE`` is set â€” Python exposes it via
    ``Connection.setconfig()`` on 3.12+, so quarantine disables the close-time checkpoint there and the WAL
    survives on disk for forensics; on 3.11 the internal checkpoint is unavoidable (post-quarantine it can
    only carry pre-corruption committed frames, since no further writes are accepted). See #90837.
    NrY   r*   r   r   re   re   Â   s   „ òr   re   a¯  FATAL: state.db reported structural corruption (database disk image is malformed outside the FTS shadow tables) on a live handle; refusing further writes, automatic reopen, and the close-time WAL checkpoint on this file. Stop the gateway, then run `hermes sessions recover --source <state.db> --inspect-only` or restore a snapshot. Unwritten transcripts are diverted to sessions/<id>.jsonl (and the gateway pending_messages spool).r5   r4   r3   r9   r8   r6   ))z
turn leaser5   ))zclosed by compressionr4   ))zbeing compressedzcompression leaser3   ))zdeleted state.db-walzdeleted state.db-shmr9   ))zwas replaced underneathr8   ))r2   r   r2   c                 ó  ‡— | €yt         D ]  \  }}t        | |«      sŒ|c S  t        | «      ryt        | «      j	                  «       Št
        D ]  \  }}t        ˆfd„|D «       «      sŒ|c S  t        | «      st        ˆfd„dD «       «      ryy)a:  Coarse cause bucket (PERSISTENCE_ERROR_CAUSES) so the user's guidance
    matches: "locked" = busy, retry; "disk" = full/read-only/permissions;
    "compression" = a live lease refused the write; "compression_closed" = adopt
    the rotated session id; "turn_lease" = fencing, not storage; "corrupt" =
    file damage (repair path, not disk space); "fts_index" = SQLite scoped the
    corruption to the FTS index (the transcript store is not damaged); "replaced" =
    main-file replacement; "deleted_wal" = a retired sidecar generation requiring
    capture inspection.r;   r7   c              3   ó&   •K  — | ]  }|‰v –— Œ
 y ­wr   r*   rA   s     €r   r   z-classify_persistence_error.<locals>.<genexpr>  s   øè ø€ Ò4 &ˆv˜Œ~Ñ4ùr,   c              3   ó&   •K  — | ]  }|‰v –— Œ
 y ­wr   r*   )r   ÚmrB   s     €r   r   z-classify_persistence_error.<locals>.<genexpr>  s   øè ø€ Ò,b¸1¨Q°$¬YÑ,bùr,   )r:   Úreadonlyz	read-onlyr:   )Ú_PERSISTENCE_CAUSE_BY_TYPEr   rI   r	   r
   Ú_PERSISTENCE_CAUSE_BY_PHRASEr   r1   )rG   Úexc_typeÚcauseÚmarkersrB   s       @r   Úclassify_persistence_errorrp   ô   s•   ø€ ð ÐØô
 6ò ‰ˆ�%Ü�j (Õ+ØŠLðô
 & jÔ1ØÜˆz‹?× Ñ Ó"€DÜ6ò ‰ˆ�ÜÓ4¨GÔ4Õ4ØŠLðô ˜*Ô%¬Ó,bÐ@aÔ,bÔ)bØØr   )+rU   r(   Úrer   r$   r   ÚBaseExceptionÚboolr   Ú_DISK_IO_ERROR_MARKERr    ÚErrorr   r!   r%   r0   r	   r1   ÚPERSISTENCE_ERROR_CAUSESrF   r.   r=   ÚcompilerC   rI   ÚRuntimeErrorrK   rX   r[   r]   r_   ra   Ú_STATE_DB_APPLICATION_ID_OFFSETÚ_STATE_DB_GENERATION_KEYÚ_STATE_DB_REPLACED_MSGÚSTORAGE_RECOVERY_DOCS_URLÚ_DELETED_WAL_GENERATION_MSGr   re   Ú_STATE_DB_CORRUPT_MSGrk   rl   rp   r*   r   r   ú<module>r      s%  ðñ/ó Û 	Û ð ;Ð ØXÐ3ÐXÐ5WÑXÐ ð˜}ð °ó ð )Ð ð
 Ð/Ð1KÈVðÐ ð
8˜'Ÿ-™-ð 8¨Dó 8ð =ð °Tó ð =ð °Tó ðÐ ðC˜M¨CÑ/°$Ñ6ð C¸4ó CðÐ ðÐ ñ
 ˜gÐ'<¸cÓBÐ ð �—‘Ð0Ó1€ðD°$ó Dô2
 Lô 
ôI ,ô IôEÐ(Cô EôX ô XôC˜<ô CôAÐ 4ô Að #%Ð Ø/Ð ðYð ð
 mÐ ðð
 (Ð(ð )PðPð ô˜'×/Ñ/ô ð&Dð ð  Ð-Ø"Ð$8Ð9Ø  -Ð0ð  Ð.Ø˜:Ð&Ø˜)Ð$ð	Ð ð $Ø6Ø>àEØ.Ø˜YÐ'Ø"ð	 Ð ð¨cô r   