@astrosheep/square 0.3.71 → 0.3.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact.d.ts +0 -3
- package/dist/artifact.js +5 -59
- package/dist/catch-decisions.d.ts +7 -1
- package/dist/catch-decisions.js +21 -8
- package/dist/claude-delivery.d.ts +1 -1
- package/dist/claude-delivery.js +7 -3
- package/dist/cli/ledger-command.d.ts +12 -0
- package/dist/cli/ledger-command.js +68 -0
- package/dist/cli/registry.js +2 -0
- package/dist/help.js +7 -2
- package/dist/host-ledger-file-adapter.d.ts +4 -10
- package/dist/host-ledger-file-adapter.js +238 -198
- package/dist/host-ledger-migration.d.ts +26 -0
- package/dist/host-ledger-migration.js +227 -0
- package/dist/host-ledger-sqlite.d.ts +62 -0
- package/dist/host-ledger-sqlite.js +341 -0
- package/dist/host-ledger.d.ts +13 -4
- package/dist/ledger-maintenance.d.ts +30 -0
- package/dist/ledger-maintenance.js +110 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
- package/dist/packages/agent-delivery/src/index.d.ts +2 -42
- package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
- package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
- package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
- package/dist/packages/agent-delivery/src/types.d.ts +42 -0
- package/dist/participant-host.d.ts +6 -3
- package/dist/participant-host.js +44 -38
- package/dist/ports.d.ts +7 -1
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -1
- package/dist/routes.js +1 -10
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +12 -6
- package/dist/sqlite-transaction.d.ts +5 -0
- package/dist/sqlite-transaction.js +72 -0
- package/dist/square-actions.js +26 -22
- package/dist/square-application.js +2 -14
- package/dist/square-migration.js +2 -1
- package/dist/square-sqlite.d.ts +2 -2
- package/dist/square-sqlite.js +15 -4
- package/guides/catch-contention-performance.md +157 -0
- package/guides/change-waits.md +1 -1
- package/guides/host-ledger.md +53 -6
- package/guides/indexed-host-ledger-performance.md +39 -0
- package/guides/postcommit-simplification-performance.md +36 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/client/square-message.tsx +51 -40
- package/plugins/paseo/shared/message.ts +1 -0
- package/plugins/pi/square-pi.js +2 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HostLedgerPort } from './host-ledger.js';
|
|
2
|
+
/** The two host-ledger capabilities maintenance uses; no parallel ledger contract is owned here. */
|
|
3
|
+
type MaintenanceLedger = Pick<HostLedgerPort, 'listEvidence' | 'retireEvidenceIfUnchanged'>;
|
|
4
|
+
export interface LedgerMaintenanceOptions {
|
|
5
|
+
/** Prove eligible rows without retiring anything. */
|
|
6
|
+
readonly dryRun?: boolean;
|
|
7
|
+
readonly now?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Counts are retired rows, not reclaimed bytes: SQLite reuses freed storage without necessarily
|
|
11
|
+
* shrinking its file. Retired Claude correlations leave indefinite recognition tokens; total
|
|
12
|
+
* token count remains unbounded. Maintenance does not run VACUUM.
|
|
13
|
+
*/
|
|
14
|
+
export interface LedgerMaintenanceReport {
|
|
15
|
+
readonly dryRun: boolean;
|
|
16
|
+
/** Old rows whose closure proof passed; exact-record CAS may still refuse. */
|
|
17
|
+
readonly eligible: number;
|
|
18
|
+
/** Rows actually removed by this run; always 0 in a dry run. */
|
|
19
|
+
readonly retired: number;
|
|
20
|
+
/** Eligible rows whose full-record compare-and-swap refused; always 0 in a dry run. */
|
|
21
|
+
readonly casMisses: number;
|
|
22
|
+
/** Canonical locations whose artifact could not be read; their rows were retained. */
|
|
23
|
+
readonly unavailableArtifacts: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/** Command/application entry: root selection uses the caller's captured environment. */
|
|
26
|
+
export declare function gcHostLedger(options?: LedgerMaintenanceOptions & {
|
|
27
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
28
|
+
}): Promise<LedgerMaintenanceReport>;
|
|
29
|
+
export declare function maintainHostLedger(ledger: MaintenanceLedger, options?: LedgerMaintenanceOptions): Promise<LedgerMaintenanceReport>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createArtifactPort } from './artifact.js';
|
|
2
|
+
import { canonicalPath } from './canonical-path.js';
|
|
3
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
4
|
+
import { validateName } from './model.js';
|
|
5
|
+
import { hostLedgerForEnv } from './registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Explicit host-ledger evidence maintenance.
|
|
8
|
+
*
|
|
9
|
+
* Old wake/presentation rows are retired only when a typed artifact snapshot proves the
|
|
10
|
+
* recipient's attention is permanently closed: the activity reached them historically, and
|
|
11
|
+
* either catch recorded a receipt or the recipient joined again past it. Everything else is
|
|
12
|
+
* retained. Artifacts are read directly, one snapshot per canonical location, outside any
|
|
13
|
+
* ledger lock; retirement is the ledger's exact-record compare-and-swap. No SQL lives here.
|
|
14
|
+
*/
|
|
15
|
+
/** A row is old enough to consider once its landing time is at least this far behind now. */
|
|
16
|
+
const RETIREMENT_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
/** The canonical activity coordinate the row names; leading zeros and other spellings are invalid. */
|
|
18
|
+
function activityIndex(activity) {
|
|
19
|
+
const index = parseActivityId(activity);
|
|
20
|
+
return index === undefined || formatActivityId(index) !== activity ? undefined : index;
|
|
21
|
+
}
|
|
22
|
+
/** Old, durably closed-looking wake/presentation rows with usable coordinates. */
|
|
23
|
+
function isCandidate(record, now) {
|
|
24
|
+
if (record.kind !== 'wake' && record.kind !== 'presentation')
|
|
25
|
+
return false;
|
|
26
|
+
if (record.outcome === 'dispatching')
|
|
27
|
+
return false;
|
|
28
|
+
if (typeof record.at !== 'number' || !Number.isFinite(record.at) || record.at > now - RETIREMENT_AGE_MS)
|
|
29
|
+
return false;
|
|
30
|
+
if (typeof record.location !== 'string' || record.location.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
validateName(record.participant);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One artifact snapshot for one location either proves each candidate closed or reports the
|
|
42
|
+
* whole location unavailable. A missing, unreadable, or non-artifact path returns undefined
|
|
43
|
+
* and its rows are retained.
|
|
44
|
+
*/
|
|
45
|
+
async function provenClosable(location, group) {
|
|
46
|
+
const artifact = createArtifactPort(location);
|
|
47
|
+
try {
|
|
48
|
+
return (await artifact.read((facts) => group.filter((candidate) => {
|
|
49
|
+
const participant = candidate.record.participant;
|
|
50
|
+
if (facts.directedAt(participant, candidate.index) === undefined)
|
|
51
|
+
return false;
|
|
52
|
+
if (facts.receipt(participant, candidate.index) !== undefined)
|
|
53
|
+
return true;
|
|
54
|
+
const lastJoin = facts.participant(participant)?.lastJoinIndex;
|
|
55
|
+
return lastJoin !== undefined && lastJoin > candidate.index;
|
|
56
|
+
}))).value;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await artifact.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Command/application entry: root selection uses the caller's captured environment. */
|
|
66
|
+
export function gcHostLedger(options = {}) {
|
|
67
|
+
return maintainHostLedger(hostLedgerForEnv(options.env ?? process.env), options);
|
|
68
|
+
}
|
|
69
|
+
export async function maintainHostLedger(ledger, options = {}) {
|
|
70
|
+
const dryRun = options.dryRun === true;
|
|
71
|
+
const now = options.now ?? Date.now();
|
|
72
|
+
const records = await ledger.listEvidence({ includeReleased: true, includeExpired: true, now });
|
|
73
|
+
const groups = new Map();
|
|
74
|
+
for (const record of records) {
|
|
75
|
+
if (!isCandidate(record, now))
|
|
76
|
+
continue;
|
|
77
|
+
const index = activityIndex(record.activity);
|
|
78
|
+
if (index === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const location = await canonicalPath(record.location).catch(() => undefined);
|
|
81
|
+
if (location === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
const group = groups.get(location);
|
|
84
|
+
if (group === undefined)
|
|
85
|
+
groups.set(location, [{ record, index }]);
|
|
86
|
+
else
|
|
87
|
+
group.push({ record, index });
|
|
88
|
+
}
|
|
89
|
+
let eligible = 0;
|
|
90
|
+
let retired = 0;
|
|
91
|
+
let casMisses = 0;
|
|
92
|
+
const unavailable = new Set();
|
|
93
|
+
for (const location of [...groups.keys()].sort()) {
|
|
94
|
+
const closable = await provenClosable(location, groups.get(location));
|
|
95
|
+
if (closable === undefined) {
|
|
96
|
+
unavailable.add(location);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
eligible += closable.length;
|
|
100
|
+
if (dryRun)
|
|
101
|
+
continue;
|
|
102
|
+
for (const candidate of closable) {
|
|
103
|
+
if (await ledger.retireEvidenceIfUnchanged(candidate.record))
|
|
104
|
+
retired += 1;
|
|
105
|
+
else
|
|
106
|
+
casMisses += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { dryRun, eligible, retired, casMisses, unavailableArtifacts: [...unavailable].sort() };
|
|
110
|
+
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import type { ClaudeDeliveryResult, ClaudeSendTextOptions, ClaudeTarget } from './types.js';
|
|
2
|
-
export interface ClaudeConnectExistingOptions {
|
|
3
|
-
readonly harness: 'claude';
|
|
4
|
-
readonly sessionId: string;
|
|
5
|
-
readonly endpoint: string;
|
|
6
|
-
readonly token?: string;
|
|
7
|
-
readonly claudeHome?: string;
|
|
8
|
-
readonly timeoutMs?: number;
|
|
9
|
-
readonly signal?: AbortSignal;
|
|
10
|
-
}
|
|
1
|
+
import type { ClaudeConnectOptions, ClaudeDeliveryResult, ClaudeSendTextOptions, ClaudeTarget } from './types.js';
|
|
11
2
|
/** Resolve an explicitly identified existing Claude native inbox; never discovers or starts one. */
|
|
12
|
-
export declare function connectExisting(options:
|
|
3
|
+
export declare function connectExisting(options: ClaudeConnectOptions): Promise<ClaudeTarget>;
|
|
13
4
|
/** Write once to Claude's next native boundary; local write is never native admission. */
|
|
14
5
|
export declare function sendText(target: ClaudeTarget, text: string, options?: ClaudeSendTextOptions): Promise<ClaudeDeliveryResult>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DeliveryError } from './errors.js';
|
|
2
|
+
import type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions } from './types.js';
|
|
3
|
+
export type { OpenCodeConnectOptions, ClaudeConnectOptions, PiConnectOptions, PaseoConnectOptions } from './types.js';
|
|
2
4
|
export interface SendOptions {
|
|
3
5
|
readonly timeoutMs?: number;
|
|
4
6
|
readonly signal?: AbortSignal;
|
|
@@ -16,48 +18,6 @@ export interface Steerable<O = SendOptions> {
|
|
|
16
18
|
export interface Queueable<O = SendOptions> {
|
|
17
19
|
queue(text: string, options?: O): Promise<Receipt>;
|
|
18
20
|
}
|
|
19
|
-
export interface OpenCodeConnectOptions {
|
|
20
|
-
readonly harness: 'opencode';
|
|
21
|
-
readonly sessionId: string;
|
|
22
|
-
readonly endpoint?: {
|
|
23
|
-
readonly url: string;
|
|
24
|
-
readonly auth?: {
|
|
25
|
-
readonly type: 'basic';
|
|
26
|
-
readonly username: string;
|
|
27
|
-
readonly password: string;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
readonly registrationFile?: string;
|
|
31
|
-
readonly timeoutMs?: number;
|
|
32
|
-
readonly signal?: AbortSignal;
|
|
33
|
-
}
|
|
34
|
-
export interface ClaudeConnectOptions {
|
|
35
|
-
readonly harness: 'claude';
|
|
36
|
-
readonly sessionId: string;
|
|
37
|
-
readonly endpoint: string;
|
|
38
|
-
readonly token?: string;
|
|
39
|
-
readonly claudeHome?: string;
|
|
40
|
-
readonly timeoutMs?: number;
|
|
41
|
-
readonly signal?: AbortSignal;
|
|
42
|
-
}
|
|
43
|
-
export interface PiConnectOptions {
|
|
44
|
-
readonly harness: 'pi';
|
|
45
|
-
readonly sessionId: string;
|
|
46
|
-
readonly endpoint: string;
|
|
47
|
-
readonly timeoutMs?: number;
|
|
48
|
-
readonly signal?: AbortSignal;
|
|
49
|
-
}
|
|
50
|
-
export interface PaseoConnectOptions {
|
|
51
|
-
readonly harness: 'paseo';
|
|
52
|
-
readonly agentId: string;
|
|
53
|
-
readonly endpoint?: string;
|
|
54
|
-
readonly password?: string;
|
|
55
|
-
readonly authHeader?: string;
|
|
56
|
-
readonly localCredential?: string;
|
|
57
|
-
readonly paseoHome?: string;
|
|
58
|
-
readonly timeoutMs?: number;
|
|
59
|
-
readonly signal?: AbortSignal;
|
|
60
|
-
}
|
|
61
21
|
export type OpenCodeAgent = Steerable<KeyedSendOptions> & Queueable<KeyedSendOptions> & {
|
|
62
22
|
readonly harness: 'opencode';
|
|
63
23
|
readonly sessionId: string;
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { OpenCodeDeliveryResult, OpenCodeTarget, OpenCodeSendTextOptions } from './types.js';
|
|
3
|
-
export interface OpenCodeConnectExistingOptions {
|
|
4
|
-
readonly harness: 'opencode';
|
|
5
|
-
readonly sessionId: string;
|
|
6
|
-
readonly endpoint?: Endpoint;
|
|
7
|
-
readonly registrationFile?: string;
|
|
8
|
-
readonly timeoutMs?: number;
|
|
9
|
-
readonly signal?: AbortSignal;
|
|
10
|
-
}
|
|
1
|
+
import type { OpenCodeConnectOptions, OpenCodeDeliveryResult, OpenCodeTarget, OpenCodeSendTextOptions } from './types.js';
|
|
11
2
|
/** Resolve an existing service and persisted session. Never starts or stops a service. */
|
|
12
|
-
export declare function connectExisting(options:
|
|
3
|
+
export declare function connectExisting(options: OpenCodeConnectOptions): Promise<OpenCodeTarget>;
|
|
13
4
|
/** Submit once through the shared native sender; HTTP metadata alone refines rejection. */
|
|
14
5
|
export declare function sendText(target: OpenCodeTarget, text: string, options?: OpenCodeSendTextOptions): Promise<OpenCodeDeliveryResult>;
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import type { PaseoDeliveryResult, PaseoSendTextOptions, PaseoTarget } from './types.js';
|
|
2
|
-
export interface PaseoConnectExistingOptions {
|
|
3
|
-
readonly harness: 'paseo';
|
|
4
|
-
readonly agentId: string;
|
|
5
|
-
readonly endpoint?: string;
|
|
6
|
-
readonly password?: string;
|
|
7
|
-
readonly authHeader?: string;
|
|
8
|
-
readonly localCredential?: string;
|
|
9
|
-
readonly paseoHome?: string;
|
|
10
|
-
readonly timeoutMs?: number;
|
|
11
|
-
readonly signal?: AbortSignal;
|
|
12
|
-
}
|
|
1
|
+
import type { PaseoConnectOptions, PaseoDeliveryResult, PaseoSendTextOptions, PaseoTarget } from './types.js';
|
|
13
2
|
/** Connect to an existing daemon and address one existing agent. Never starts or stops anything. */
|
|
14
|
-
export declare function connectPaseo(options:
|
|
3
|
+
export declare function connectPaseo(options: PaseoConnectOptions): Promise<PaseoTarget>;
|
|
15
4
|
/**
|
|
16
5
|
* Every call owns its daemon connection: one for connect to prove the endpoint
|
|
17
6
|
* and credential, and one per send, closed again as soon as the daemon answers.
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import type { PiDeliveryResult, PiSendTextOptions, PiTarget } from './types.js';
|
|
2
|
-
export
|
|
3
|
-
readonly harness: 'pi';
|
|
4
|
-
readonly sessionId: string;
|
|
5
|
-
readonly endpoint: string;
|
|
6
|
-
readonly timeoutMs?: number;
|
|
7
|
-
readonly signal?: AbortSignal;
|
|
8
|
-
}
|
|
9
|
-
export declare function connectPi(options: PiConnectExistingOptions): Promise<PiTarget>;
|
|
1
|
+
import type { PiConnectOptions, PiDeliveryResult, PiSendTextOptions, PiTarget } from './types.js';
|
|
2
|
+
export declare function connectPi(options: PiConnectOptions): Promise<PiTarget>;
|
|
10
3
|
export declare function sendPiText(target: PiTarget, text: string, options?: PiSendTextOptions): Promise<PiDeliveryResult>;
|
|
@@ -46,6 +46,48 @@ export interface PaseoSendTextOptions {
|
|
|
46
46
|
readonly signal?: AbortSignal;
|
|
47
47
|
}
|
|
48
48
|
export type SendTextOptions = OpenCodeSendTextOptions | ClaudeSendTextOptions | PiSendTextOptions | PaseoSendTextOptions;
|
|
49
|
+
export interface OpenCodeConnectOptions {
|
|
50
|
+
readonly harness: 'opencode';
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
readonly endpoint?: {
|
|
53
|
+
readonly url: string;
|
|
54
|
+
readonly auth?: {
|
|
55
|
+
readonly type: 'basic';
|
|
56
|
+
readonly username: string;
|
|
57
|
+
readonly password: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
readonly registrationFile?: string;
|
|
61
|
+
readonly timeoutMs?: number;
|
|
62
|
+
readonly signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
export interface ClaudeConnectOptions {
|
|
65
|
+
readonly harness: 'claude';
|
|
66
|
+
readonly sessionId: string;
|
|
67
|
+
readonly endpoint: string;
|
|
68
|
+
readonly token?: string;
|
|
69
|
+
readonly claudeHome?: string;
|
|
70
|
+
readonly timeoutMs?: number;
|
|
71
|
+
readonly signal?: AbortSignal;
|
|
72
|
+
}
|
|
73
|
+
export interface PiConnectOptions {
|
|
74
|
+
readonly harness: 'pi';
|
|
75
|
+
readonly sessionId: string;
|
|
76
|
+
readonly endpoint: string;
|
|
77
|
+
readonly timeoutMs?: number;
|
|
78
|
+
readonly signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
export interface PaseoConnectOptions {
|
|
81
|
+
readonly harness: 'paseo';
|
|
82
|
+
readonly agentId: string;
|
|
83
|
+
readonly endpoint?: string;
|
|
84
|
+
readonly password?: string;
|
|
85
|
+
readonly authHeader?: string;
|
|
86
|
+
readonly localCredential?: string;
|
|
87
|
+
readonly paseoHome?: string;
|
|
88
|
+
readonly timeoutMs?: number;
|
|
89
|
+
readonly signal?: AbortSignal;
|
|
90
|
+
}
|
|
49
91
|
interface OpenCodeAttempt {
|
|
50
92
|
readonly harness: 'opencode';
|
|
51
93
|
readonly sessionId: string;
|
|
@@ -12,8 +12,11 @@ export declare function processIdentity(env: NodeJS.ProcessEnv): {
|
|
|
12
12
|
channel: PresenceChannel;
|
|
13
13
|
};
|
|
14
14
|
export declare function identityRouteDraft(context: HostContext, participant: string): Promise<Omit<WakeRoute, 'updatedAt'> | undefined>;
|
|
15
|
-
|
|
15
|
+
/** One postcommit host observation; presence and route publication fail independently. */
|
|
16
|
+
export declare function publishCommittedIdentity(context: HostContext, participant: string, epoch?: number): Promise<void>;
|
|
16
17
|
export declare function retireIdentityRoute(context: HostContext, participant: string, expectedEpoch?: number): Promise<void>;
|
|
17
18
|
export declare function assertLiveOwner(context: HostContext, participant: string, expectedEpoch?: number): Promise<boolean>;
|
|
18
|
-
/**
|
|
19
|
-
export declare function ensureLocalPresence(context: HostContext, participant: string, epoch?: number): Promise<
|
|
19
|
+
/** Best-effort postcommit presence; a returned epoch also witnesses a readable host observation. */
|
|
20
|
+
export declare function ensureLocalPresence(context: HostContext, participant: string, epoch?: number): Promise<{
|
|
21
|
+
readonly epoch?: number;
|
|
22
|
+
} | undefined>;
|
package/dist/participant-host.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { currentOwner, presenceEpoch, sessionOwnsParticipant } from './registry.js';
|
|
2
2
|
import { firstHarnessSession } from './participant-identity.js';
|
|
3
|
+
import { canonicalPath } from './canonical-path.js';
|
|
3
4
|
import { defaultWakeRouteCapabilities, publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, ROUTE_FRESH_MS } from './routes.js';
|
|
4
5
|
export function processIdentity(env) {
|
|
5
6
|
const found = firstHarnessSession(env);
|
|
@@ -13,19 +14,17 @@ export async function identityRouteDraft(context, participant) {
|
|
|
13
14
|
const capabilities = context.hostLedger === undefined ? { canUse: () => false } : await defaultWakeRouteCapabilities(context.hostLedger);
|
|
14
15
|
return resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const owner = await readParticipantOwner(context.location, participant, hostLedger);
|
|
21
|
-
return owner === undefined ? undefined : presenceEpoch(owner);
|
|
22
|
-
}
|
|
23
|
-
export async function publishIdentityRoute(context, participant, epoch) {
|
|
24
|
-
const route = await identityRouteDraft(context, participant);
|
|
25
|
-
if (route === undefined)
|
|
17
|
+
/** One postcommit host observation; presence and route publication fail independently. */
|
|
18
|
+
export async function publishCommittedIdentity(context, participant, epoch) {
|
|
19
|
+
const observed = await ensureLocalPresence(context, participant, epoch).catch(() => undefined);
|
|
20
|
+
if (observed === undefined)
|
|
26
21
|
return;
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
try {
|
|
23
|
+
const route = await identityRouteDraft(context, participant);
|
|
24
|
+
if (route !== undefined)
|
|
25
|
+
await publishWakeRoute(context.artifact, { ...route, ...observed }, { at: context.clock(), requireCurrentSession: true });
|
|
26
|
+
}
|
|
27
|
+
catch { /* Optional route enrichment cannot undo the committed activity. */ }
|
|
29
28
|
}
|
|
30
29
|
export async function retireIdentityRoute(context, participant, expectedEpoch) {
|
|
31
30
|
if (context.location === undefined || context.location === 'memory')
|
|
@@ -43,35 +42,42 @@ export async function assertLiveOwner(context, participant, expectedEpoch) {
|
|
|
43
42
|
return true;
|
|
44
43
|
return sessionOwnsParticipant(context.location, participant, identity.session, hostLedger, expectedEpoch);
|
|
45
44
|
}
|
|
46
|
-
/**
|
|
45
|
+
/** Best-effort postcommit presence; a returned epoch also witnesses a readable host observation. */
|
|
47
46
|
export async function ensureLocalPresence(context, participant, epoch) {
|
|
48
47
|
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
49
48
|
return;
|
|
50
49
|
const identity = processIdentity(context.env ?? process.env);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
// Semantic publish: an unexpired presence row already stands when it carries the same owner epoch.
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
let existing;
|
|
54
52
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
existing = await context.hostLedger.listPresence({ location: await canonicalPath(context.location), participant, now });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
process.stderr.write(`! host presence degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const owner = currentOwner(existing);
|
|
60
|
+
const ownerEpoch = epoch ?? (owner === undefined ? undefined : presenceEpoch(owner));
|
|
61
|
+
const observed = ownerEpoch === undefined ? {} : { epoch: ownerEpoch };
|
|
62
|
+
// A fresh exact local binding at the observed (or explicit claim) generation already stands.
|
|
63
|
+
if (existing.some((row) => row.session === identity.session && row.channel === identity.channel
|
|
64
|
+
&& now - (row.updatedAt ?? 0) < ROUTE_FRESH_MS
|
|
65
|
+
&& (ownerEpoch === undefined || presenceEpoch(row) === ownerEpoch)))
|
|
66
|
+
return observed;
|
|
67
|
+
try {
|
|
68
|
+
const result = await context.hostLedger.ensurePresence({
|
|
69
|
+
location: context.location,
|
|
70
|
+
participant,
|
|
71
|
+
session: identity.session,
|
|
72
|
+
channel: identity.channel,
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
...(ownerEpoch === undefined || ownerEpoch <= 0 ? {} : { epoch: ownerEpoch }),
|
|
75
|
+
});
|
|
76
|
+
if (result.status === 'degraded')
|
|
77
|
+
throw result.error;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
process.stderr.write(`! host presence degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
60
81
|
}
|
|
61
|
-
|
|
62
|
-
const result = await context.hostLedger.ensurePresence({
|
|
63
|
-
location: context.location,
|
|
64
|
-
participant,
|
|
65
|
-
session: identity.session,
|
|
66
|
-
channel: identity.channel,
|
|
67
|
-
// Presence rows are host-ledger wall-time evidence; the square clock belongs to artifact activities.
|
|
68
|
-
updatedAt: Date.now(),
|
|
69
|
-
...(ownerEpoch === undefined || ownerEpoch <= 0 ? {} : { epoch: ownerEpoch }),
|
|
70
|
-
}).catch((error) => ({
|
|
71
|
-
status: 'degraded',
|
|
72
|
-
record: { location: context.location, participant, session: identity.session, channel: identity.channel },
|
|
73
|
-
error,
|
|
74
|
-
}));
|
|
75
|
-
if (result.status === 'degraded')
|
|
76
|
-
process.stderr.write(`! host presence degraded: ${result.error instanceof Error ? result.error.message : String(result.error)}\n`);
|
|
82
|
+
return observed;
|
|
77
83
|
}
|
package/dist/ports.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Act, HardCap, InboxNotification, StoredAct, WakeRoute, WakeRouteKind, WatchLease } from './model.js';
|
|
2
|
-
export type { ClaimResult, EvidenceClaim, EvidenceRelease,
|
|
2
|
+
export type { ClaimResult, EvidenceClaim, EvidenceRelease, EvidenceLookup, EvidenceRecord, NativeTokenLookup, NativeTokenResult, HostLedgerPort, PresenceKey, PresenceLookup, PresenceRecord, PresenceClaimResult, PresenceChannel, PresenceResult, } from './host-ledger.js';
|
|
3
3
|
import type { HostLedgerPort } from './host-ledger.js';
|
|
4
4
|
export type ActivityIndex = number;
|
|
5
5
|
export interface SquareHeader {
|
|
@@ -20,6 +20,11 @@ export interface ParticipantFact {
|
|
|
20
20
|
readonly lastActiveAt?: number;
|
|
21
21
|
readonly lastJoinIndex?: number;
|
|
22
22
|
}
|
|
23
|
+
export interface ParticipantCounts {
|
|
24
|
+
readonly known: number;
|
|
25
|
+
readonly joined: number;
|
|
26
|
+
readonly done: number;
|
|
27
|
+
}
|
|
23
28
|
export interface AttentionFact {
|
|
24
29
|
readonly listener: string;
|
|
25
30
|
readonly target: string;
|
|
@@ -98,6 +103,7 @@ export interface RouteFact {
|
|
|
98
103
|
export interface SquareRead {
|
|
99
104
|
header(): SquareHeader;
|
|
100
105
|
participant(requestedName: string): ParticipantFact | undefined;
|
|
106
|
+
participantCounts(): ParticipantCounts;
|
|
101
107
|
roster(): readonly ParticipantFact[];
|
|
102
108
|
attention(listener: string): readonly AttentionFact[];
|
|
103
109
|
incomingAttention(sender: string): readonly AttentionFact[];
|
package/dist/registry.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { type FileHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
3
3
|
import type { HostLedgerPort, PresenceChannel, PresenceRecord } from './host-ledger.js';
|
|
4
4
|
export declare function presenceEpoch(record: PresenceRecord | undefined): number;
|
|
5
|
+
/** The current owner: the highest epoch wins, then the most recently updated row. */
|
|
6
|
+
export declare function currentOwner(rows: readonly PresenceRecord[]): PresenceRecord | undefined;
|
|
5
7
|
export declare function localParticipantName(squarePath: string, hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
6
8
|
export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
|
|
7
9
|
export type OwnershipClaim = {
|
package/dist/registry.js
CHANGED
|
@@ -8,7 +8,7 @@ export function presenceEpoch(record) {
|
|
|
8
8
|
return typeof record?.epoch === 'number' && Number.isSafeInteger(record.epoch) && record.epoch > 0 ? record.epoch : 0;
|
|
9
9
|
}
|
|
10
10
|
/** The current owner: the highest epoch wins, then the most recently updated row. */
|
|
11
|
-
function currentOwner(rows) {
|
|
11
|
+
export function currentOwner(rows) {
|
|
12
12
|
return [...rows].sort((left, right) => presenceEpoch(right) - presenceEpoch(left) || (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
|
|
13
13
|
}
|
|
14
14
|
export async function localParticipantName(squarePath, hostLedger, env = process.env) { const canonical = await canonicalPath(squarePath); const names = new Set((await Promise.all(localSessionIdentities(env).map(async (identity) => (await hostLedger.listPresence({ location: canonical, session: identity.sessionId })).map((row) => row.participant)))).flat()); return names.size === 1 ? [...names][0] : undefined; }
|
package/dist/routes.js
CHANGED
|
@@ -3,15 +3,6 @@ import { canonicalPath, canonicalPathSync } from './canonical-path.js';
|
|
|
3
3
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
4
4
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
5
5
|
export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
6
|
-
let userCapable = hostLedger !== undefined;
|
|
7
|
-
if (hostLedger !== undefined) {
|
|
8
|
-
try {
|
|
9
|
-
await hostLedger.listPresence({ now: Date.now() });
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
userCapable = false;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
6
|
const available = new Set();
|
|
16
7
|
try {
|
|
17
8
|
const { CodexQueueAdapter } = await import('./codex-queue.js');
|
|
@@ -23,7 +14,7 @@ export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
|
23
14
|
available.add(new PaseoAdapter().kind);
|
|
24
15
|
}
|
|
25
16
|
catch { /* optional */ }
|
|
26
|
-
return { canUse: (kind, address) =>
|
|
17
|
+
return { canUse: (kind, address) => hostLedger !== undefined && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
|
|
27
18
|
}
|
|
28
19
|
export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
|
|
29
20
|
const paseoAgentId = env.PASEO_AGENT_ID?.trim();
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ActivityId } from './square-core.js';
|
|
2
2
|
import { type StoredAct, type WatchLease } from './model.js';
|
|
3
|
-
import type { SquareRead } from './ports.js';
|
|
3
|
+
import type { ParticipantCounts, ParticipantFact, SquareRead } from './ports.js';
|
|
4
4
|
export declare const WATCH_HEARTBEAT_MS: number;
|
|
5
5
|
export declare const SLEEP_MS: number;
|
|
6
6
|
export declare const STALE_MS: number;
|
|
@@ -13,6 +13,7 @@ export declare function nowMs(): number;
|
|
|
13
13
|
export declare function rosterNames(facts: SquareRead): string[];
|
|
14
14
|
export declare function inSquareCount(facts: SquareRead): number;
|
|
15
15
|
export declare function actId(actOrIndex: StoredAct | number): ActivityId;
|
|
16
|
+
export declare function terminalStatusFor(current: ParticipantFact | undefined, hardCap: number | null, counts: ParticipantCounts): 'capped' | 'quorum' | undefined;
|
|
16
17
|
export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
|
|
17
18
|
/** A continuous prefix stops at the first unseen historical audience edge after join. */
|
|
18
19
|
export declare function readCursor(facts: SquareRead, name: string): number;
|
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatActivityId } from './square-core.js';
|
|
2
|
-
import { SquareError
|
|
2
|
+
import { SquareError } from './model.js';
|
|
3
3
|
function parseIntegerEnv(name, fallback) {
|
|
4
4
|
const raw = process.env[name];
|
|
5
5
|
if (raw === undefined)
|
|
@@ -27,14 +27,20 @@ export function nowMs() {
|
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
29
|
export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
|
|
30
|
-
export function inSquareCount(facts) { return facts.
|
|
30
|
+
export function inSquareCount(facts) { return facts.participantCounts().joined; }
|
|
31
31
|
export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
|
|
32
|
+
export function terminalStatusFor(current, hardCap, counts) {
|
|
33
|
+
if (hardCap !== null && (current?.sayCount ?? 0) >= hardCap)
|
|
34
|
+
return 'capped';
|
|
35
|
+
const peers = counts.known - Number(current !== undefined);
|
|
36
|
+
const donePeers = counts.done - Number(current?.done === true);
|
|
37
|
+
return peers > 0 && donePeers === peers ? 'quorum' : undefined;
|
|
38
|
+
}
|
|
32
39
|
export function watchTerminalStatus(facts, name) {
|
|
33
|
-
const cap = facts.header().hardCap;
|
|
34
|
-
if (cap !== null && (
|
|
40
|
+
const cap = facts.header().hardCap, current = facts.participant(name);
|
|
41
|
+
if (cap !== null && (current?.sayCount ?? 0) >= cap)
|
|
35
42
|
return 'capped';
|
|
36
|
-
|
|
37
|
-
return peers.length > 0 && peers.every((peer) => peer.done) ? 'quorum' : undefined;
|
|
43
|
+
return terminalStatusFor(current, cap, facts.participantCounts());
|
|
38
44
|
}
|
|
39
45
|
/** A continuous prefix stops at the first unseen historical audience edge after join. */
|
|
40
46
|
export function readCursor(facts, name) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/** Yield rather than blocking the process while another connection owns SQLite. */
|
|
3
|
+
export declare function retrySqliteBusy<T>(operation: () => T, signal?: AbortSignal, retryMs?: number | (() => number)): Promise<T>;
|
|
4
|
+
/** Acquisition may retry; COMMIT retries retain the callback's result and transaction. */
|
|
5
|
+
export declare function inSqliteTransaction<T, V>(db: DatabaseSync, write: boolean, validate: () => V, change: (validated: V) => T, signal?: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
function abort(signal) {
|
|
3
|
+
if (signal?.aborted)
|
|
4
|
+
throw signal.reason ?? new Error('SQLite operation aborted');
|
|
5
|
+
}
|
|
6
|
+
function isBusy(error) {
|
|
7
|
+
return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
8
|
+
}
|
|
9
|
+
/** Yield rather than blocking the process while another connection owns SQLite. */
|
|
10
|
+
export async function retrySqliteBusy(operation, signal, retryMs = 25) {
|
|
11
|
+
for (;;) {
|
|
12
|
+
abort(signal);
|
|
13
|
+
try {
|
|
14
|
+
return operation();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!isBusy(error))
|
|
18
|
+
throw error;
|
|
19
|
+
try {
|
|
20
|
+
await sleep(typeof retryMs === 'function' ? retryMs() : retryMs, undefined, { signal });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
abort(signal);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Acquisition may retry; COMMIT retries retain the callback's result and transaction. */
|
|
30
|
+
export async function inSqliteTransaction(db, write, validate, change, signal) {
|
|
31
|
+
let begun = false, backoffMs = 25;
|
|
32
|
+
// Spread writer contenders out without delaying a writer that already owns the lock.
|
|
33
|
+
const writerDelay = () => {
|
|
34
|
+
const delay = 1 + Math.floor(Math.random() * backoffMs);
|
|
35
|
+
backoffMs = Math.min(100, backoffMs * 2);
|
|
36
|
+
return delay;
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const validated = await retrySqliteBusy(() => {
|
|
40
|
+
db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
|
|
41
|
+
begun = true;
|
|
42
|
+
try {
|
|
43
|
+
return validate();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
db.exec('ROLLBACK');
|
|
47
|
+
begun = false;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}, signal, write ? writerDelay : 25);
|
|
51
|
+
abort(signal);
|
|
52
|
+
const result = change(validated);
|
|
53
|
+
if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
|
|
54
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
55
|
+
throw new TypeError('SQLite transaction callbacks must be synchronous.');
|
|
56
|
+
}
|
|
57
|
+
// A busy commit still owns the writer lock. Retry promptly once readers leave;
|
|
58
|
+
// acquisition contenders keep the longer backoff above.
|
|
59
|
+
await retrySqliteBusy(() => db.exec('COMMIT'), signal, 1);
|
|
60
|
+
begun = false;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (begun) {
|
|
65
|
+
try {
|
|
66
|
+
db.exec('ROLLBACK');
|
|
67
|
+
}
|
|
68
|
+
catch { /* retain original failure */ }
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|