Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/services/write-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* WriteResult — the one contract every write tool answers with (GIT-101).
*
* create_thread established it (GIT-63/GIT-67, R3/R4): say where the record
* landed and whether it survives this machine, and only call it a success
* when it reached the store of truth for this tier. Other write tools grew
* their own shapes — resolve_thread returned success: true whether or not the
* durable store accepted the write — so a caller could not tell a saved
* record from a lost one without reading prose.
*
* durable the record is in Supabase (pro/dev). Never true on free.
* stored_in "supabase" | "local" (free: the local store IS the truth) |
* "local_only" (pro: kept on this machine only, the durable
* write failed) | null (stored nowhere).
* success durable || !hasSupabase() — and false whenever nothing was
* stored at all.
*/

import { hasSupabase } from "./tier.js";
import type { WriteResult, StoredIn } from "../types/index.js";

export type { WriteResult, StoredIn };

/**
* The outcome of a write.
*
* @param durable the durable store (Supabase) accepted the write
* @param storedLocally a local copy exists (threads.json, the free-tier
* store, session state). Default: true on free, where
* reaching this call means the local write succeeded;
* false on pro, where a failed durable write keeps
* nothing unless the tool says it did.
*/
export function writeResult(durable: boolean, storedLocally: boolean = !hasSupabase()): WriteResult {
if (durable && hasSupabase()) return { success: true, durable: true, stored_in: "supabase" };
if (!hasSupabase()) {
return storedLocally
? { success: true, durable: false, stored_in: "local" }
: notStored();
}
return { success: false, durable: false, stored_in: storedLocally ? "local_only" : null };
}

/** Nothing was stored anywhere. */
export function notStored(): WriteResult {
return { success: false, durable: false, stored_in: null };
}
15 changes: 8 additions & 7 deletions src/tools/archive-learning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { getStorage } from "../services/storage.js";
import { flushCache } from "../services/startup.js";
import { Timer } from "../services/metrics.js";
import { wrapDisplay } from "../services/display-protocol.js";
import { writeResult, notStored } from "../services/write-result.js";
import type { WriteResult } from "../types/index.js";

