pi-session-continuity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/docs/deploys/README.md +18 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0001-package-skeleton.md +53 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0002-core-artifact-engine.md +39 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0003-manual-checkpoint.md +38 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0004-status-settings.md +38 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0005-automatic-threshold.md +37 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0006-compaction-hygiene.md +38 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0007-tests-smoke.md +38 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0008-public-docs-release-readiness.md +37 -0
- package/docs/deploys/deploy-2026-07-07-SCOPE-0009-dogfood-hardening-local-readiness.md +40 -0
- package/docs/deploys/deploy-2026-07-08-SCOPE-0010-ux-settings-menu.md +41 -0
- package/docs/deploys/deploy-2026-07-08-SCOPE-0011-compact-status-footer.md +26 -0
- package/docs/deploys/deploy-2026-07-08-SCOPE-0012-remove-settings-show.md +20 -0
- package/docs/deploys/deploy-2026-07-08-SCOPE-0014-status-and-effort.md +21 -0
- package/docs/deploys/deploy-2026-07-08-SCOPE-0016-codex-empty-synthesis.md +21 -0
- package/docs/product-spec.md +702 -0
- package/docs/release-readiness/local-v1-readiness-2026-07-07.md +76 -0
- package/extensions/session-continuity/index.ts +317 -0
- package/package.json +58 -0
- package/scripts/smoke/manual-checks.sh +42 -0
- package/src/artifact.ts +535 -0
- package/src/config.ts +360 -0
- package/src/constants.ts +68 -0
- package/src/handoff.ts +674 -0
- package/src/status.ts +130 -0
- package/src/trigger.ts +40 -0
package/src/status.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { buildArtifactPaths } from "./artifact.js";
|
|
5
|
+
import type { ResolvedContinuityConfig } from "./config.js";
|
|
6
|
+
import { PRODUCT_NAME } from "./constants.js";
|
|
7
|
+
import type { HandoffState } from "./handoff.js";
|
|
8
|
+
|
|
9
|
+
export interface StalePendingArtifact {
|
|
10
|
+
path: string;
|
|
11
|
+
modifiedMs: number;
|
|
12
|
+
lockPath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function findStalePendingArtifacts(
|
|
16
|
+
artifactDirectory: string,
|
|
17
|
+
sessionId: string,
|
|
18
|
+
): Promise<StalePendingArtifact[]> {
|
|
19
|
+
const paths = buildArtifactPaths(artifactDirectory, sessionId, "placeholder");
|
|
20
|
+
try {
|
|
21
|
+
const names = await readdir(paths.pendingDir);
|
|
22
|
+
const artifacts = await Promise.all(
|
|
23
|
+
names
|
|
24
|
+
.filter((name) => name.endsWith(".md"))
|
|
25
|
+
.map(async (name) => {
|
|
26
|
+
const path = join(paths.pendingDir, name);
|
|
27
|
+
const info = await stat(path);
|
|
28
|
+
const eventId = name.replace(/\.md$/, "");
|
|
29
|
+
const lockPath = join(paths.lockDir, `${eventId}.json`);
|
|
30
|
+
try {
|
|
31
|
+
await stat(lockPath);
|
|
32
|
+
return { path, modifiedMs: info.mtimeMs, lockPath };
|
|
33
|
+
} catch {
|
|
34
|
+
return { path, modifiedMs: info.mtimeMs };
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
return artifacts.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function statusHeadlineForConfig(
|
|
45
|
+
config: ResolvedContinuityConfig,
|
|
46
|
+
): string {
|
|
47
|
+
return !config.trusted
|
|
48
|
+
? `${PRODUCT_NAME}: automatic behavior disabled because project is not trusted.`
|
|
49
|
+
: !config.valid
|
|
50
|
+
? `${PRODUCT_NAME} disabled: invalid configuration in ${config.configPath}.`
|
|
51
|
+
: config.enabled
|
|
52
|
+
? `${PRODUCT_NAME}: enabled · trigger ${config.triggerAtPercent}% · keep ${config.keepRecentPercent}%.`
|
|
53
|
+
: `${PRODUCT_NAME}: disabled by configuration.`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatStatus(
|
|
57
|
+
config: ResolvedContinuityConfig,
|
|
58
|
+
state: HandoffState,
|
|
59
|
+
stalePendingPath?: string,
|
|
60
|
+
): string {
|
|
61
|
+
const lines = [
|
|
62
|
+
statusHeadlineForConfig(config),
|
|
63
|
+
"",
|
|
64
|
+
"Status",
|
|
65
|
+
`- Mode: ${config.enabled && config.valid && config.trusted ? "Enabled" : "Disabled"}`,
|
|
66
|
+
`- Trigger threshold: ${config.triggerAtPercent}% of the active context window`,
|
|
67
|
+
`- Keep after handoff: ${config.keepRecentPercent}% of the active context window`,
|
|
68
|
+
`- Synthesis model: ${config.synthesisModel}`,
|
|
69
|
+
`- Synthesis effort: ${config.synthesisEffort}`,
|
|
70
|
+
`- Artifacts: ${config.artifactDirectoryPath}`,
|
|
71
|
+
`- Active operation: ${state.activeOperation ?? "none"}`,
|
|
72
|
+
`- Last checkpoint: ${state.lastCheckpointAt ?? "none"}`,
|
|
73
|
+
`- Last artifact: ${state.lastArtifactPath ?? "none"}`,
|
|
74
|
+
`- Last failure: ${state.lastFailure ?? "none"}`,
|
|
75
|
+
];
|
|
76
|
+
if (config.disabledReason)
|
|
77
|
+
lines.push(`- Disabled reason: ${config.disabledReason}`);
|
|
78
|
+
if (stalePendingPath)
|
|
79
|
+
lines.push(`- Stale pending artifact: ${stalePendingPath}`);
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatSettings(config: ResolvedContinuityConfig): string {
|
|
84
|
+
return [
|
|
85
|
+
`${PRODUCT_NAME}: settings`,
|
|
86
|
+
"",
|
|
87
|
+
"Config",
|
|
88
|
+
`- Enabled: ${config.enabled ? "on" : "off"}`,
|
|
89
|
+
`- Trigger threshold: ${config.triggerAtPercent}%`,
|
|
90
|
+
`- Keep after handoff: ${config.keepRecentPercent}%`,
|
|
91
|
+
`- Synthesis model: ${config.synthesisModel}`,
|
|
92
|
+
`- Synthesis effort: ${config.synthesisEffort}`,
|
|
93
|
+
`- Artifact directory: ${config.artifactDirectory}`,
|
|
94
|
+
`- Resolved artifacts: ${config.artifactDirectoryPath}`,
|
|
95
|
+
`- Config path: ${config.configPath}`,
|
|
96
|
+
`- Disabled reason: ${config.disabledReason ?? "none"}`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function notificationLevelForMessage(
|
|
101
|
+
message: string,
|
|
102
|
+
): "info" | "warning" {
|
|
103
|
+
const headline = message.split("\n")[0]?.toLowerCase() ?? "";
|
|
104
|
+
return headline.includes("disabled") ||
|
|
105
|
+
headline.includes("failed") ||
|
|
106
|
+
headline.includes("invalid") ||
|
|
107
|
+
headline.includes("untrusted")
|
|
108
|
+
? "warning"
|
|
109
|
+
: "info";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function footerStatusForMessage(message: string): string {
|
|
113
|
+
const headline = message.split("\n")[0] ?? "";
|
|
114
|
+
const threshold = headline.match(/trigger (\d+)% · keep (\d+)%/);
|
|
115
|
+
if (threshold) return `Session Continuation @ ${threshold[1]}%`;
|
|
116
|
+
const normalized = headline.toLowerCase();
|
|
117
|
+
if (normalized.includes("settings")) return "PSC settings";
|
|
118
|
+
if (normalized.includes("checkpoint") || normalized.includes("handoff"))
|
|
119
|
+
return "PSC handoff";
|
|
120
|
+
if (normalized.includes("failed")) return "PSC failed";
|
|
121
|
+
if (normalized.includes("disabled")) return "PSC disabled";
|
|
122
|
+
return "PSC ready";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function notifyStatus(ctx: ExtensionContext, message: string): void {
|
|
126
|
+
const level = notificationLevelForMessage(message);
|
|
127
|
+
ctx.ui.setWidget("session-continuity", undefined);
|
|
128
|
+
ctx.ui.notify(message, level);
|
|
129
|
+
ctx.ui.setStatus("session-continuity", footerStatusForMessage(message));
|
|
130
|
+
}
|
package/src/trigger.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ResolvedContinuityConfig } from "./config.js";
|
|
2
|
+
import { shouldTriggerHandoff } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export interface ContextUsageSnapshot {
|
|
5
|
+
tokens: number | null;
|
|
6
|
+
contextWindow: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AutomaticTriggerDecision =
|
|
10
|
+
| { shouldRun: true; reason: "threshold-reached" }
|
|
11
|
+
| {
|
|
12
|
+
shouldRun: false;
|
|
13
|
+
reason:
|
|
14
|
+
| "disabled"
|
|
15
|
+
| "invalid-config"
|
|
16
|
+
| "untrusted"
|
|
17
|
+
| "usage-unavailable"
|
|
18
|
+
| "window-unavailable"
|
|
19
|
+
| "below-threshold";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function decideAutomaticTrigger(
|
|
23
|
+
config: ResolvedContinuityConfig,
|
|
24
|
+
usage: ContextUsageSnapshot | undefined,
|
|
25
|
+
): AutomaticTriggerDecision {
|
|
26
|
+
if (!config.trusted) return { shouldRun: false, reason: "untrusted" };
|
|
27
|
+
if (!config.valid) return { shouldRun: false, reason: "invalid-config" };
|
|
28
|
+
if (!config.enabled) return { shouldRun: false, reason: "disabled" };
|
|
29
|
+
if (!usage) return { shouldRun: false, reason: "usage-unavailable" };
|
|
30
|
+
if (usage.tokens === null || usage.contextWindow <= 0) {
|
|
31
|
+
return { shouldRun: false, reason: "window-unavailable" };
|
|
32
|
+
}
|
|
33
|
+
return shouldTriggerHandoff(
|
|
34
|
+
usage.tokens,
|
|
35
|
+
usage.contextWindow,
|
|
36
|
+
config.triggerAtPercent,
|
|
37
|
+
)
|
|
38
|
+
? { shouldRun: true, reason: "threshold-reached" }
|
|
39
|
+
: { shouldRun: false, reason: "below-threshold" };
|
|
40
|
+
}
|