@gajae-code/coding-agent 0.15.2 → 0.15.4
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/CHANGELOG.md +43 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +65 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +9 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +66 -1
- package/src/config/settings.ts +1 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +473 -66
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +54 -6
- 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
|
-
|
|
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(
|
|
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 {
|