export interface ArchiveLearningParams {
/** UUID or short ID prefix of the learning to archive */
Expand All @@ -24,8 +26,7 @@ export interface ArchiveLearningParams {
reason?: string;
}

export interface ArchiveLearningResult {
success: boolean;
export interface ArchiveLearningResult extends WriteResult {
id: string;
archived_at?: string;
reason?: string;
Expand Down Expand Up @@ -95,7 +96,7 @@ export async function archiveLearning(params: ArchiveLearningParams): Promise<Ar
if (!params.id || typeof params.id !== "string") {
const msg = "Missing required parameter: id (UUID or short ID prefix of the learning to archive)";
return {
success: false,
...notStored(),
id: "",
cache_flushed: false,
display: wrapDisplay(msg),
Expand All @@ -108,7 +109,7 @@ export async function archiveLearning(params: ArchiveLearningParams): Promise<Ar
const resolved = await resolveIdPrefix(params.id);
if ("error" in resolved) {
return {
success: false,
...notStored(),
id: params.id,
cache_flushed: false,
display: wrapDisplay(resolved.error),
Expand Down Expand Up @@ -147,7 +148,7 @@ export async function archiveLearning(params: ArchiveLearningParams): Promise<Ar
if (!existing) {
const msg = `Learning ${resolvedId} not found in local storage`;
return {
success: false,
...notStored(),
id: resolvedId,
cache_flushed: false,
display: wrapDisplay(msg),
Expand All @@ -172,7 +173,7 @@ export async function archiveLearning(params: ArchiveLearningParams): Promise<Ar
const display = `Archived learning ${idDisplay}.${reasonText}\n(${latencyMs}ms)`;

return {
success: true,
...writeResult(hasSupabase()),
id: resolvedId,
archived_at: archivedAt,
reason: params.reason,
Expand All @@ -184,7 +185,7 @@ export async function archiveLearning(params: ArchiveLearningParams): Promise<Ar
const message = error instanceof Error ? error.message : String(error);
const latencyMs = timer.stop();
return {
success: false,
...notStored(),
id: resolvedId,
cache_flushed: false,
display: wrapDisplay(`Failed to archive learning: ${message}`),
Expand Down
5 changes: 3 additions & 2 deletions src/tools/create-decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
CreateDecisionResult,
PerformanceBreakdown,
} from "../types/index.js";
import { writeResult, notStored } from "../services/write-result.js";

/**
* Execute create_decision tool
Expand Down Expand Up @@ -141,7 +142,7 @@ export async function createDecision(
}).catch(() => {});

return {
success: true,
...writeResult(hasSupabase()),
decision_id: decisionId,
performance: perfData,
display: wrapDisplay(`Decision logged: "${params.title}"\nID: ${decisionId}`),
Expand All @@ -151,7 +152,7 @@ export async function createDecision(
const latencyMs = timer.stop();
const perfData = buildPerformanceData("create_decision", latencyMs, 0);
return {
success: false,
...notStored(),
decision_id: "",
performance: perfData,
display: wrapDisplay(`Failed to log decision`),
Expand Down
7 changes: 4 additions & 3 deletions src/tools/create-learning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
PerformanceBreakdown,
ComponentPerformance,
} from "../types/index.js";
import { writeResult, notStored } from "../services/write-result.js";

/**
* Validate scar-specific requirements
Expand Down Expand Up @@ -85,7 +86,7 @@ export async function createLearning(
const latencyMs = timer.stop();
const perfData = buildPerformanceData("create_learning", latencyMs, 0);
return {
success: false,
...notStored(),
learning_id: "",
embedding_generated: false,
errors,
Expand Down Expand Up @@ -314,7 +315,7 @@ export async function createLearning(
const se = params.severity ? (SEV[params.severity] || "") + " " : "";

return {
success: true,
...writeResult(hasSupabase()),
learning_id: learningId,
embedding_generated: embeddingGenerated,
performance: perfData,
Expand All @@ -327,7 +328,7 @@ export async function createLearning(
const latencyMs = timer.stop();
const perfData = buildPerformanceData("create_learning", latencyMs, 0);
return {
success: false,
...notStored(),
learning_id: "",
embedding_generated: false,
errors: [errorMsg],
Expand Down
14 changes: 5 additions & 9 deletions src/tools/create-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
import { wrapDisplay, truncate } from "../services/display-protocol.js";
import { formatThreadForDisplay } from "../services/timezone.js";
import type { ThreadWithEmbedding } from "../services/thread-dedup.js";
import type { ThreadObject, PerformanceData, Project } from "../types/index.js";
import type { ThreadObject, PerformanceData, Project, WriteResult, StoredIn } from "../types/index.js";

// --- Types ---

Expand Down Expand Up @@ -73,21 +73,17 @@ export type NotStoredReason =
/**
* Where a thread actually landed (R3: every response names its store).
* `local_only` is the honest middle state from R4 — written somewhere, but
* not somewhere durable.
* not somewhere durable. GIT-101: this contract is now shared by every write
* tool (WriteResult).
*/
export type StoredIn = "supabase" | "local" | "local_only" | null;
export type { StoredIn } from "../types/index.js";

export interface CreateThreadResult {
success: boolean;
export interface CreateThreadResult extends WriteResult {
/**
* Whether a row exists. Distinct from `success` on purpose: a dedup refusal
* is a well-formed answer to a well-formed request, not a crash.
*/
stored: boolean;
/** Whether what was stored survives this machine. */
durable: boolean;
/** R3: names the store in-band, which also kills the f104e10d silent-local trap. */
stored_in: StoredIn;
reason?: NotStoredReason;
thread?: ThreadObject;
error?: string;
Expand Down
19 changes: 13 additions & 6 deletions src/tools/promote-suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
buildPerformanceData,
} from "../services/metrics.js";
import type { ThreadObject, PerformanceData, Project, ThreadSuggestion } from "../types/index.js";
import { writeResult, notStored } from "../services/write-result.js";
import type { WriteResult } from "../types/index.js";

// --- Types ---

Expand All @@ -30,8 +32,7 @@ export interface PromoteSuggestionParams {
project?: Project;
}

export interface PromoteSuggestionResult {
success: boolean;
export interface PromoteSuggestionResult extends WriteResult {
thread?: ThreadObject;
suggestion?: ThreadSuggestion;
error?: string;
Expand All @@ -49,7 +50,7 @@ export async function promoteSuggestion(
if (!params.suggestion_id) {
const latencyMs = timer.stop();
return {
success: false,
...notStored(),
error: "suggestion_id is required",
performance: buildPerformanceData("promote_suggestion" as any, latencyMs, 0),
display: wrapDisplay(`Failed: suggestion_id is required`),
Expand All @@ -64,7 +65,7 @@ export async function promoteSuggestion(
if (!target) {
const latencyMs = timer.stop();
return {
success: false,
...notStored(),
error: `Pending suggestion not found: "${params.suggestion_id}"`,
performance: buildPerformanceData("promote_suggestion" as any, latencyMs, 0),
display: wrapDisplay(`Suggestion not found: ${params.suggestion_id}`),
Expand All @@ -79,8 +80,12 @@ export async function promoteSuggestion(

if (!threadResult.success || !threadResult.thread) {
const latencyMs = timer.stop();
// GIT-101: pass create_thread's own outcome through (a local_only thread
// is not a promotion), rather than restating it.
return {
success: false,
...notStored(),
durable: threadResult.durable,
stored_in: threadResult.stored_in,
error: `Thread creation failed: ${threadResult.error || "unknown"}`,
performance: buildPerformanceData("promote_suggestion" as any, latencyMs, 0),
display: wrapDisplay(`Failed to promote suggestion`),
Expand All @@ -93,7 +98,9 @@ export async function promoteSuggestion(

const latencyMs = timer.stop();
return {
success: true,
success: threadResult.success,
durable: threadResult.durable,
stored_in: threadResult.stored_in,
thread: threadResult.thread,
suggestion: target,
performance: buildPerformanceData("promote_suggestion" as any, latencyMs, 1),
Expand Down
11 changes: 8 additions & 3 deletions src/tools/record-scar-usage-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ScarUsageEntry,
Project,
} from "../types/index.js";
import { writeResult, notStored } from "../services/write-result.js";

const TARGET_LATENCY_MS = 2000; // Target for batch operation

Expand Down Expand Up @@ -98,9 +99,13 @@ export async function recordScarUsageBatch(
const metricsId = uuidv4();

if (!hasSupabase()) {
// Free tier: scar usage tracked locally via record-scar-usage (single), not batch
// Free tier: scar usage tracked locally via record-scar-usage (single), not batch.
// GIT-101: nothing is stored here, and the result says so (stored_in: null);
// success stays true because there is no durable store to have missed.
return {
success: true,
durable: false,
stored_in: null,
usage_ids: [],
resolved_count: 0,
failed_count: 0,
Expand Down Expand Up @@ -191,7 +196,7 @@ export async function recordScarUsageBatch(
});

return {
success: true,
...writeResult(hasSupabase()),
usage_ids: usageIds,
resolved_count: resolvedCount,
failed_count: failedIdentifiers.length,
Expand All @@ -205,7 +210,7 @@ export async function recordScarUsageBatch(
console.error("[record-scar-usage-batch] Error recording batch:", error);

return {
success: false,
...notStored(),
error: error instanceof Error ? error.message : String(error),
usage_ids: usageIds,
resolved_count: resolvedCount,
Expand Down
5 changes: 3 additions & 2 deletions src/tools/record-scar-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
RecordScarUsageResult,
PerformanceBreakdown,
} from "../types/index.js";
import { writeResult, notStored } from "../services/write-result.js";

/**
* Execute record_scar_usage tool
Expand Down Expand Up @@ -101,7 +102,7 @@ export async function recordScarUsage(
}).catch(() => {});

return {
success: true,
...writeResult(hasSupabase()),
usage_id: usageId,
performance: perfData,
display: wrapDisplay(`Scar usage recorded\nID: ${usageId}`),
Expand All @@ -112,7 +113,7 @@ export async function recordScarUsage(
const latencyMs = timer.stop();
const perfData = buildPerformanceData("record_scar_usage", latencyMs, 0);
return {
success: false,
...notStored(),
usage_id: "",
errors: [errorMsg],
performance: perfData,
Expand Down
Loading
Loading