@gajae-code/coding-agent 0.15.2 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/dist/types/config/autorouting-tier-map.d.ts +0 -9
  3. package/dist/types/config/file-lock.d.ts +7 -1
  4. package/dist/types/config/keybindings.d.ts +13 -3
  5. package/dist/types/config/model-registry.d.ts +5 -0
  6. package/dist/types/config/model-resolver.d.ts +1 -0
  7. package/dist/types/config/settings-schema.d.ts +13 -1
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
  10. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/modes/action-registry.d.ts +1 -0
  13. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  14. package/dist/types/modes/components/welcome.d.ts +2 -0
  15. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  16. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  17. package/dist/types/modes/interactive-mode.d.ts +7 -0
  18. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  19. package/dist/types/modes/types.d.ts +3 -0
  20. package/dist/types/modes/ui-language.d.ts +2 -1
  21. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  22. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  23. package/dist/types/sdk/bus/index.d.ts +4 -1
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  26. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  27. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  28. package/dist/types/sdk/session.d.ts +5 -0
  29. package/dist/types/session/agent-session.d.ts +4 -0
  30. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  31. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  32. package/dist/types/tools/ask.d.ts +8 -0
  33. package/dist/types/utils/clipboard.d.ts +9 -1
  34. package/package.json +7 -7
  35. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  36. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  37. package/src/commands/sdk.ts +2 -2
  38. package/src/config/autorouting-tier-map.ts +0 -9
  39. package/src/config/file-lock.ts +15 -1
  40. package/src/config/keybindings.ts +13 -3
  41. package/src/config/model-registry.ts +132 -21
  42. package/src/config/model-resolver.ts +2 -1
  43. package/src/config/models-config-schema.ts +2 -2
  44. package/src/config/settings-schema.ts +13 -1
  45. package/src/coordinator/contract.ts +1 -0
  46. package/src/coordinator-mcp/question-state.ts +235 -4
  47. package/src/coordinator-mcp/server.ts +402 -2
  48. package/src/dap/client.ts +19 -8
  49. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  50. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  51. package/src/gjc-runtime/session-state-lock.ts +11 -2
  52. package/src/internal-urls/docs-index.generated.ts +8 -7
  53. package/src/lsp/client.ts +39 -4
  54. package/src/main.ts +42 -11
  55. package/src/modes/action-registry.ts +7 -0
  56. package/src/modes/components/iterm-pet-transport.ts +4 -2
  57. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  58. package/src/modes/components/settings-selector.ts +1 -0
  59. package/src/modes/components/tool-status-header.ts +149 -41
  60. package/src/modes/components/welcome.ts +8 -0
  61. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  62. package/src/modes/controllers/input-controller.ts +141 -7
  63. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  64. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  65. package/src/modes/controllers/selector-controller.ts +11 -2
  66. package/src/modes/interactive-mode.ts +88 -1
  67. package/src/modes/shared/oauth-url-copy.ts +33 -0
  68. package/src/modes/types.ts +3 -0
  69. package/src/modes/ui-language.ts +44 -3
  70. package/src/runtime-mcp/oauth-flow.ts +19 -8
  71. package/src/runtime-mcp/transports/stdio.ts +27 -13
  72. package/src/sdk/acp/adapter.ts +29 -2
  73. package/src/sdk/broker/broker.ts +26 -6
  74. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  75. package/src/sdk/broker/lifecycle.ts +664 -5
  76. package/src/sdk/broker/session-index.ts +246 -0
  77. package/src/sdk/broker/transport.ts +1 -0
  78. package/src/sdk/bus/index.ts +135 -38
  79. package/src/sdk/bus/natives-augment.d.ts +20 -1
  80. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  81. package/src/sdk/bus/telegram-daemon.ts +3 -0
  82. package/src/sdk/cli/session-cli.ts +34 -7
  83. package/src/sdk/lifecycle/service.ts +166 -11
  84. package/src/sdk/mcp/server.ts +2 -1
  85. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  86. package/src/sdk/protocol/operation-registry.ts +2 -1
  87. package/src/sdk/session.ts +91 -7
  88. package/src/session/agent-session.ts +403 -56
  89. package/src/session/fallback-chain-controller.ts +17 -0
  90. package/src/setup/frictionless-onboarding.ts +4 -4
  91. package/src/slash-commands/aside-command.test.ts +366 -0
  92. package/src/slash-commands/builtin-registry.ts +19 -0
  93. package/src/slash-commands/helpers/aside.ts +299 -0
  94. package/src/tools/ask.ts +8 -0
  95. package/src/utils/clipboard.ts +37 -28
@@ -1677,8 +1677,12 @@ function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
1677
1677
  "lifecycleDeleteMetadata",
1678
1678
  "lifecycleParentIdentity",
1679
1679
  "lifecycleFiles",
1680
+ "uncertainRetirement",
1680
1681
  ]);
1681
1682
  const parentIdentity = record.lifecycleParentIdentity as Record<string, unknown> | undefined;
