Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
1 parent
056bed7aeb
commit
45bfae6e94
543 files changed
+92949
No files matched your search
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Repo for schedule runtime state: intent and state are separate — the file is
|
||||
* declarative intent, this table only records runtime state such as
|
||||
* "fired before / last fired / missed / disabled" plus the creator.
|
||||
*
|
||||
* Identity rule: a change to `start_at` is treated as a new task instance
|
||||
* (registerOrSync resets the trigger state); a change to the file content fingerprint
|
||||
* only clears the disabled flag (the file becomes effective again after reconciliation).
|
||||
*/
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
export interface ScheduleStateRow {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
name: string;
|
||||
creatorUserId: string | null;
|
||||
startAtMs: number;
|
||||
defHash: string;
|
||||
lastSlotMs: number | null;
|
||||
lastFiredAt: string | null;
|
||||
firedOnce: boolean;
|
||||
missed: boolean;
|
||||
invalidReason: string | null;
|
||||
}
|
||||
|
||||
function mapRow(r: Record<string, unknown>): ScheduleStateRow {
|
||||
return {
|
||||
projectId: r.project_id as string,
|
||||
agentId: r.agent_id as string,
|
||||
name: r.name as string,
|
||||
creatorUserId: (r.creator_user_id as string | null) ?? null,
|
||||
startAtMs: Number(r.start_at_ms),
|
||||
defHash: r.def_hash as string,
|
||||
lastSlotMs: r.last_slot_ms === null ? null : Number(r.last_slot_ms),
|
||||
lastFiredAt: (r.last_fired_at as string | null) ?? null,
|
||||
firedOnce: Number(r.fired_once) === 1,
|
||||
missed: Number(r.missed) === 1,
|
||||
invalidReason: (r.invalid_reason as string | null) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export class SchedulesRepo {
|
||||
constructor(private readonly db: DatabaseSync) {}
|
||||
|
||||
find(projectId: string, agentId: string, name: string): ScheduleStateRow | null {
|
||||
const r = this.db
|
||||
.prepare("SELECT * FROM schedule_state WHERE project_id = ? AND agent_id = ? AND name = ?")
|
||||
.get(projectId, agentId, name);
|
||||
return r ? mapRow(r as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
listByAgent(projectId: string, agentId: string): ScheduleStateRow[] {
|
||||
const rows = this.db
|
||||
.prepare("SELECT * FROM schedule_state WHERE project_id = ? AND agent_id = ? ORDER BY name")
|
||||
.all(projectId, agentId);
|
||||
return rows.map((r) => mapRow(r as Record<string, unknown>));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register or sync a task's runtime state, returning the synced row plus a `fresh`
|
||||
* flag:
|
||||
* - Insert if it doesn't exist (creator is only persisted at this point; a hand-edited
|
||||
* file gets registered via reconciliation, with creator falling back to the Project
|
||||
* owner);
|
||||
* - A change to `start_at` resets the trigger state (a new task instance);
|
||||
* - Otherwise, a change to the file fingerprint only clears the disabled flag.
|
||||
* `fresh` = this call was an insert or reset — the scheduler only establishes its
|
||||
* "missed, don't backfill" baseline at this moment; afterward, last_slot being NULL
|
||||
* only means "no scheduled time has been consumed yet", and it must fire normally once
|
||||
* reached.
|
||||
*/
|
||||
registerOrSync(args: {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
name: string;
|
||||
startAtMs: number;
|
||||
defHash: string;
|
||||
creatorUserId: string | null;
|
||||
}): { row: ScheduleStateRow; fresh: boolean } {
|
||||
const existing = this.find(args.projectId, args.agentId, args.name);
|
||||
let fresh = false;
|
||||
if (!existing) {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO schedule_state
|
||||
(project_id, agent_id, name, creator_user_id, start_at_ms, def_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
args.projectId,
|
||||
args.agentId,
|
||||
args.name,
|
||||
args.creatorUserId,
|
||||
args.startAtMs,
|
||||
args.defHash,
|
||||
);
|
||||
fresh = true;
|
||||
} else if (existing.startAtMs !== args.startAtMs) {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state
|
||||
SET start_at_ms = ?, def_hash = ?, last_slot_ms = NULL, last_fired_at = NULL,
|
||||
fired_once = 0, missed = 0, invalid_reason = NULL
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(args.startAtMs, args.defHash, args.projectId, args.agentId, args.name);
|
||||
fresh = true;
|
||||
} else if (existing.defHash !== args.defHash) {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state SET def_hash = ?, invalid_reason = NULL
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(args.defHash, args.projectId, args.agentId, args.name);
|
||||
}
|
||||
const row = this.find(args.projectId, args.agentId, args.name);
|
||||
if (!row) throw new Error("schedule_state 登记后读取失败");
|
||||
return { row, fresh };
|
||||
}
|
||||
|
||||
/** Advance the consumed scheduled time (advances whether triggered or skipped; restarts don't re-trigger). */
|
||||
markSlot(projectId: string, agentId: string, name: string, slotMs: number): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state SET last_slot_ms = ?
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(slotMs, projectId, agentId, name);
|
||||
}
|
||||
|
||||
/** Record an actual send (also sets fired_once for a one-shot task). */
|
||||
markFired(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
name: string,
|
||||
firedAt: string,
|
||||
oneShot: boolean,
|
||||
): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state SET last_fired_at = ?, fired_once = CASE WHEN ? THEN 1 ELSE fired_once END
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(firedAt, oneShot ? 1 : 0, projectId, agentId, name);
|
||||
}
|
||||
|
||||
/** Missed marker for a one-shot task (the scheduled time had already passed at startup/registration reconciliation; missed means not backfilled). */
|
||||
markMissed(projectId: string, agentId: string, name: string): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state SET missed = 1
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(projectId, agentId, name);
|
||||
}
|
||||
|
||||
/** Mark as disabled (e.g. the bound Session was deleted); cleared via registerOrSync after the file is modified. */
|
||||
markInvalid(projectId: string, agentId: string, name: string, reason: string): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE schedule_state SET invalid_reason = ?
|
||||
WHERE project_id = ? AND agent_id = ? AND name = ?`,
|
||||
)
|
||||
.run(reason, projectId, agentId, name);
|
||||
}
|
||||
|
||||
/** Deleting the file removes the task: clears its runtime state. */
|
||||
delete(projectId: string, agentId: string, name: string): void {
|
||||
this.db
|
||||
.prepare("DELETE FROM schedule_state WHERE project_id = ? AND agent_id = ? AND name = ?")
|
||||
.run(projectId, agentId, name);
|
||||
}
|
||||
|
||||
/** Reconciliation cleanup: deletes state rows under this Agent that aren't in the current file list, returning the removed names. */
|
||||
deleteMissing(projectId: string, agentId: string, presentNames: string[]): string[] {
|
||||
const rows = this.listByAgent(projectId, agentId);
|
||||
const present = new Set(presentNames);
|
||||
const removed: string[] = [];
|
||||
for (const row of rows) {
|
||||
if (!present.has(row.name)) {
|
||||
this.delete(projectId, agentId, row.name);
|
||||
removed.push(row.name);
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
deleteByProject(projectId: string): void {
|
||||
this.db.prepare("DELETE FROM schedule_state WHERE project_id = ?").run(projectId);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user