1683
+ const filesValid = Array.isArray(record.lifecycleFiles) && record.lifecycleFiles.every(isLifecycleCleanupFile);
1684
+ const receiptValid =
1685
+ record.uncertainRetirement === undefined || isLifecycleRetirementReceipt(record.uncertainRetirement);
1682
1686
  return (
1683
1687
  Object.keys(record).every(key => allowed.has(key)) &&
1684
1688
  record.phase === "lifecycle" &&
@@ -1692,18 +1696,23 @@ function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
1692
1696
  typeof parentIdentity.ino === "string" &&
1693
1697
  /^\d+$/.test(parentIdentity.ino) &&
1694
1698
  Array.isArray(record.lifecycleFiles) &&
1695
- record.lifecycleFiles.length > 0 &&
1699
+ (record.lifecycleFiles.length > 0 || isLifecycleRetirementCleanup(cleanup)) &&
1696
1700
  record.lifecycleFiles.length <= 4 &&
1697
1701
  (record.lifecycleDeleteMetadata === undefined || record.lifecycleDeleteMetadata === true) &&
1698
- record.lifecycleFiles.every(isLifecycleCleanupFile)
1702
+ receiptValid &&
1703
+ filesValid
1699
1704
  );
1700
1705
  }
1701
1706
 
1702
1707
  function validateLifecycleCleanupShape(cleanup: CleanupEvidence): BrokerResponse | undefined {
1708
+ const retirement = isLifecycleRetirementCleanup(cleanup) ? cleanup.uncertainRetirement : undefined;
1703
1709
  if (
1704
1710
  !isLifecycleCleanupEvidence(cleanup) ||
1705
1711
  lifecycleCleanupHasMixedMetadataSchema(cleanup) ||
1706
- (cleanup.lifecycleDeleteMetadata === true && cleanup.lifecycleFiles!.length > 2)
1712
+ (cleanup.lifecycleDeleteMetadata === true && cleanup.lifecycleFiles!.length > 2) ||
1713
+ (retirement !== undefined && cleanup.lifecycleDeleteMetadata === true) ||
1714
+ (retirement !== undefined && cleanup.sessionId !== retirement.identity.sessionId) ||
1715
+ (retirement !== undefined && path.resolve(cleanup.metadataRoot!) !== path.resolve(retirement.identity.stateRoot))
1707
1716
  )
1708
1717
  return fail("terminal_uncertain", "Lifecycle cleanup replay lacks a complete unambiguous schema.");
1709
1718
  const files = cleanup.lifecycleFiles!;
@@ -1737,6 +1746,522 @@ function validateLifecycleCleanupShape(cleanup: CleanupEvidence): BrokerResponse
1737
1746
  return undefined;
1738
1747
  }
1739
1748
 
1749
+ function retirementCleanupFromResponse(value: unknown): LifecycleRetirementCleanup | undefined {
1750
+ if (typeof value !== "object" || value === null || (value as { ok?: unknown }).ok !== false) return undefined;
1751
+ const cleanup = (value as { error?: { cleanup?: unknown } }).error?.cleanup;
1752
+ return isLifecycleRetirementCleanup(cleanup) ? cleanup : undefined;
1753
+ }
1754
+
1755
+ function exactLifecycleRootIdentity(root: string): { dev: string; ino: string } | undefined {
1756
+ try {
1757
+ const rootStat = fsSync.lstatSync(root, { bigint: true });
1758
+ const sdk = fsSync.lstatSync(path.join(root, "sdk"), { bigint: true });
1759
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || !sdk.isDirectory() || sdk.isSymbolicLink())
1760
+ return undefined;
1761
+ return { dev: sdk.dev.toString(), ino: sdk.ino.toString() };
1762
+ } catch {
1763
+ return undefined;
1764
+ }
1765
+ }
1766
+
1767
+ /** Only ENOENT under the exact, non-symlinked sdk parent proves endpoint absence. */
1768
+ function exactLifecycleEndpointAbsent(root: string, id: string): boolean {
1769
+ if (!exactLifecycleRootIdentity(root)) return false;
1770
+ try {
1771
+ fsSync.lstatSync(path.join(root, "sdk", `${id}.json`));
1772
+ return false;
1773
+ } catch (error) {
1774
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
1775
+ }
1776
+ }
1777
+
1778
+ function sameRetirementIdentityAsRecord(identity: LifecycleRetirementIdentity, record: IndexedSession): boolean {
1779
+ return (
1780
+ identity.sessionId === record.sessionId &&
1781
+ path.resolve(identity.stateRoot) === path.resolve(record.locator.stateRoot) &&
1782
+ identity.endpointGeneration === record.endpointGeneration &&
1783
+ identity.endpointMtimeMs === record.endpointMtimeMs &&
1784
+ identity.pid === record.pid &&
1785
+ identity.processIncarnation === record.processIncarnation &&
1786
+ identity.hostIncarnation === record.hostIncarnation &&
1787
+ identity.lifecycleRequestId === record.lifecycleRequestId
1788
+ );
1789
+ }
1790
+
1791
+ function findRetirementRecord(
1792
+ broker: Broker,
1793
+ id: string,
1794
+ receipt: LifecycleRetirementReceipt | undefined,
1795
+ ): IndexedSession | undefined {
1796
+ if (receipt !== undefined) {
1797
+ const historical = broker.index.findHistoricalSessionIdentity(receipt.identity);
1798
+ if (historical) return historical;
1799
+ // A later registration may make a same-ID successor the public authority.
1800
+ // Receipt replay must search the retained identity set first so the
1801
+ // successor can never shadow the exact staged retirement identity.
1802
+ const staged = broker.index
1803
+ .listSessionIdentities()
1804
+ .find(session => session.sessionId === id && sameRetirementIdentityAsRecord(receipt.identity, session));
1805
+ if (staged) return staged;
1806
+ }
1807
+ const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
1808
+ return current;
1809
+ }
1810
+
1811
+ function retirementMarkerCapture(
1812
+ root: string,
1813
+ id: string,
1814
+ pathName: string,
1815
+ expected: EffectMarker,
1816
+ ): LifecycleFileCapture | undefined {
1817
+ const capture = captureLifecycleFile(pathName, true, true);
1818
+ if (!capture) return undefined;
1819
+ let parsed: unknown;
1820
+ try {
1821
+ parsed = parseLifecycleJson(capture.bytes);
1822
+ } catch {
1823
+ throw new Error("Lifecycle retirement marker is malformed.");
1824
+ }
1825
+ if (
1826
+ !isExactEffectMarker(parsed) ||
1827
+ !sameEffectMarker(parsed, expected) ||
1828
+ canonicalJson(parsed) !== decodeLifecycleUtf8(capture.bytes) ||
1829
+ path.dirname(path.resolve(pathName)) !== path.join(path.resolve(root), "sdk") ||
1830
+ path.basename(pathName) !==
1831
+ path.basename(
1832
+ pathName === lifecycleMarkerPath(root, id) ? lifecycleMarkerPath(root, id) : lifecycleReadyPath(root, id),
1833
+ )
1834
+ )
1835
+ throw new Error("Lifecycle retirement marker identity is incomplete.");
1836
+ return capture;
1837
+ }
1838
+
1839
+ function retirementCleanupPlan(
1840
+ root: string,
1841
+ id: string,
1842
+ create: { identity: string; effectMarker?: string },
1843
+ identity: LifecycleRetirementIdentity,
1844
+ ): LifecycleRetirementCleanup | BrokerResponse {
1845
+ const parentIdentity = exactLifecycleRootIdentity(root);
1846
+ if (!parentIdentity) return fail("terminal_uncertain", "Lifecycle metadata root or parent identity is unavailable.");
1847
+ if (!create.effectMarker || create.effectMarker !== identity.lifecycleRequestId)
1848
+ return fail("terminal_uncertain", "Create ledger identity lacks the indexed lifecycle request marker.");
1849
+ const expected: EffectMarker = {
1850
+ pid: identity.pid,
1851
+ effectMarker: identity.lifecycleRequestId,
1852
+ incarnation: identity.processIncarnation,
1853
+ };
1854
+ const markerPath = lifecycleMarkerPath(root, id);
1855
+ const readyPath = lifecycleReadyPath(root, id);
1856
+ let marker: LifecycleFileCapture | undefined;
1857
+ let ready: LifecycleFileCapture | undefined;
1858
+ try {
1859
+ marker = retirementMarkerCapture(root, id, markerPath, expected);
1860
+ ready = retirementMarkerCapture(root, id, readyPath, expected);
1861
+ } catch {
1862
+ return fail("terminal_uncertain", "Lifecycle marker or readiness evidence is malformed or replaced.");
1863
+ }
1864
+ // A readiness sibling without its canonical marker is never an authority. A
1865
+ // fresh retirement must also retain both captures so the owner chain is
1866
+ // complete; the staged replay receipt is the only path that may proceed with
1867
+ // both siblings already detached.
1868
+ if (!marker || !ready) return fail("terminal_uncertain", "Lifecycle marker and readiness evidence are incomplete.");
1869
+ const directory = path.join(root, "sdk");
1870
+ const files: LifecycleCleanupFile[] = [
1871
+ {
1872
+ path: markerPath,
1873
+ identity: serializeCleanupIdentity({ ...marker.identity, size: Number(marker.identity.size) }),
1874
+ attempt: 1,
1875
+ plannedPath: path.join(directory, `.gjc-delete-${randomUUID()}-${path.basename(markerPath)}`),
1876
+ },
1877
+ {
1878
+ path: readyPath,
1879
+ identity: serializeCleanupIdentity({ ...ready.identity, size: Number(ready.identity.size) }),
1880
+ attempt: 1,
1881
+ plannedPath: path.join(directory, `.gjc-delete-${randomUUID()}-${path.basename(readyPath)}`),
1882
+ },
1883
+ ];
1884
+ return {
1885
+ phase: "lifecycle",
1886
+ sessionId: id,
1887
+ metadataRoot: root,
1888
+ lifecycleParentIdentity: parentIdentity,
1889
+ lifecycleFiles: files,
1890
+ uncertainRetirement: {
1891
+ version: 1,
1892
+ stage: "cleanup",
1893
+ identity,
1894
+ },
1895
+ };
1896
+ }
1897
+
1898
+ function retirementCleanupSettled(cleanup: LifecycleRetirementCleanup): boolean {
1899
+ const expectedParent = cleanup.lifecycleParentIdentity;
1900
+ if (!expectedParent || !cleanup.metadataRoot) return false;
1901
+ const parent = lifecycleParentIdentity(path.join(cleanup.metadataRoot, "sdk"));
1902
+ if (!parent || parent.dev !== expectedParent.dev || parent.ino !== expectedParent.ino) return false;
1903
+ for (const file of cleanup.lifecycleFiles ?? []) {
1904
+ try {
1905
+ fsSync.lstatSync(file.path);
1906
+ return false;
1907
+ } catch (error) {
1908
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false;
1909
+ }
1910
+ for (const candidate of [file.detachedPath, file.plannedPath]) {
1911
+ if (!candidate) continue;
1912
+ try {
1913
+ const stat = fsSync.lstatSync(candidate, { bigint: true });
1914
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== 0n) return false;
1915
+ } catch (error) {
1916
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false;
1917
+ }
1918
+ }
1919
+ }
1920
+ return true;
1921
+ }
1922
+
1923
+ function retirementCleanupParentMatches(cleanup: LifecycleRetirementCleanup): boolean {
1924
+ const expectedParent = cleanup.lifecycleParentIdentity;
1925
+ if (!expectedParent || !cleanup.metadataRoot) return false;
1926
+ const parent = lifecycleParentIdentity(path.join(cleanup.metadataRoot, "sdk"));
1927
+ return parent !== undefined && parent.dev === expectedParent.dev && parent.ino === expectedParent.ino;
1928
+ }
1929
+
1930
+ function retirementIdentityFromInput(
1931
+ input: Input,
1932
+ record: IndexedSession,
1933
+ createIdentity: string,
1934
+ ): LifecycleRetirementIdentity | BrokerResponse {
1935
+ const stateRoot = text(input.stateRoot);
1936
+ const lifecycleRequestId = text(input.lifecycleRequestId);
1937
+ const processIdentity = text(input.processIncarnation);
1938
+ const hostIdentity = text(input.hostIncarnation);
1939
+ const remoteCreateKey = text(input.remoteCreateKey);
1940
+ const endpointGeneration = input.endpointGeneration;
1941
+ const endpointMtimeMs = input.endpointMtimeMs;
1942
+ if (
1943
+ !stateRoot ||
1944
+ !path.isAbsolute(stateRoot) ||
1945
+ !lifecycleRequestId ||
1946
+ !processIdentity ||
1947
+ !hostIdentity ||
1948
+ !remoteCreateKey ||
1949
+ typeof endpointGeneration !== "number" ||
1950
+ !Number.isSafeInteger(endpointGeneration) ||
1951
+ endpointGeneration <= 0 ||
1952
+ typeof endpointMtimeMs !== "number" ||
1953
+ !Number.isFinite(endpointMtimeMs) ||
1954
+ endpointMtimeMs <= 0 ||
1955
+ !boundedRetirementString(lifecycleRequestId, MAX_EFFECT_MARKER_LENGTH) ||
1956
+ !/^[A-Za-z0-9._-]+$/.test(lifecycleRequestId) ||
1957
+ !boundedRetirementString(processIdentity, MAX_PROCESS_INCARNATION_LENGTH) ||
1958
+ !boundedRetirementString(hostIdentity, MAX_PROCESS_INCARNATION_LENGTH)
1959
+ )
1960
+ return fail("invalid_input", "Retirement requires the complete indexed identity proof.");
1961
+ const identity: LifecycleRetirementIdentity = {
1962
+ sessionId: record.sessionId,
1963
+ stateRoot,
1964
+ endpointGeneration,
1965
+ endpointMtimeMs,
1966
+ pid: record.pid,
1967
+ processIncarnation: processIdentity,
1968
+ hostIncarnation: hostIdentity,
1969
+ lifecycleRequestId,
1970
+ createIdentity,
1971
+ remoteCreateKey,
1972
+ };
1973
+ if (!isLifecycleRetirementIdentity(identity))
1974
+ return fail("invalid_input", "Retirement identity is malformed or exceeds the bounded proof schema.");
1975
+ if (
1976
+ path.resolve(record.locator.stateRoot) !== path.resolve(stateRoot) ||
1977
+ record.endpointGeneration !== endpointGeneration ||
1978
+ record.endpointMtimeMs !== endpointMtimeMs ||
1979
+ record.lifecycleRequestId !== lifecycleRequestId ||
1980
+ (record.processIncarnation ?? record.hostIncarnation) !== processIdentity ||
1981
+ (record.hostIncarnation ?? record.processIncarnation) !== hostIdentity
1982
+ )
1983
+ return fail(
1984
+ "retirement_proof_stale",
1985
+ "Retirement proof does not match the indexed session authority before effect start.",
1986
+ );
1987
+ return identity;
1988
+ }
1989
+
1990
+ function retirementProof(identity: LifecycleRetirementIdentity, indexSeq?: number): BrokerResponse {
1991
+ return {
1992
+ ok: true,
1993
+ result: {
1994
+ sessionId: identity.sessionId,
1995
+ retired: true,
1996
+ ledgerState: "terminal_error",
1997
+ indexType: "session_closed",
1998
+ stateRoot: identity.stateRoot,
1999
+ endpointGeneration: identity.endpointGeneration,
2000
+ endpointMtimeMs: identity.endpointMtimeMs,
2001
+ processIncarnation: identity.processIncarnation,
2002
+ hostIncarnation: identity.hostIncarnation,
2003
+ lifecycleRequestId: identity.lifecycleRequestId,
2004
+ remoteCreateKey: identity.remoteCreateKey,
2005
+ ...(indexSeq === undefined ? {} : { indexSeq }),
2006
+ },
2007
+ };
2008
+ }
2009
+
2010
+ function isLifecycleBrokerResponse(value: unknown): value is BrokerResponse {
2011
+ return (
2012
+ typeof value === "object" &&
2013
+ value !== null &&
2014
+ "ok" in value &&
2015
+ typeof (value as { ok?: unknown }).ok === "boolean"
2016
+ );
2017
+ }
2018
+
2019
+ async function executeUncertainRetirement(
2020
+ broker: Broker,
2021
+ input: Input,
2022
+ identity: string,
2023
+ cleanup?: LifecycleRetirementCleanup,
2024
+ ): Promise<BrokerResponse> {
2025
+ const id = sessionId(input);
2026
+ if (!id) return fail("invalid_input", "sessionId is required.");
2027
+ if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
2028
+ await broker.index.refresh();
2029
+ let receipt = cleanup?.uncertainRetirement;
2030
+ let record = findRetirementRecord(broker, id, receipt);
2031
+ if (!record) return fail("not_found", "session is not indexed");
2032
+ let retirementIdentity: LifecycleRetirementIdentity;
2033
+ let create = receipt ? broker.ledger.get(receipt.identity.createIdentity) : undefined;
2034
+ if (receipt) {
2035
+ const suppliedIdentity = retirementIdentityFromInput(input, record, receipt.identity.createIdentity);
2036
+ if (isLifecycleBrokerResponse(suppliedIdentity)) return suppliedIdentity;
2037
+ if (
2038
+ receipt.identity.sessionId !== id ||
2039
+ canonicalJson(suppliedIdentity) !== canonicalJson(receipt.identity) ||
2040
+ !sameRetirementIdentityAsRecord(receipt.identity, record)
2041
+ )
2042
+ return fail(
2043
+ "endpoint_stale",
2044
+ "Staged retirement identity no longer matches the indexed session authority.",
2045
+ cleanup,
2046
+ );
2047
+ retirementIdentity = suppliedIdentity;
2048
+ if (!create) return fail("terminal_uncertain", "Staged retirement create identity is no longer present.");
2049
+ } else {
2050
+ if (record.ambiguous || !isSessionAuthorityEligible(record))
2051
+ return fail("endpoint_stale", "Session authority is ambiguous and cannot be retired safely.");
2052
+ if (record.terminalUncertain !== true)
2053
+ return fail("invalid_input", "session.reconcile_uncertain only accepts terminalUncertain create rows.");
2054
+ const lifecycleRequestId = text(input.lifecycleRequestId);
2055
+ const remoteCreateKey = text(input.remoteCreateKey);
2056
+ if (remoteCreateKey === undefined)
2057
+ return fail("invalid_input", "remoteCreateKey must be a bounded non-empty string.");
2058
+ const matches = broker.ledger.listUncertainCreatesBySessionId(id, lifecycleRequestId, remoteCreateKey);
2059
+ if (matches.length === 0) {
2060
+ const markerCandidates = broker.ledger.listUncertainCreatesBySessionId(id, undefined, remoteCreateKey);
2061
+ if (markerCandidates.length === 1 && markerCandidates[0]!.effectMarker !== lifecycleRequestId)
2062
+ return fail(
2063
+ "retirement_proof_stale",
2064
+ "Retirement lifecycle marker does not match the indexed create identity before effect start.",
2065
+ );
2066
+ return fail("not_found", "No complete terminal_uncertain create identity matches this session.");
2067
+ }
2068
+ if (matches.length !== 1)
2069
+ return fail("terminal_uncertain", "Multiple terminal_uncertain create identities match this session.");
2070
+ create = matches[0]!;
2071
+ const proof = retirementIdentityFromInput(input, record, create.identity);
2072
+ if (isLifecycleBrokerResponse(proof)) return proof;
2073
+ retirementIdentity = proof;
2074
+ if (
2075
+ create.effectMarker !== retirementIdentity.lifecycleRequestId ||
2076
+ create.effectIntent?.sessionId !== id ||
2077
+ path.resolve(create.effectIntent.stateRoot) !== path.resolve(retirementIdentity.stateRoot)
2078
+ )
2079
+ return fail("terminal_uncertain", "Create ledger identity does not match the indexed lifecycle authority.");
2080
+ if (observeProcess(record.pid, retirementIdentity.hostIncarnation) !== "exited")
2081
+ return fail("terminal_uncertain", "Session host exit could not be proven.");
2082
+ if (!exactLifecycleEndpointAbsent(retirementIdentity.stateRoot, id))
2083
+ return fail("terminal_uncertain", "The indexed session endpoint still exists or is unsafe to inspect.");
2084
+ const planned = retirementCleanupPlan(retirementIdentity.stateRoot, id, create, retirementIdentity);
2085
+ if (isLifecycleBrokerResponse(planned)) return planned;
2086
+ cleanup = planned;
2087
+ await broker.ledger.transition(identity, "effect_started", {
2088
+ intendedSessionId: id,
2089
+ effectMarker: retirementIdentity.lifecycleRequestId,
2090
+ response: fail(
2091
+ "cleanup_pending",
2092
+ "Uncertain session retirement is staged for exact lifecycle cleanup.",
2093
+ cleanup,
2094
+ ),
2095
+ });
2096
+ receipt = cleanup.uncertainRetirement;
2097
+ }
2098
+
2099
+ if (!create || !receipt || !cleanup) return fail("terminal_uncertain", "Retirement receipt is incomplete.");
2100
+ if (create.state !== "terminal_uncertain" && !(create.state === "terminal_error" && receipt.stage === "ledger"))
2101
+ return fail("terminal_uncertain", "Create ledger identity is no longer an uncertain retirement candidate.");
2102
+ if (receipt.stage === "cleanup") {
2103
+ if (cleanup.lifecycleFiles?.length) {
2104
+ const cleanupResponse = await reconcileLifecycleCleanup(
2105
+ broker,
2106
+ identity,
2107
+ cleanup,
2108
+ fail("cleanup_pending", "Uncertain session retirement cleanup remains staged.", cleanup),
2109
+ );
2110
+ if (!cleanupResponse.ok && cleanupResponse.error.code !== "cleanup_pending") return cleanupResponse;
2111
+ const persisted =
2112
+ retirementCleanupFromResponse(cleanupResponse) ??
2113
+ retirementCleanupFromResponse(broker.ledger.get(identity)?.response);
2114
+ if (!persisted || !retirementCleanupSettled(persisted)) return cleanupResponse;
2115
+ cleanup = persisted;
2116
+ }
2117
+ if (!exactLifecycleEndpointAbsent(retirementIdentity.stateRoot, id))
2118
+ return fail(
2119
+ "cleanup_pending",
2120
+ "Uncertain session retirement is pending because the endpoint reappeared.",
2121
+ cleanup,
2122
+ );
2123
+ for (const candidate of [
2124
+ lifecycleMarkerPath(retirementIdentity.stateRoot, id),
2125
+ lifecycleReadyPath(retirementIdentity.stateRoot, id),
2126
+ ]) {
2127
+ if (!exactLifecycleEndpointAbsent(retirementIdentity.stateRoot, id))
2128
+ return fail(
2129
+ "cleanup_pending",
2130
+ "Uncertain session retirement is pending because lifecycle authority reappeared.",
2131
+ cleanup,
2132
+ );
2133
+ try {
2134
+ if (fsSync.lstatSync(candidate))
2135
+ return fail(
2136
+ "cleanup_pending",
2137
+ "Uncertain session retirement is pending because lifecycle authority reappeared.",
2138
+ cleanup,
2139
+ );
2140
+ } catch (error) {
2141
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT")
2142
+ return fail(
2143
+ "cleanup_pending",
2144
+ "Uncertain session retirement could not prove lifecycle authority absence.",
2145
+ cleanup,
2146
+ );
2147
+ }
2148
+ }
2149
+ if (!retirementCleanupParentMatches(cleanup))
2150
+ return fail(
2151
+ "cleanup_pending",
2152
+ "Uncertain session retirement is pending because the lifecycle parent identity changed.",
2153
+ cleanup,
2154
+ );
2155
+ const staged: LifecycleRetirementCleanup = {
2156
+ ...cleanup,
2157
+ uncertainRetirement: { ...receipt, stage: "index" },
2158
+ };
2159
+ await broker.ledger.transition(identity, "effect_started", {
2160
+ intendedSessionId: id,
2161
+ response: fail("cleanup_pending", "Uncertain session retirement is staged before index closure.", staged),
2162
+ });
2163
+ cleanup = staged;
2164
+ receipt = staged.uncertainRetirement;
2165
+ }
2166
+ if (!retirementCleanupSettled(cleanup))
2167
+ return fail(
2168
+ "cleanup_pending",
2169
+ "Uncertain session retirement is pending because lifecycle cleanup is not durably settled.",
2170
+ cleanup,
2171
+ );
2172
+
2173
+ await broker.index.refresh();
2174
+ record = findRetirementRecord(broker, id, receipt);
2175
+ if (!record || !sameRetirementIdentityAsRecord(retirementIdentity, record))
2176
+ return fail("endpoint_stale", "Session authority changed before retirement index closure.", cleanup);
2177
+ if (!exactLifecycleEndpointAbsent(retirementIdentity.stateRoot, id))
2178
+ return fail(
2179
+ "cleanup_pending",
2180
+ "Uncertain session retirement is pending because the endpoint reappeared.",
2181
+ cleanup,
2182
+ );
2183
+ if (!retirementCleanupParentMatches(cleanup))
2184
+ return fail(
2185
+ "cleanup_pending",
2186
+ "Uncertain session retirement is pending because the lifecycle parent identity changed.",
2187
+ cleanup,
2188
+ );
2189
+ if (
2190
+ receipt.stage === "ledger" &&
2191
+ (!receipt.indexSeq ||
2192
+ !record.terminal ||
2193
+ record.indexSeq < receipt.indexSeq ||
2194
+ broker.index.findSessionClosedEvidence(record) === undefined)
2195
+ )
2196
+ return fail(
2197
+ "terminal_uncertain",
2198
+ "Uncertain session retirement ledger replay lacks the durable session_closed index proof.",
2199
+ cleanup,
2200
+ );
2201
+ if (receipt.stage === "index") {
2202
+ let indexSeq = receipt.indexSeq;
2203
+ const sessionClosedEvidence = broker.index.findSessionClosedEvidence(record);
2204
+ if (sessionClosedEvidence !== undefined) indexSeq ??= sessionClosedEvidence;
2205
+ else if (broker.index.findSessionTerminalEvidence(record)?.type === "session_deleted")
2206
+ return fail(
2207
+ "terminal_uncertain",
2208
+ "Uncertain session retirement cannot certify session closure after a deletion tombstone.",
2209
+ cleanup,
2210
+ );
2211
+ else {
2212
+ try {
2213
+ await broker.index.append({
2214
+ type: "session_closed",
2215
+ sessionId: id,
2216
+ locator: record.locator,
2217
+ endpointGeneration: record.endpointGeneration,
2218
+ pid: record.pid,
2219
+ ...(record.processIncarnation === undefined ? {} : { processIncarnation: record.processIncarnation }),
2220
+ ...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
2221
+ ...(record.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: record.endpointMtimeMs }),
2222
+ ...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
2223
+ });
2224
+ const verifiedIndexSeq = broker.index.findSessionClosedEvidence(record);
2225
+ if (verifiedIndexSeq === undefined)
2226
+ return fail(
2227
+ "terminal_uncertain",
2228
+ "Uncertain session retirement could not verify the appended session closure against the current index.",
2229
+ cleanup,
2230
+ );
2231
+ indexSeq = verifiedIndexSeq;
2232
+ } catch {
2233
+ return fail("cleanup_pending", "Uncertain session retirement is staged before index closure.", cleanup);
2234
+ }
2235
+ }
2236
+ const staged: LifecycleRetirementCleanup = {
2237
+ ...cleanup,
2238
+ uncertainRetirement: { ...receipt, stage: "ledger", ...(indexSeq === undefined ? {} : { indexSeq }) },
2239
+ };
2240
+ try {
2241
+ await broker.ledger.transition(identity, "effect_started", {
2242
+ intendedSessionId: id,
2243
+ response: fail("cleanup_pending", "Uncertain session retirement is staged before ledger closure.", staged),
2244
+ });
2245
+ } catch {
2246
+ return fail("cleanup_pending", "Uncertain session retirement remains staged after index closure.", staged);
2247
+ }
2248
+ cleanup = staged;
2249
+ receipt = staged.uncertainRetirement;
2250
+ }
2251
+
2252
+ if (create.state !== "terminal_error") {
2253
+ try {
2254
+ await broker.ledger.transition(create.identity, "terminal_error", {
2255
+ intendedSessionId: id,
2256
+ response: fail("terminal_uncertain", "Uncertain create retired after exact identity proof."),
2257
+ });
2258
+ } catch {
2259
+ return fail("cleanup_pending", "Uncertain session retirement remains staged after index closure.", cleanup);
2260
+ }
2261
+ }
2262
+ return retirementProof(retirementIdentity, receipt.indexSeq);
2263
+ }
2264
+
1740
2265
  function validateLifecycleCleanupFile(root: string, id: string, file: LifecycleCleanupFile): boolean {
1741
2266
  const directory = path.join(path.resolve(root), "sdk");
1742
2267
  const original = path.resolve(file.path);
@@ -2187,6 +2712,14 @@ async function reconcileLifecycleCleanup(
2187
2712
  let foundUnauthorized = false;
2188
2713
  for (const candidate of candidates) {
2189
2714
  if (!lifecycleProofWithinDeadline(proofBudget)) return deadlineFailure();
2715
+ if (path.resolve(candidate) !== path.resolve(file.path)) {
2716
+ try {
2717
+ const quarantineStat = fsSync.lstatSync(candidate, { bigint: true });
2718
+ if (quarantineStat.isFile() && quarantineStat.size === 0n) continue;
2719
+ } catch (error) {
2720
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
2721
+ }
2722
+ }
2190
2723
  try {
2191
2724
  const stat = fsSync.lstatSync(candidate);
2192
2725
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -2466,6 +2999,112 @@ type LifecycleFileCapture = {
2466
2999
  digest: string;
2467
3000
  };
2468
3001
 
3002
+ /**
3003
+ * Identity-bound receipt for the three durable retirement boundaries. This is
3004
+ * carried inside the ordinary lifecycle cleanup response so a broker restart
3005
+ * enters the same replay path as every other identity-bound cleanup receipt.
3006
+ * It is deliberately credential-free and bounded: it contains only the
3007
+ * indexed process/session identity and the ledger/index stage.
3008
+ */
3009
+ type LifecycleRetirementIdentity = {
3010
+ sessionId: string;
3011
+ stateRoot: string;
3012
+ endpointGeneration: number;
3013
+ endpointMtimeMs: number;
3014
+ pid: number;
3015
+ processIncarnation: string;
3016
+ hostIncarnation: string;
3017
+ lifecycleRequestId: string;
3018
+ createIdentity: string;
3019
+ remoteCreateKey?: string;
3020
+ };
3021
+
3022
+ type LifecycleRetirementStage = "cleanup" | "index" | "ledger";
3023
+
3024
+ type LifecycleRetirementReceipt = {
3025
+ version: 1;
3026
+ stage: LifecycleRetirementStage;
3027
+ identity: LifecycleRetirementIdentity;
3028
+ indexSeq?: number;
3029
+ };
3030
+
3031
+ type LifecycleRetirementCleanup = CleanupEvidence & {
3032
+ uncertainRetirement: LifecycleRetirementReceipt;
3033
+ };
3034
+
3035
+ function boundedRetirementString(value: unknown, max: number): value is string {
3036
+ return typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
3037
+ }
3038
+
3039
+ function isLifecycleRetirementIdentity(value: unknown): value is LifecycleRetirementIdentity {
3040
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3041
+ const identity = value as Record<string, unknown>;
3042
+ const keys = Object.keys(identity);
3043
+ if (
3044
+ keys.some(
3045
+ key =>
3046
+ !new Set([
3047
+ "sessionId",
3048
+ "stateRoot",
3049
+ "endpointGeneration",
3050
+ "endpointMtimeMs",
3051
+ "pid",
3052
+ "processIncarnation",
3053
+ "hostIncarnation",
3054
+ "lifecycleRequestId",
3055
+ "createIdentity",
3056
+ "remoteCreateKey",
3057
+ ]).has(key),
3058
+ )
3059
+ ) {
3060
+ return false;
3061
+ }
3062
+ const stateRoot = identity.stateRoot;
3063
+ if (typeof stateRoot !== "string" || !boundedRetirementString(stateRoot, 4096) || !path.isAbsolute(stateRoot))
3064
+ return false;
3065
+ return (
3066
+ typeof identity.sessionId === "string" &&
3067
+ isCanonicalSessionId(identity.sessionId) &&
3068
+ typeof identity.endpointGeneration === "number" &&
3069
+ Number.isSafeInteger(identity.endpointGeneration) &&
3070
+ identity.endpointGeneration > 0 &&
3071
+ typeof identity.endpointMtimeMs === "number" &&
3072
+ Number.isFinite(identity.endpointMtimeMs) &&
3073
+ identity.endpointMtimeMs > 0 &&
3074
+ typeof identity.pid === "number" &&
3075
+ Number.isSafeInteger(identity.pid) &&
3076
+ identity.pid > 0 &&
3077
+ boundedRetirementString(identity.processIncarnation, MAX_PROCESS_INCARNATION_LENGTH) &&
3078
+ boundedRetirementString(identity.hostIncarnation, MAX_PROCESS_INCARNATION_LENGTH) &&
3079
+ boundedRetirementString(identity.lifecycleRequestId, MAX_EFFECT_MARKER_LENGTH) &&
3080
+ typeof identity.lifecycleRequestId === "string" &&
3081
+ /^[A-Za-z0-9._-]+$/.test(identity.lifecycleRequestId) &&
3082
+ boundedRetirementString(identity.createIdentity, 256) &&
3083
+ boundedRetirementString(identity.remoteCreateKey, 256)
3084
+ );
3085
+ }
3086
+
3087
+ function isLifecycleRetirementReceipt(value: unknown): value is LifecycleRetirementReceipt {
3088
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3089
+ const receipt = value as Record<string, unknown>;
3090
+ if (
3091
+ Object.keys(receipt).some(key => !new Set(["version", "stage", "identity", "indexSeq"]).has(key)) ||
3092
+ receipt.version !== 1 ||
3093
+ (receipt.stage !== "cleanup" && receipt.stage !== "index" && receipt.stage !== "ledger") ||
3094
+ !isLifecycleRetirementIdentity(receipt.identity)
3095
+ )
3096
+ return false;
3097
+ return (
3098
+ receipt.indexSeq === undefined ||
3099
+ (typeof receipt.indexSeq === "number" && Number.isSafeInteger(receipt.indexSeq) && receipt.indexSeq > 0)
3100
+ );
3101
+ }
3102
+
3103
+ function isLifecycleRetirementCleanup(value: unknown): value is LifecycleRetirementCleanup {
3104
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3105
+ return isLifecycleRetirementReceipt((value as Record<string, unknown>).uncertainRetirement);
3106
+ }
3107
+
2469
3108
  function captureLifecycleFile(file: string, requireRegular = false, bounded = false): LifecycleFileCapture | undefined {
2470
3109
  let descriptor: number | undefined;
2471
3110
  try {
@@ -2656,7 +3295,13 @@ export async function removeOwnedLifecycleArtifactsForTest(
2656
3295
  return removeOwnedLifecycleArtifacts(root, id, expected);
2657
3296
  }
2658
3297
 
2659
- async function recordTerminalUncertain(broker: Broker, id: string, root: string, pid: number): Promise<void> {
3298
+ async function recordTerminalUncertain(
3299
+ broker: Broker,
3300
+ id: string,
3301
+ root: string,
3302
+ pid: number,
3303
+ expected?: EffectMarker,
3304
+ ): Promise<void> {
2660
3305
  await broker.index.refresh();
2661
3306
  const registered = broker.index.listSessions().sessions.find(session => session.sessionId === id);
2662
3307
  if (registered)
@@ -2668,6 +3313,12 @@ async function recordTerminalUncertain(broker: Broker, id: string, root: string,
2668
3313
  pid: registered.pid,
2669
3314
  ...(registered.processIncarnation === undefined ? {} : { processIncarnation: registered.processIncarnation }),
2670
3315
  ...(registered.hostIncarnation === undefined ? {} : { hostIncarnation: registered.hostIncarnation }),
3316
+ ...(registered.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: registered.endpointMtimeMs }),
3317
+ ...(registered.lifecycleRequestId === undefined
3318
+ ? expected?.effectMarker === undefined
3319
+ ? {}
3320
+ : { lifecycleRequestId: expected.effectMarker }
3321
+ : { lifecycleRequestId: registered.lifecycleRequestId }),
2671
3322
  terminalUncertain: true,
2672
3323
  });
2673
3324
  else
@@ -2700,7 +3351,7 @@ async function terminateSpawnedChild(
2700
3351
  const incarnation = expected?.incarnation ?? processIncarnationForBroker(broker, pid);
2701
3352
  const proofBudget: LifecycleProofBudget = { timing, deadlineAt: deadline };
2702
3353
  const failClosed = async (): Promise<boolean> => {
2703
- await recordTerminalUncertain(broker, id, root, pid);
3354
+ await recordTerminalUncertain(broker, id, root, pid, expected);
2704
3355
  return false;
2705
3356
  };
2706
3357
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
@@ -5262,6 +5913,9 @@ async function executeLifecycleResponse(
5262
5913
  if (record) await appendSessionDeletedEvidence(broker, record);
5263
5914
  return completion;
5264
5915
  }
5916
+ if (operation === "session.reconcile_uncertain") {
5917
+ return await executeUncertainRetirement(broker, input, identity, undefined);
5918
+ }
5265
5919
  return fail("invalid_input", "Unknown lifecycle operation.");
5266
5920
  }
5267
5921
 
@@ -5394,6 +6048,11 @@ export async function executeLifecycle(
5394
6048
  let proofBudget = lifecycleProofBudgetFromInput(broker, input);
5395
6049
  if (!proofBudget)
5396
6050
  proofBudget = lifecycleProofBudgetFromEffectIntent(broker, broker.ledger.get(identity)?.effectIntent);
6051
+ if (operation === "session.reconcile_uncertain" && cleanup && isLifecycleRetirementCleanup(cleanup)) {
6052
+ const shapeValidation = validateLifecycleCleanupShape(cleanup);
6053
+ if (shapeValidation) return { response: shapeValidation };
6054
+ return { response: await executeUncertainRetirement(broker, input, identity, cleanup) };
6055
+ }
5397
6056
  if (cleanup?.phase === "metadata") {
5398
6057
  if (operation !== "session.delete")
5399
6058
  return {